Navigation dans les projets multi-modules



La navigation dans le développement d'applications Android est très importante et vous devriez réfléchir à deux fois à ce qui convient le mieux à la bibliothèque (ou à votre propre solution) et à la façon dont elle sera pratique à utiliser lorsque l'application deviendra plus grande. En outre, il peut être utile de réfléchir à la facilité avec laquelle il sera possible de remplacer votre implémentation par une autre.

Avant de commencer, permettez-moi de raconter une histoire. Appelons cela comme ceci "Comment nous avons rendu le projet modulaire et pourquoi j'ai détesté notre navigation."

Nous avions un projet de module unique et tout fonctionnait bien sauf le temps de construction, c'est parce que nous avons utilisé Dagger et qu'il a fallu trop de temps pour générer un seul composant. Nous avons donc décidé de séparer le projet en modules par fonctionnalités, par exemple, une fonctionnalité de connexion, une fonctionnalité d'aide, etc.

Nous avons eu beaucoup de difficultés car nous avons dû déplacer la logique réseau dans son propre module Gradle, la connexion au domaine dans son module et nous avons également eu des problèmes de navigation.

Dans notre implémentation, nous avions un routeur qui connaissait chaque fragment et qui était responsable des transitions entre eux. Malheureusement, chaque Fragment savait qu'il y avait le routeur et connaissait d'autres Fragments, car le Fragment a dit au routeur quel Fragment devait être ouvert ou le Fragment attendait les résultats d'un autre Fragment. Ces choses ont ruiné l'idée de créer des modules de fonctionnalités indépendants.

Par conséquent, j'ai pensé comment l'améliorer, comment rompre ces connexions entre les fragments, comment se débarrasser de la connaissance qu'il existe un routeur. Dans cet article, je vais montrer ce que j'ai fait.

Ce que nous ferons



L'application sera super simple mais elle sera modulaire. Il y aura un module d'application et trois modules de fonctionnalités:

  • Module d' application - ce module sait tout sur l'application et là, nous mettrons en œuvre notre navigation. Nous utiliserons le composant de navigation de JetPack.
  • Module Questions - ce module est responsable de la fonction questions. Le module ne sait rien du module App ou de tout autre module. Il y aura un QuestionsFragment et une interface QuestionsNavigation .
  • Module de question - il y aura une fonction de question. Comme le module précédent, il ne connaît pas les autres modules. Ce module contiendra un QuestionFragment et une interface QuestionNavigation .
  • Module de résultat - c'est une fonction de résultat. De plus, c'est un module complètement indépendant. Il y aura un RightAnswerFragment avec RightAnswerNavigation interface RightAnswerNavigation et il y aura un WrongAnswerFragment avec une interface WrongAnswerNavigation .

Le code source de l'application peut être trouvé ici: GitHub .

Implémentation


Dans cet article, j'utiliserai la bibliothèque ComponentsManager pour obtenir la navigation de la fonctionnalité. Dans un vrai projet, je fournirai la navigation des fonctionnalités par Dagger mais pour ce petit projet ce n'est pas nécessaire. De plus, la bibliothèque ComponentManager vous aide à utiliser Dagger dans des projets multi-modules.

Êtes-vous prêt? C'est parti!

Module Questions


Créez un module Android et appelez-le des questions . Après cela, assurez-vous que Kotlin est configuré dans le module et que ConstraintLayout et ComponentsManager sont ajoutés en tant que dépendances.

 //build.gradle (for the questions module) apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { ... } dependencies { ... implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0-alpha01' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation "com.github.valeryponomarenko.componentsmanager:androidx:2.0.1" ... } 

Au lieu d'utiliser Route ou Navigator dans le Fragment , nous allons créer une interface QuestionsNavigation qui définit les transitions nécessaires. Ainsi, pour le Fragment, peu importe comment ces transitions seront implémentées, il a juste besoin de cette interface et dépend du moment où la méthode est appelée, l'écran nécessaire sera ouvert.

 interface QuestionsNavigation { fun openQuestion(questionId: Long) } 

Pour ouvrir un écran avec une question, nous appelons simplement la openQuestion(questionId: Long) et c'est tout. Nous ne nous soucions pas de la façon dont l'écran sera ouvert, nous ne nous soucions même pas s'il s'agit d'un Fragment ou d'une Activity ou autre chose.

Voici le QuestionsFragment et sa mise en page .

 class QuestionsFragment : Fragment() { private val navigation: QuestionsNavigation by lazy { XInjectionManager.findComponent<QuestionsNavigation>() } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_questions, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) button_first_question.setOnClickListener { navigation.openQuestion(1) } button_second_question.setOnClickListener { navigation.openQuestion(2) } button_third_question.setOnClickListener { navigation.openQuestion(3) } } } 

Module de questions


Tout d'abord, créez un module de bibliothèque Android et appelez-le question . Le module build.gradle du module doit contenir les mêmes dépendances que le fichier build.gradle du module question.

 //build.gradle (for the question module) apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { ... } dependencies { ... implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0-alpha01' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation "com.github.valeryponomarenko.componentsmanager:androidx:2.0.1" ... } 

Après cela, nous créons une interface qui définit la navigation du module. L'idée de navigation dans les autres modules sera la même que dans le précédent.

À partir de QuestionFragment utilisateur peut ouvrir un mauvais écran de réponse ou un bon écran de réponse afin que l'interface ait deux méthodes.

 interface QuestionNavigation { fun openWrongAnswer() fun openRightAnswer() } 

La dernière chose est un fragment. Pour ce fragment, nous ajouterons une méthode complémentaire qui renvoie un Bundle afin que nous puissions passer l'ID de la question et l'utiliser. La disposition du fragment.

 class QuestionFragment : Fragment() { companion object { private const val EXTRA_QUESTION_ID = "me.vponomarenko.modular.navigation.question.id" fun createBundle(questionId: Long) = Bundle().apply { putLong(EXTRA_QUESTION_ID, questionId) } } private val navigation: QuestionNavigation by lazy { XInjectionManager.findComponent<QuestionNavigation>() } private val questionId: Long by lazy { arguments?.getLong(EXTRA_QUESTION_ID) ?: throw IllegalStateException("no questionId") } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_question, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) text_question.text = getString(R.string.question, questionId) button_right_answer.setOnClickListener { navigation.openRightAnswer() } button_wrong_answer.setOnClickListener { navigation.openWrongAnswer() } } } 

Module de résultat


Le dernier module de fonctionnalités est un module de résultats. Il y aura deux fragments qui montrent une bonne et une mauvaise réponse. Créez un module de bibliothèque Android et appelez-le result , puis modifiez le build.gradle du module afin qu'il ait les mêmes dépendances que les modules de fonctionnalités précédents.

 //build.gradle (for the result module) apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { ... } dependencies { ... implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0-alpha01' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation "com.github.valeryponomarenko.componentsmanager:androidx:2.0.1" ... } 

Commençons par le bon fragment de réponse. L'interface de navigation aura une méthode, car l'utilisateur ne peut ouvrir qu'un écran toutes questions.

 interface RightAnswerNavigation { fun openAllQuestions() } 

Le RightAnswerFragment , sa disposition .

 class RightAnswerFragment : Fragment() { private val navigation: RightAnswerNavigation by lazy { XInjectionManager.findComponent<RightAnswerNavigation>() } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_right, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) button_all_questions.setOnClickListener { navigation.openAllQuestions() } } } 

Comme je l'ai dit plus tôt, ce module a deux Fragments , alors implémentons un mauvais écran de réponse.

Dans mon implémentation, à partir d'un mauvais écran de réponse, l'utilisateur ne peut que revenir à l'écran de la question et essayer de répondre à nouveau à la question, de sorte que l'interface de navigation n'a qu'une seule méthode.

 interface WrongAnswerNavigation { fun tryAgain() } 

Et le WrongAnswerFragment, sa disposition .

 class WrongAnswerFragment : Fragment() { private val navigation: WrongAnswerNavigation by lazy { XInjectionManager.findComponent<WrongAnswerNavigation>() } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_wrong, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) button_try_again.setOnClickListener { navigation.tryAgain() } } } 

Module d'application


Nous avons créé les modules et il est temps de les connecter et d'exécuter l'application.

Tout d'abord, nous devons modifier le build.gradle du module d'application. Il doit avoir tous les modules créés et une bibliothèque de composants de navigation avec le gestionnaire de composants.

 //build.gradle (for the app module) apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { ... } dependencies { ... implementation project(':questions') implementation project(':question') implementation project(':result') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0-alpha01' implementation 'com.github.valeryponomarenko.componentsmanager:androidx:2.0.1' implementation 'android.arch.navigation:navigation-fragment-ktx:1.0.0-alpha11' ... } 

Avant d'implémenter les classes qui fonctionnent avec la navigation, nous devons ajouter la navigation elle-même. Nous utiliserons le composant de navigation pour le construire.

Créez un fichier de ressources de navigation et appelez-le nav_graph.xml . Il y aura trois connexions:

  • Le QuestionsFragment avec le QuestionFragment
  • Le QuestionFragment avec le WrongAnswerFragment
  • Le QuestionFragment avec le RightAnwerFragment . Cette connexion a une petite différence. Si l'utilisateur est sur le RightAnswerFragment et que l'utilisateur appuie sur le bouton de retour, il sera renvoyé au QuestionsFragment . Comment y arriver? Sélectionnez simplement la flèche qui relie le questionFragment à rightAnswerFragment , puis dans la liste déroulante près du Pop pour sélectionner le questionsFragment .


Voici la représentation XML du graphique de navigation.

 <?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/nav_graph" app:startDestination="@id/questionsFragment"> <fragment android:id="@+id/questionsFragment" android:name="me.vponomarenko.modular.navigation.questions.QuestionsFragment" android:label="QuestionsFragment"> <action android:id="@+id/action_questionsFragment_to_questionFragment" app:destination="@id/questionFragment" /> </fragment> <fragment android:id="@+id/questionFragment" android:name="me.vponomarenko.modular.navigation.question.QuestionFragment" android:label="QuestionFragment"> <action android:id="@+id/action_questionFragment_to_wrongAnswerFragment" app:destination="@id/wrongAnswerFragment" /> <action android:id="@+id/action_questionFragment_to_rightAnswerFragment" app:destination="@id/rightAnswerFragment" app:popUpTo="@+id/questionsFragment" /> </fragment> <fragment android:id="@+id/wrongAnswerFragment" android:name="me.vponomarenko.modular.navigation.result.wrong.WrongAnswerFragment" android:label="WrongAnswerFragment" /> <fragment android:id="@+id/rightAnswerFragment" android:name="me.vponomarenko.modular.navigation.result.right.RightAnswerFragment" android:label="RightAnswerFragment" /> </navigation> 

Créez ensuite une classe Navigator . De toute évidence, la classe est responsable de faire des transitions entre les fragments. Il implémente toutes les interfaces de navigation à partir des modules fonctionnels, n'oubliez pas d'ajouter des appels des actions correspondantes pour ouvrir l'écran requis. De plus, la classe a des méthodes pour lier le navController et le navController .

 class Navigator : QuestionsNavigation, QuestionNavigation, RightAnswerNavigation, WrongAnswerNavigation { private var navController: NavController? = null override fun openQuestion(questionId: Long) { navController?.navigate( R.id.action_questionsFragment_to_questionFragment, QuestionFragment.createBundle(questionId) ) } override fun openWrongAnswer() { navController?.navigate(R.id.action_questionFragment_to_wrongAnswerFragment) } override fun openRightAnswer() { navController?.navigate(R.id.action_questionFragment_to_rightAnswerFragment) } override fun openAllQuestions() { navController?.popBackStack() } override fun tryAgain() { navController?.popBackStack() } fun bind(navController: NavController) { this.navController = navController } fun unbind() { navController = null } } 

Sur la 8 e ligne, vous pouvez voir comment je transmets l'identifiant de la QuestionFragment au QuestionFragment .

Après cela, créez une classe NavApplication . En fait, j'ai dû ajouter cette classe pour rendre Navigator disponible pour les autres classes, sinon, il serait plus difficile d'obtenir le navigateur dans les modules de fonctionnalités. N'oubliez pas d'ajouter cette classe au manifeste.

 class NavApplication : Application() { override fun onCreate() { super.onCreate() XInjectionManager.bindComponentToCustomLifecycle(object : IHasComponent<Navigator> { override fun getComponent(): Navigator = Navigator() }) } } 

Avec les 4 ème -6 ème lignes, il serait possible d'obtenir les implémentations de l'interface de navigation des fonctionnalités en appelant XInjectionManager.findComponent ().

Le dernier mais non le moindre, changez la classe MainActivity et sa disposition .

 class MainActivity : AppCompatActivity() { private val navigator: Navigator by lazy { XInjectionManager.findComponent<Navigator>() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } override fun onResume() { super.onResume() navigator.bind(findNavController(R.id.nav_host_fragment)) } override fun onPause() { super.onPause() navigator.unbind() } override fun onSupportNavigateUp(): Boolean = findNavController(R.id.nav_host_fragment).navigateUp() } 

C'est tout, vous pouvez maintenant exécuter l'application et voir comment cela fonctionne.

Résumé


Bon travail! Nous avons créé un système de navigation qui:

  • rompt les connexions entre les fragments dans différents modules de fonctionnalités,
  • peut être facilement changé,
  • est facile à tester.

Crosspost

Source: https://habr.com/ru/post/fr443288/


All Articles