
A navegação no desenvolvimento de aplicativos para Android é muito importante e você deve pensar duas vezes sobre o que a biblioteca mais se adequa (ou sua própria solução) e como será conveniente usar quando o aplicativo ficar maior. Além disso, pode ser bom pensar em como será fácil alterar sua implementação para outra.
Antes de começarmos, deixe-me contar uma história. Vamos chamar assim: "Como fizemos o projeto modular e por que eu odiava nossa navegação".
Tivemos um projeto de módulo único e tudo funcionou bem, exceto o tempo de construção, porque usamos Dagger e demorou muito para gerar um único componente. Por isso, decidimos separar o projeto em módulos por recursos, por exemplo, um recurso de login, um recurso de ajuda e etc.
Tivemos muitas dificuldades porque tivemos que mover a lógica da rede para o seu próprio módulo Gradle, fazer o login do domínio no módulo e também tivemos problemas com a navegação.
Em nossa implementação, tínhamos um roteador que conhecia cada fragmento e era responsável pelas transições entre eles. Infelizmente, todo fragmento sabia que havia o roteador e sabia de outros fragmentos, porque o fragmento dizia ao roteador qual fragmento deveria ser aberto ou o fragmento aguardava os resultados de outro fragmento. Essas coisas arruinaram a idéia de criar módulos de recursos independentes.
Portanto, estive pensando em como torná-lo melhor, em como quebrar essas conexões entre os Fragmentos, em como livrar-se do conhecimento de que existe algum roteador. Neste artigo, mostrarei o que fiz.
O que faremos
A aplicação será super simples, mas será modular. Haverá um módulo de aplicativo e três módulos de recursos:
- Módulo do aplicativo - este módulo sabe tudo sobre o aplicativo e lá implementaremos nossa navegação. Usaremos o componente de navegação do JetPack.
- Módulo de perguntas - este módulo é responsável pelo recurso de perguntas. O módulo não sabe nada sobre o módulo App ou qualquer outro módulo. Haverá uma interface
QuestionsFragment
e QuestionsNavigation
. - Módulo de perguntas - haverá um recurso de perguntas. Como o módulo anterior, ele não conhece outros módulos. Esse módulo conterá um
QuestionFragment
e uma interface QuestionNavigation
. - Módulo de resultado - esse é um recurso de resultado. Além disso, é um módulo completamente independente. Haverá uma interface
RightAnswerNavigation
com RightAnswerNavigation
e haverá uma interface WrongAnswerFragment
com uma interface WrongAnswerNavigation
.
O código fonte do aplicativo pode ser encontrado aqui:
GitHub .
Implementação
Neste artigo, usarei a biblioteca
ComponentsManager para obter a navegação do recurso. Em um projeto real, fornecerei a navegação de recursos por
Dagger, mas para este pequeno projeto não é necessário. Além disso, a biblioteca
ComponentManager ajuda você a usar o
Dagger em projetos com vários módulos.
Você está pronta? Vamos lá!
Módulo de perguntas
Crie um módulo Android e chame-o de
perguntas . Depois disso, verifique se o Kotlin está configurado no módulo e se o
ConstraintLayout e o
ComponentsManager foram adicionados como dependências.
Em vez de usar
Route
ou
Navigator
no
Fragment
, criaremos uma interface
QuestionsNavigation
que define as transições necessárias. Portanto, para o fragmento, não importa como essas transições serão implementadas, ele só precisa dessa interface e depende de quando o método for chamado, a tela necessária será aberta.
interface QuestionsNavigation { fun openQuestion(questionId: Long) }
Para abrir uma tela com uma pergunta, chamamos o
openQuestion(questionId: Long)
e é isso. Não nos importamos como a tela será aberta, nem nos importamos se é
Fragment
ou
Activity
ou outra coisa.
Aqui está o
QuestionsFragment
e seu
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) } } }
Módulo de perguntas
Primeiro, crie um módulo de biblioteca Android e chame de
pergunta . O build.gradle do módulo deve conter as mesmas dependências que o arquivo build.gradle do módulo de pergunta.
Depois disso, criamos uma interface que define a navegação do módulo. A ideia de navegação em outros módulos será a mesma que na anterior.
No usuário do
QuestionFragment
possível abrir uma tela de resposta errada ou uma tela de resposta correta, para que a interface tenha dois métodos.
interface QuestionNavigation { fun openWrongAnswer() fun openRightAnswer() }
A última coisa é fragmento. Para esse fragmento, adicionaremos um método complementar que retornará um
Bundle
para que possamos passar o ID da pergunta e usá-lo. O
layout do 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 resultado
O módulo de recurso final é um módulo de resultado. Haverá dois fragmentos que mostram uma resposta certa e errada. Crie um módulo de biblioteca do Android e chame-o de
resultado , depois altere o build.gradle do módulo para que ele tenha as mesmas dependências dos módulos de recursos anteriores.
Vamos começar pelo fragmento de resposta certa. A interface de navegação terá um método, pois o usuário pode abrir apenas uma tela de todas as perguntas.
interface RightAnswerNavigation { fun openAllQuestions() }
O
RightAnswerFragment
, seu
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() } } }
Como eu disse anteriormente, este módulo possui dois
Fragments
, então vamos implementar uma tela de resposta errada.
Na minha implementação, na tela de resposta errada, o usuário só pode voltar para a tela da pergunta e tentar responder à pergunta novamente, para que a interface de navegação tenha apenas um método.
interface WrongAnswerNavigation { fun tryAgain() }
E o WrongAnswerFragment, seu
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() } } }
Módulo de aplicação
Criamos os módulos e é hora de conectá-los e executar o aplicativo.
Primeiro, precisamos editar o build.gradle do módulo de aplicativo. Ele deve ter todos os módulos criados e uma biblioteca de Componentes de Navegação com o Gerenciador de Componentes.
Antes da implementação das classes que funcionam com a navegação, precisamos adicionar a própria navegação. Usaremos o componente de navegação para construí-lo.
Crie um arquivo de recurso de navegação e chame-o de
nav_graph.xml . Haverá três conexões:
- O
QuestionsFragment
com o QuestionFragment
- O
QuestionFragment
com o WrongAnswerFragment
- O
QuestionFragment
com o RightAnwerFragment
. Essa conexão tem uma pequena diferença. Se o usuário estiver no RightAnswerFragment
e pressionar o botão Voltar, ele retornará ao QuestionsFragment
. Como fazer isso acontecer? Basta selecionar a seta que conecta o questionFragment
ao rightAnswerFragment
, em seguida, na lista suspensa próxima ao Pop para selecionar o questionsFragment
.
Aqui está a representação XML do gráfico de navegação.
<?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>
Em seguida, crie uma classe
Navigator
. Obviamente, a classe é responsável por fazer transições entre fragmentos. Ele implementa todas as interfaces de navegação dos módulos de recursos, não se esqueça de adicionar chamadas das ações correspondentes para abrir a tela necessária. Além disso, a classe possui métodos para ligar o
navController
e
navController
-lo.
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 } }
Na 8ª linha, você pode ver como passo o ID da
QuestionFragment
para o
QuestionFragment
.
Depois disso, crie uma classe
NavApplication
. Na verdade, eu tive que adicionar essa classe para disponibilizar o
Navigator
para outras classes, caso contrário, seria mais difícil obter o navegador nos módulos de recursos. Não se esqueça de adicionar esta classe ao manifesto.
class NavApplication : Application() { override fun onCreate() { super.onCreate() XInjectionManager.bindComponentToCustomLifecycle(object : IHasComponent<Navigator> { override fun getComponent(): Navigator = Navigator() }) } }
Com as
4ª- 6ª linhas, seria possível obter as implementações da interface de navegação dos recursos chamando XInjectionManager.findComponent ().
Por último, mas não menos importante, altere a classe
MainActivity
e seu
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() }
Isso é tudo, agora você pode executar o aplicativo e ver como ele funciona.
Sumário
Bom trabalho! Criamos um sistema de navegação que:
- quebra as conexões entre fragmentos em diferentes módulos de recursos,
- pode ser facilmente alterado,
- é fácil de testar.
Crosspost