Navegação em projetos com vários módulos



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.

 //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" ... } 

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.

 //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" ... } 

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.

 //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" ... } 

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.

 //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' ... } 

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

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


All Articles