
Die Navigation bei der Entwicklung von Android-Apps ist sehr wichtig, und Sie sollten sich zweimal überlegen, welche Bibliothek (oder Ihre eigene Lösung) am besten geeignet ist und wie sie bequem zu verwenden ist, wenn die App größer wird. Es kann auch sinnvoll sein, darüber nachzudenken, wie einfach es sein wird, Ihre Implementierung auf eine andere zu ändern.
Bevor wir anfangen, lassen Sie mich eine Geschichte erzählen. Nennen wir es so: "Wie wir das Projekt modular gemacht haben und warum ich unsere Navigation gehasst habe."
Wir hatten ein einzelnes Modulprojekt und alles hat gut funktioniert, außer der Bauzeit. Das liegt daran, dass wir Dagger verwendet haben und es zu lange gedauert hat, eine einzelne Komponente zu generieren. Daher haben wir beschlossen, das Projekt nach Funktionen in Module zu unterteilen, z. B. nach einer Anmeldefunktion, einer Hilfefunktion usw.
Wir hatten viele Schwierigkeiten, weil wir die Netzwerklogik in ein eigenes Gradle-Modul verschieben mussten, uns die Domain in das Modul einloggen mussten und auch Probleme mit der Navigation hatten.
In unserer Implementierung hatten wir einen Router, der über jedes Fragment Bescheid wusste und für Übergänge zwischen ihnen verantwortlich war. Leider wusste jedes Fragment, dass es den Router gab, und wusste über andere Fragmente Bescheid, da das Fragment dem Router sagte, welches Fragment geöffnet werden sollte, oder das Fragment auf die Ergebnisse eines anderen Fragments wartete. Diese Dinge ruinierten die Idee, unabhängige Funktionsmodule zu erstellen.
Daher habe ich darüber nachgedacht, wie ich es besser machen kann, wie ich diese Verbindungen zwischen Fragmenten unterbrechen kann, wie ich das Wissen loswerden kann, dass es einen Router gibt. In diesem Artikel werde ich zeigen, was ich gemacht habe.
Was wir tun werden
Die Anwendung wird sehr einfach, aber modular sein. Es wird ein Anwendungsmodul und drei Funktionsmodule geben:
- App- Modul - Dieses Modul weiß alles über die App und dort implementieren wir unsere Navigation. Wir werden die Navigationskomponente von JetPack verwenden.
- Fragenmodul - Dieses Modul ist für die Fragenfunktion verantwortlich. Das Modul weiß nichts über das App- Modul oder ein anderes Modul. Es wird eine
QuestionsFragment
und eine QuestionsNavigation
Oberfläche geben. - Fragenmodul - Es wird eine Fragenfunktion geben. Wie das vorherige Modul kennt es keine anderen Module. Dieses Modul enthält ein
QuestionFragment
und eine QuestionNavigation
Schnittstelle. - Ergebnismodul - Dies ist eine Ergebnisfunktion. Es ist auch ein völlig unabhängiges Modul. Es wird ein
RightAnswerFragment
mit RightAnswerNavigation
Schnittstelle und ein WrongAnswerFragment
mit einer WrongAnswerNavigation
Schnittstelle geben.
Den Quellcode für die App finden Sie hier:
GitHub .
Implementierung
In diesem Artikel werde ich die
ComponentsManager- Bibliothek verwenden, um die Navigation der Funktion abzurufen. In einem realen Projekt werde ich die Navigation des Features von
Dagger bereitstellen, aber für dieses kleine Projekt ist dies nicht erforderlich. Mit der
ComponentManager- Bibliothek können Sie
Dagger auch in Projekten mit mehreren Modulen verwenden.
Bist du bereit Lass uns gehen!
Fragenmodul
Erstellen Sie ein Android-Modul und nennen Sie es
Fragen . Stellen Sie anschließend sicher, dass Kotlin im Modul konfiguriert ist und das
ConstraintLayout und der
ComponentsManager als Abhängigkeiten hinzugefügt werden.
Anstatt
Route
oder
Navigator
im
Fragment
, erstellen wir eine Schnittstelle
QuestionsNavigation
, die die erforderlichen Übergänge definiert. Für das Fragment spielt es also keine Rolle, wie diese Übergänge implementiert werden. Es benötigt lediglich diese Schnittstelle und ist darauf angewiesen, dass beim Aufrufen der Methode der erforderliche Bildschirm geöffnet wird.
interface QuestionsNavigation { fun openQuestion(questionId: Long) }
Um einen Bildschirm mit einer Frage zu öffnen, rufen wir einfach die Methode
openQuestion(questionId: Long)
und
openQuestion(questionId: Long)
. Es ist uns egal, wie der Bildschirm geöffnet wird, es ist uns egal, ob es sich um
Fragment
oder
Activity
oder etwas anderes handelt.
Hier ist das
QuestionsFragment
und sein
Layout .
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) } } }
Fragenmodul
Erstellen Sie zunächst ein Android-Bibliotheksmodul und nennen Sie es
Frage . Das build.gradle des Moduls muss dieselben Abhängigkeiten enthalten wie die build.gradle-Datei des Fragemoduls.
Danach erstellen wir eine Schnittstelle, die die Navigation des Moduls definiert. Die Navigationsidee in anderen Modulen ist dieselbe wie im vorherigen.
Über das
QuestionFragment
kann der Benutzer einen falschen oder einen richtigen Antwortbildschirm öffnen, sodass die Benutzeroberfläche über zwei Methoden verfügt.
interface QuestionNavigation { fun openWrongAnswer() fun openRightAnswer() }
Das Letzte ist Fragment. Für dieses Fragment fügen wir eine Begleitmethode hinzu, die ein
Bundle
zurückgibt, damit wir die ID der Frage übergeben und verwenden können. Das
Layout des Fragments.
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() } } }
Ergebnismodul
Das letzte Funktionsmodul ist ein Ergebnismodul. Es wird zwei Fragmente geben, die eine richtige und eine falsche Antwort zeigen. Erstellen Sie ein Android-Bibliotheksmodul und rufen Sie es als
Ergebnis auf . Ändern Sie dann das build.gradle des Moduls so, dass es dieselben Abhängigkeiten wie die vorherigen Funktionsmodule aufweist.
Beginnen wir mit dem richtigen Antwortfragment. Die Navigationsoberfläche verfügt über eine Methode, da der Benutzer nur einen Bildschirm mit allen Fragen öffnen kann.
interface RightAnswerNavigation { fun openAllQuestions() }
Das
RightAnswerFragment
, sein
Layout .
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() } } }
Wie ich bereits sagte, hat dieses Modul zwei
Fragments
. Implementieren wir also einen falschen Antwortbildschirm.
In meiner Implementierung kann der Benutzer vom falschen Antwortbildschirm aus nur zum Bildschirm der Frage zurückkehren und versuchen, die Frage erneut zu beantworten, sodass die Navigationsoberfläche nur eine Methode hat.
interface WrongAnswerNavigation { fun tryAgain() }
Und das WrongAnswerFragment, sein
Layout .
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() } } }
App-Modul
Wir haben die Module erstellt und es ist Zeit, sie zu verbinden und die App auszuführen.
Als erstes müssen wir das build.gradle des App-Moduls bearbeiten. Es muss über alle erstellten Module und eine Navigationskomponentenbibliothek mit dem Komponentenmanager verfügen.
Vor der Implementierung der Klassen, die mit der Navigation arbeiten, müssen wir die Navigation selbst hinzufügen. Wir werden die Navigationskomponente verwenden, um sie zu erstellen.
Erstellen Sie eine Navigationsressourcendatei und nennen Sie sie
nav_graph.xml . Es wird drei Verbindungen geben:
- Das
QuestionsFragment
mit dem QuestionFragment
- Das
QuestionFragment
mit dem WrongAnswerFragment
- Das
QuestionFragment
mit dem RightAnwerFragment
. Diese Verbindung hat einen kleinen Unterschied. Befindet sich der Benutzer im RightAnswerFragment
und drückt er die Zurück-Taste, wird er zum QuestionsFragment
. Wie kann das passieren? rightAnswerFragment
einfach den Pfeil aus, der das questionFragment
mit rightAnswerFragment
verbindet questionFragment
und rightAnswerFragment
dann in der Dropdown-Liste neben dem Pop das questionsFragment
.
Hier ist die XML-Darstellung des Navigationsdiagramms.
<?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>
Erstellen Sie dann eine
Navigator
Klasse. Offensichtlich ist die Klasse für Übergänge zwischen Fragmenten verantwortlich. Es implementiert alle Navigationsschnittstellen aus den Funktionsmodulen. Vergessen Sie nicht, Aufrufe der entsprechenden Aktionen hinzuzufügen, um den erforderlichen Bildschirm zu öffnen. Außerdem verfügt die Klasse über Methoden, um den
navController
zu binden und die
navController
aufzuheben.
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 } }
In der 8. Zeile können Sie sehen, wie ich die ID der
QuestionFragment
an das
QuestionFragment
.
Erstellen Sie anschließend eine
NavApplication
Klasse. Eigentlich musste ich diese Klasse hinzufügen, um
Navigator
für andere Klassen verfügbar zu machen, sonst wäre es schwieriger, den Navigator in die Funktionsmodule zu bekommen. Vergessen Sie nicht, diese Klasse zu Manifest hinzuzufügen.
class NavApplication : Application() { override fun onCreate() { super.onCreate() XInjectionManager.bindComponentToCustomLifecycle(object : IHasComponent<Navigator> { override fun getComponent(): Navigator = Navigator() }) } }
Mit der 4.
- 6. Zeile wäre es möglich, die Implementierungen der Navigationsoberfläche von Features durch Aufrufen von XInjectionManager.findComponent () abzurufen.
Zu
MainActivity
Letzt ändern Sie die
MainActivity
Klasse und ihr
Layout .
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() }
Das ist alles, jetzt können Sie die App ausführen und sehen, wie es funktioniert.
Zusammenfassung
Gute Arbeit! Wir haben ein Navigationssystem entwickelt, das:
- unterbricht die Verbindungen zwischen Fragmenten in verschiedenen Funktionsmodulen,
- kann leicht geändert werden,
- ist leicht zu testen.
Crosspost