
La navegaci贸n en el desarrollo de aplicaciones de Android es bastante importante y debe pensar dos veces qu茅 biblioteca se adapta mejor (o su propia soluci贸n) y c贸mo ser谩 conveniente usarla cuando la aplicaci贸n se haga m谩s grande. Adem谩s, podr铆a ser bueno pensar en lo f谩cil que ser谩 cambiar su implementaci贸n a otra.
Antes de comenzar, d茅jame contar una historia. Llam茅moslo as铆: "C贸mo hicimos el proyecto modular y por qu茅 odiaba nuestra navegaci贸n".
Ten铆amos un proyecto de m贸dulo 煤nico y todo funcion贸 bien, excepto el tiempo de construcci贸n, eso se debe a que usamos Dagger y nos llev贸 demasiado tiempo generar un solo componente. As铆 que decidimos separar el proyecto en m贸dulos por caracter铆sticas, por ejemplo, una funci贸n de inicio de sesi贸n, una funci贸n de ayuda, etc.
Tuvimos muchas dificultades porque tuvimos que mover la l贸gica de red a su propio m贸dulo Gradle, el inicio de sesi贸n de dominio en su m贸dulo y tambi茅n tuvimos problemas con la navegaci贸n.
En nuestra implementaci贸n, ten铆amos un enrutador que conoc铆a cada Fragmento y era responsable de las transiciones entre ellos. Desafortunadamente, todos los Fragmentos sab铆an que exist铆a el enrutador y sab铆an sobre otros Fragmentos, porque el Fragmento le dijo al enrutador qu茅 Fragmento deber铆a abrirse o el Fragmento estaba esperando los resultados de otro Fragmento. Estas cosas arruinaron la idea de crear m贸dulos de funciones independientes.
Por lo tanto, he estado pensando c贸mo mejorarlo, c贸mo romper estas conexiones entre los Fragmentos, c贸mo deshacerme del conocimiento de que hay alg煤n enrutador. En este art铆culo, mostrar茅 lo que he hecho.
Que haremos
La aplicaci贸n ser谩 s煤per simple pero ser谩 modular. Habr谩 un m贸dulo de aplicaci贸n y tres m贸dulos de caracter铆sticas:
- M贸dulo de aplicaci贸n : este m贸dulo sabe todo sobre la aplicaci贸n y all铆 implementaremos nuestra navegaci贸n. Usaremos el componente de navegaci贸n de JetPack.
- M贸dulo de preguntas : este m贸dulo es responsable de la funci贸n de preguntas. El m贸dulo no sabe nada sobre el m贸dulo de la aplicaci贸n o cualquier otro m贸dulo. Habr谩 un
QuestionsFragment
y una interfaz de QuestionsNavigation
. - M贸dulo de preguntas : habr谩 una funci贸n de preguntas. Como el m贸dulo anterior, no conoce otros m贸dulos. Ese m贸dulo contendr谩 un
QuestionFragment
y una interfaz QuestionNavigation
. - M贸dulo de resultados : esta es una funci贸n de resultados. Adem谩s, es un m贸dulo completamente independiente. Habr谩 un
RightAnswerFragment
con la interfaz RightAnswerNavigation
y habr谩 un WrongAnswerFragment
con una interfaz WrongAnswerNavigation
.
El c贸digo fuente de la aplicaci贸n se puede encontrar aqu铆:
GitHub .
Implementaci贸n
En este art铆culo,
usar茅 la biblioteca
ComponentsManager para obtener la navegaci贸n de la caracter铆stica. En un proyecto real, proporcionar茅 la navegaci贸n de caracter铆sticas por
Dagger, pero para este peque帽o proyecto no es necesario. Adem谩s, la biblioteca
ComponentManager le ayuda a usar
Dagger en proyectos de m贸dulos m煤ltiples.
Estas listo Vamos!
M贸dulo de preguntas
Crea un m贸dulo de Android y ll谩malo
preguntas . Despu茅s de eso, aseg煤rese de que Kotlin est茅 configurado en el m贸dulo y que
ConstraintLayout y
ComponentsManager se agreguen como dependencias.
En lugar de utilizar
Route
o
Navigator
en el
Fragment
, crearemos una interfaz de
QuestionsNavigation
que defina las transiciones necesarias. Por lo tanto, para el Fragmento, no importa c贸mo se implementar谩n esas transiciones, solo necesita esa interfaz y se basa en cuando se llama al m茅todo, la pantalla necesaria estar谩 abierta.
interface QuestionsNavigation { fun openQuestion(questionId: Long) }
Para abrir una pantalla con una pregunta, simplemente llamamos al
openQuestion(questionId: Long)
y ya est谩. No nos importa c贸mo se abrir谩 la pantalla, ni siquiera nos importa si es
Fragment
o
Activity
u otra cosa.
Aqu铆 est谩 el
QuestionsFragment
y su
dise帽o .
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) } } }
M贸dulo de preguntas
En primer lugar, cree un m贸dulo de biblioteca de Android y ll谩melo
pregunta . El build.gradle del m贸dulo debe contener las mismas dependencias que el archivo build.gradle del m贸dulo de preguntas.
Despu茅s de eso creamos una interfaz que define la navegaci贸n del m贸dulo. La idea de navegaci贸n en otros m贸dulos ser谩 la misma que en la anterior.
Desde el
QuestionFragment
usuario puede abrir una pantalla de respuesta incorrecta o una pantalla de respuesta correcta para que la interfaz tenga dos m茅todos.
interface QuestionNavigation { fun openWrongAnswer() fun openRightAnswer() }
Lo 煤ltimo es fragmento. Para este fragmento, agregaremos un m茅todo complementario que devuelve un
Bundle
para que podamos pasar la identificaci贸n de la pregunta y usarla. El
dise帽o del fragmento.
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() } } }
M贸dulo de resultados
El 煤ltimo m贸dulo de caracter铆sticas es un m贸dulo de resultados. Habr谩 dos fragmentos que muestran una respuesta correcta e incorrecta. Cree un m贸dulo de biblioteca de Android y ll谩melo
resultado , luego cambie build.gradle del m贸dulo para que tenga las mismas dependencias que los m贸dulos de caracter铆sticas anteriores.
Comencemos por el fragmento de respuesta correcta. La interfaz de navegaci贸n tendr谩 un m茅todo porque el usuario solo puede abrir una pantalla de preguntas.
interface RightAnswerNavigation { fun openAllQuestions() }
El
RightAnswerFragment
, su
dise帽o .
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() } } }
Como dije anteriormente, este m贸dulo tiene dos
Fragments
, as铆 que implementemos una pantalla de respuesta incorrecta.
En mi implementaci贸n, desde la pantalla de respuesta incorrecta, el usuario solo puede volver a la pantalla de la pregunta e intentar responderla nuevamente, por lo que la interfaz de navegaci贸n solo tiene un m茅todo.
interface WrongAnswerNavigation { fun tryAgain() }
Y el WrongAnswerFragment, su
dise帽o .
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() } } }
M贸dulo de aplicaci贸n
Hemos creado los m贸dulos y es hora de conectarlos y ejecutar la aplicaci贸n.
Lo primero es lo primero, necesitamos editar el m贸dulo build.gradle de la aplicaci贸n. Debe tener todos los m贸dulos creados y una biblioteca de componentes de navegaci贸n con el Administrador de componentes.
Antes de implementar las clases que funcionan con la navegaci贸n, tenemos que agregar la navegaci贸n en s铆. Utilizaremos el componente de navegaci贸n para construirlo.
Cree un archivo de recursos de navegaci贸n y
ll谩melo nav_graph.xml . Habr谩 tres conexiones:
- El
QuestionsFragment
con el QuestionFragment
- El
WrongAnswerFragment
QuestionFragment
con el WrongAnswerFragment
QuestionFragment
WrongAnswerFragment
- El
RightAnwerFragment
QuestionFragment
con el RightAnwerFragment
QuestionFragment
RightAnwerFragment
. Esta conexi贸n tiene una peque帽a diferencia. Si el usuario se encuentra en el RightAnswerFragment
y presiona el bot贸n Atr谩s, se lo devolver谩 al QuestionsFragment
. 驴C贸mo hacer que eso suceda? Simplemente seleccione la flecha que conecta el questionFragment
con rightAnswerFragment
, luego en la lista desplegable cerca del Pop para seleccionar el questionsFragment
.
Aqu铆 est谩 la representaci贸n XML del gr谩fico de navegaci贸n.
<?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>
Luego, crea una clase
Navigator
. Obviamente, la clase es responsable de hacer transiciones entre fragmentos. Implementa todas las interfaces de navegaci贸n desde los m贸dulos de funciones, no olvide agregar llamadas de las acciones correspondientes para abrir la pantalla requerida. Adem谩s, la clase tiene m茅todos para vincular el
navController
y desvincularlo.
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 } }
En la l铆nea 8 puede ver c贸mo paso la identificaci贸n de la
QuestionFragment
al Fragmento de
QuestionFragment
.
Despu茅s de eso, cree una clase
NavApplication
. En realidad, tuve que agregar esta clase para que
Navigator
est茅 disponible para otras clases, de lo contrario, ser铆a m谩s dif铆cil obtener el navegador en los m贸dulos de caracter铆sticas. No olvides agregar esta clase al Manifiesto.
class NavApplication : Application() { override fun onCreate() { super.onCreate() XInjectionManager.bindComponentToCustomLifecycle(object : IHasComponent<Navigator> { override fun getComponent(): Navigator = Navigator() }) } }
Con las l铆neas
4陋 a 6陋 , ser铆a posible obtener las implementaciones de la interfaz de navegaci贸n de las funciones llamando a XInjectionManager.findComponent ().
El 煤ltimo, pero no menos importante, cambia la clase
MainActivity
y su
dise帽o .
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() }
Eso es todo, ahora puedes ejecutar la aplicaci贸n y ver c贸mo funciona.
Resumen
Buen trabajo! Hemos creado un sistema de navegaci贸n que:
- rompe las conexiones entre fragmentos en diferentes m贸dulos de caracter铆sticas,
- se puede cambiar f谩cilmente
- Es f谩cil de probar.
Crosspost