Olá Habr! Apresento a você a tradução do artigo "
Desenvolvimento Android moderno com Kotlin (parte 2) ", de Mladen Rakonjac.
Nota Este artigo é uma tradução da série de artigos de Mladen Rakonjac , data do artigo: 23/09/2017. Github Começando a ler a primeira parte do SemperPeritus, descobri que o restante da parte não foi traduzido por algum motivo. Por isso, trago à sua atenção a segunda parte. O artigo acabou sendo volumoso.
"É muito difícil encontrar um projeto que cubra tudo o que há de novo em desenvolvimento para Android no Android Studio 3.0, então decidi escrevê-lo."
Neste artigo, analisaremos o seguinte:
- Android Studio 3 beta 1 Parte 1
- Linguagem de Programação Kotlin - Parte 1
- Parte 1 das opções de compilação
- Parte 1 do ConstraintLayout
- Biblioteca de Ligação de Dados - Parte 1
- Arquitetura MVVM + Repositório + Padrão de Wrappers do Android Manager
- RxJava2 e como isso nos ajuda na arquitetura da Parte 3
- Adaga 2.11, o que é injeção de dependência, por que você deve usar esta Parte 4
- Retrofit (com Rx Java2)
- Quarto (com Rx Java2)
Arquitetura MVVM + Repositório + Padrão de Wrappers do Android Manager
Algumas palavras sobre arquitetura no mundo Android
Por algum tempo, os desenvolvedores do Android não usam nenhuma arquitetura em seus projetos. Nos últimos três anos, muita publicidade surgiu à sua volta na comunidade de desenvolvedores do Android. O tempo da Atividade de Deus já passou e o Google publicou o repositório
Android Architecture Blueprints , com muitos exemplos e instruções sobre várias abordagens arquitetônicas. Finalmente, no Google IO '17, eles introduziram o
Android Architecture Components , uma coleção de bibliotecas projetadas para nos ajudar a criar código mais limpo e melhorar os aplicativos.
O componente diz que você pode usar todos eles, ou apenas um deles. No entanto, eu achei todos eles realmente úteis. Mais adiante, no texto e nas partes seguintes, vamos usá-los. Primeiro, abordarei o problema no código e depois refatorarei o uso desses componentes e bibliotecas para ver quais problemas eles devem resolver.
Existem dois
padrões arquiteturais principais que compartilham o código da GUI:
É difícil dizer qual é o melhor. Você deve tentar os dois e decidir. Prefiro o
MVVM usando componentes com reconhecimento do ciclo de vida e escreverei sobre isso. Se você nunca tentou usar o MVP, há muitos bons artigos no Medium sobre isso.
O que é um padrão MVVM?
MVVM é um
padrão arquitetural que se expande como um Model-View-ViewModel. Eu acho que esse nome confunde os desenvolvedores. Se eu descobrisse o nome dele, eu o chamaria View-ViewModel-Model, porque o
ViewModel está no meio, conectando
View e
Model .
View é uma abstração para
Activity ,
Fragment ou qualquer outra View customizada (
Android Custom View ). Observe que é importante não confundir essa
visualização com a visualização Android.
A visão deve ser burra, não devemos escrever nenhuma lógica para ela.
A exibição não deve conter dados. Ele deve armazenar uma referência à instância do
ViewModel e todos os dados necessários ao
View devem vir daí. Além disso, o
View deve observar esses dados e o layout deve mudar quando os dados do
ViewModel forem alterados. Para resumir, o
View é responsável pelo seguinte: exibição de layout para vários dados e estados.
ViewModel é um nome abstrato para uma classe que contém dados e lógica, quando esses dados devem ser recebidos e quando mostrados.
ViewModel armazena o
estado atual.
O ViewModel também armazena um link para um ou mais
modelos e recebe todos os dados deles. Ela não deve saber, por exemplo, de onde os dados vieram, do banco de dados ou do servidor. Além disso, o
ViewModel não precisa saber nada sobre o
View . Além disso, o
ViewModel não deve saber nada sobre a estrutura do Android.Modelo é o nome abstrato da camada que prepara os dados para o
ViewModel . Essa é a classe na qual receberemos dados do servidor e os armazenaremos em cache ou os armazenaremos em um banco de dados local. Observe que essas não são as mesmas classes de Usuário, Carro, Quadrado, outras classes de modelo que simplesmente armazenam dados. Como regra, esta é uma implementação do modelo de Repositório, que consideraremos mais adiante.
O modelo não deve saber nada sobre o
ViewModel .
O MVVM , se implementado corretamente, é uma ótima maneira de quebrar seu código e torná-lo mais testável. Isso nos ajuda a seguir os princípios do
SOLID , portanto, nosso código é mais fácil de manter.
Exemplo de código
Agora vou escrever um exemplo simples mostrando como isso funciona.
Para começar, vamos criar um
modelo simples que retorna uma linha:
RepoModel.ktclass RepoModel { fun refreshData() : String { return "Some new data" } }
Normalmente, o recebimento de dados é uma chamada
assíncrona , portanto, devemos esperar por eles. Para simular isso, mudei a classe para o seguinte:
RepoModel.kt class RepoModel { fun refreshData(onDataReadyCallback: OnDataReadyCallback) { Handler().postDelayed({ onDataReadyCallback.onDataReady("new data") },2000) } } interface OnDataReadyCallback { fun onDataReady(data : String) }
Criei a interface
OnDataReadyCallback
com o método
onDataReady
. E agora o método
refreshData
implementa (implementa)
OnDataReadyCallback
. Para simular a espera, eu uso o
Handler
. A
onDataReady
2 segundos, o método
onDataReady
será chamado nas classes que implementam a interface
OnDataReadyCallback
.
Vamos criar um
ViewModel :
MainViewModel.kt class MainViewModel { var repoModel: RepoModel = RepoModel() var text: String = "" var isLoading: Boolean = false }
Como você pode ver, há uma instância de
RepoModel
,
text
, que será mostrado, e a variável
isLoading
, que armazena o estado atual. Vamos criar um método de
refresh
responsável pela recuperação de dados:
MainViewModel.kt class MainViewModel { ... val onDataReadyCallback = object : OnDataReadyCallback { override fun onDataReady(data: String) { isLoading.set(false) text.set(data) } } fun refresh(){ isLoading.set(true) repoModel.refreshData(onDataReadyCallback) } }
O método
refresh
chama
refreshData
no
RepoModel
, que usa uma implementação
OnDataReadyCallback
em argumentos. Ok, mas o que é um
object
? Sempre que você quiser implementar uma interface ou herdar uma classe estendida sem subclassificar, você utilizará uma
declaração de objeto . E se você quiser usar isso como uma classe anônima? Nesse caso, você está usando a
expressão do objeto :
MainViewModel.kt class MainViewModel { var repoModel: RepoModel = RepoModel() var text: String = "" var isLoading: Boolean = false fun refresh() { repoModel.refreshData( object : OnDataReadyCallback { override fun onDataReady(data: String) { text = data }) } }
Quando chamamos
refresh
, devemos alterar a exibição para o estado de
carregamento e, quando os dados chegarem, defina
isLoading
como
false
.
Também devemos substituir o
text
por
ObservableField<String>
, e
isLoading
on
ObservableField<Boolean>
.
ObservableField
é uma classe da biblioteca Data Binding que podemos usar em vez de criar um objeto Observable, que envolve o objeto que queremos observar.
MainViewModel.kt class MainViewModel { var repoModel: RepoModel = RepoModel() val text = ObservableField<String>() val isLoading = ObservableField<Boolean>() fun refresh(){ isLoading.set(true) repoModel.refreshData(object : OnDataReadyCallback { override fun onDataReady(data: String) { isLoading.set(false) text.set(data) } }) } }
Observe que eu uso
val em vez de
var , porque apenas alteraremos o valor no campo, mas não o próprio campo. E se você deseja inicializá-lo, use o seguinte:
initobserv.kt val text = ObservableField("old data") val isLoading = ObservableField(false)
Vamos mudar nosso layout para que ele possa observar o
texto e
isLoading . Para começar, ligue o
MainViewModel em vez do
Repository :
activity_main.xml <data> <variable name="viewModel" type="me.mladenrakonjac.modernandroidapp.MainViewModel" /> </data>
Então:
- Altere o TextView para observar o texto do MainViewModel
- Adicione uma ProgressBar, que ficará visível apenas se isLoading true
- Botão Adicionar, que, quando clicado, chamará o método de atualização de MainViewModel e será clicável apenas se isLoading false
main_activity.xml ... <TextView android:id="@+id/repository_name" android:text="@{viewModel.text}" ... /> ... <ProgressBar android:id="@+id/loading" android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}" ... /> <Button android:id="@+id/refresh_button" android:onClick="@{() -> viewModel.refresh()}" android:clickable="@{viewModel.isLoading ? false : true}" /> ...
Se você executar agora,
View.VISIBLE and View.GONE cannot be used if View is not imported
. Bem, vamos importar:
main_activity.xml <data> <import type="android.view.View"/> <variable name="viewModel" type="me.fleka.modernandroidapp.MainViewModel" /> </data>
Ok, feito com o layout. Agora termine com a encadernação. Como eu disse, o
View
deve ter uma instância do
ViewModel
:
MainActivity.kt class MainActivity : AppCompatActivity() { lateinit var binding: ActivityMainBinding var mainViewModel = MainViewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.viewModel = mainViewModel binding.executePendingBindings() } }
Finalmente, podemos correr
Você pode ver que
os dados antigos são substituídos por
novos .
Este foi um exemplo simples do MVVM.
Mas há um problema, vamos virar a tela
dados antigos substituíram
novos dados . Como isso é possível? Dê uma olhada no ciclo de vida da atividade:
Ciclo de vida da atividade Quando você ligou o telefone, uma nova instância de Activity foi criada e o método
onCreate()
foi chamado. Veja nossa atividade:
MainActivity.kt class MainActivity : AppCompatActivity() { lateinit var binding: ActivityMainBinding var mainViewModel = MainViewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.viewModel = mainViewModel binding.executePendingBindings() } }
Como você pode ver, quando uma instância de Atividade foi criada, também foi criada uma instância de
MainViewModel . É bom que, de alguma forma, tenhamos a mesma instância do
MainViewModel para cada
MainActivity recriada?
Introdução aos componentes com reconhecimento do ciclo de vida
Porque Como muitos desenvolvedores enfrentam esse problema, os desenvolvedores da Equipe do Android Framework decidiram criar uma biblioteca projetada para ajudar a resolver isso. A classe
ViewModel é uma delas. Essa é a classe da qual todo o nosso ViewModel deve herdar.
Vamos herdar o
MainViewModel do
ViewModel dos componentes que reconhecem o ciclo de vida. Primeiro, precisamos adicionar a biblioteca de
componentes com reconhecimento do
ciclo de vida ao nosso arquivo
build.gradle :
build.gradle dependencies { ... implementation "android.arch.lifecycle:runtime:1.0.0-alpha9" implementation "android.arch.lifecycle:extensions:1.0.0-alpha9" kapt "android.arch.lifecycle:compiler:1.0.0-alpha9"
Torne
MainViewModel o herdeiro de
ViewModel :
MainViewModel.kt package me.mladenrakonjac.modernandroidapp import android.arch.lifecycle.ViewModel class MainViewModel : ViewModel() { ... }
O método
onCreate () da nossa MainActivity será parecido com este:
MainActivity.kt class MainActivity : AppCompatActivity() { lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) binding.executePendingBindings() } }
Observe que não criamos uma nova instância do
MainViewModel . Vamos obtê-lo usando o
ViewModelProviders .
ViewModelProviders é uma classe Utility que possui um método para obter o
ViewModelProvider . É tudo sobre
escopo . Se você chamar
ViewModelProviders.of (this) em uma Atividade, seu
ViewModel permanecerá ativo enquanto essa Atividade estiver ativa (até que seja destruída sem recriar). Portanto, se você chamar isso em um fragmento, seu
ViewModel permanecerá ativo enquanto o Fragmento estiver ativo, etc. Dê uma olhada no diagrama:
O ViewModelProvider é responsável por criar uma nova instância na primeira chamada ou retornar a antiga se sua Atividade ou Fragmento for recriado.
Não se confunda com
MainViewModel::class.java
No Kotlin, se você seguir
MainViewModel::class
isso retornará o
KClass , que não é o mesmo que Class from Java. Portanto, se escrevermos
.java , de acordo com a documentação, será:
Retornará uma instância da Classe Java correspondente a esta instância do KClass
Vamos ver o que acontece quando você gira a tela
Temos os mesmos dados que antes da rotação da tela.
No último artigo, eu disse que nosso aplicativo obterá uma lista de repositórios do Github e os mostrará. Para fazer isso, devemos adicionar a função
getRepositories , que retornará uma lista falsa de repositórios:
RepoModel.kt class RepoModel { fun refreshData(onDataReadyCallback: OnDataReadyCallback) { Handler().postDelayed({ onDataReadyCallback.onDataReady("new data") },2000) } fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { var arrayList = ArrayList<Repository>() arrayList.add(Repository("First", "Owner 1", 100 , false)) arrayList.add(Repository("Second", "Owner 2", 30 , true)) arrayList.add(Repository("Third", "Owner 3", 430 , false)) Handler().postDelayed({ onRepositoryReadyCallback.onDataReady(arrayList) },2000) } } interface OnDataReadyCallback { fun onDataReady(data : String) } interface OnRepositoryReadyCallback { fun onDataReady(data : ArrayList<Repository>) }
Também precisamos ter um método no
MainViewModel que chame
getRepositories do
RepoModel :
MainViewModel.kt class MainViewModel : ViewModel() { ... var repositories = ArrayList<Repository>() fun refresh(){ ... } fun loadRepositories(){ isLoading.set(true) repoModel.getRepositories(object : OnRepositoryReadyCallback{ override fun onDataReady(data: ArrayList<Repository>) { isLoading.set(false) repositories = data } }) } }
E, finalmente, precisamos mostrar esses repositórios no RecyclerView. Para fazer isso, devemos:
- Criar layout rv_item_repository.xml
- Inclua o RecyclerView no layout activity_main.xml
- Criar RepositoryRecyclerViewAdapter
- Instale o adaptador na recyclerview
Para criar
rv_item_repository.xml, usei a biblioteca CardView, por isso precisamos adicioná-la ao build.gradle (app):
implementation 'com.android.support:cardview-v7:26.0.1'
Aqui está o que parece:
rv_item_repository.xml <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <import type="android.view.View" /> <variable name="repository" type="me.mladenrakonjac.modernandroidapp.uimodels.Repository" /> </data> <android.support.v7.widget.CardView android:layout_width="match_parent" android:layout_height="96dp" android:layout_margin="8dp"> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/repository_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:text="@{repository.repositoryName}" android:textSize="20sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.083" tools:text="Modern Android App" /> <TextView android:id="@+id/repository_has_issues" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:layout_marginTop="8dp" android:text="@string/has_issues" android:textStyle="bold" android:visibility="@{repository.hasIssues ? View.VISIBLE : View.GONE}" app:layout_constraintBottom_toBottomOf="@+id/repository_name" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="1.0" app:layout_constraintStart_toEndOf="@+id/repository_name" app:layout_constraintTop_toTopOf="@+id/repository_name" app:layout_constraintVertical_bias="1.0" /> <TextView android:id="@+id/repository_owner" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:text="@{repository.repositoryOwner}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/repository_name" app:layout_constraintVertical_bias="0.0" tools:text="Mladen Rakonjac" /> <TextView android:id="@+id/number_of_starts" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:layout_marginTop="8dp" android:text="@{String.valueOf(repository.numberOfStars)}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="1" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/repository_owner" app:layout_constraintVertical_bias="0.0" tools:text="0 stars" /> </android.support.constraint.ConstraintLayout> </android.support.v7.widget.CardView> </layout>
A próxima etapa é adicionar o RecyclerView ao
activity_main.xml . Antes de fazer isso, adicione a biblioteca RecyclerView:
implementation 'com.android.support:recyclerview-v7:26.0.1'
activity_main.xml <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <import type="android.view.View"/> <variable name="viewModel" type="me.fleka.modernandroidapp.MainViewModel" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context="me.fleka.modernandroidapp.MainActivity"> <ProgressBar android:id="@+id/loading" android:layout_width="48dp" android:layout_height="48dp" android:indeterminate="true" android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}" app:layout_constraintBottom_toTopOf="@+id/refresh_button" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <android.support.v7.widget.RecyclerView android:id="@+id/repository_rv" android:layout_width="0dp" android:layout_height="0dp" android:indeterminate="true" android:visibility="@{viewModel.isLoading ? View.GONE : View.VISIBLE}" app:layout_constraintBottom_toTopOf="@+id/refresh_button" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:listitem="@layout/rv_item_repository" /> <Button android:id="@+id/refresh_button" android:layout_width="160dp" android:layout_height="40dp" android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:onClick="@{() -> viewModel.loadRepositories()}" android:clickable="@{viewModel.isLoading ? false : true}" android:text="Refresh" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="1.0" /> </android.support.constraint.ConstraintLayout> </layout>
Observe que removemos alguns elementos do TextView e agora o botão inicia o
loadRepositories em vez da
atualização :
button.xml <Button android:id="@+id/refresh_button" android:onClick="@{() -> viewModel.loadRepositories()}" ... />
Vamos remover o método de
atualização do MainViewModel e
refreshData do RepoModel como desnecessário.
Agora você precisa criar um adaptador para o RecyclerView:
RepositoryRecyclerViewAdapter.kt class RepositoryRecyclerViewAdapter(private var items: ArrayList<Repository>, private var listener: OnItemClickListener) : RecyclerView.Adapter<RepositoryRecyclerViewAdapter.ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder { val layoutInflater = LayoutInflater.from(parent?.context) val binding = RvItemRepositoryBinding.inflate(layoutInflater, parent, false) return ViewHolder(binding) } override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(items[position], listener) override fun getItemCount(): Int = items.size interface OnItemClickListener { fun onItemClick(position: Int) } class ViewHolder(private var binding: RvItemRepositoryBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(repo: Repository, listener: OnItemClickListener?) { binding.repository = repo if (listener != null) { binding.root.setOnClickListener({ _ -> listener.onItemClick(layoutPosition) }) } binding.executePendingBindings() } } }
Observe que o ViewHolder usa uma instância do tipo
RvItemRepositoryBinding , em vez de
View , para que possamos implementar a Ligação de dados no ViewHolder para cada elemento. Não tenha vergonha da função de linha única (on-line):
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(items[position], listener)
Esta é apenas uma pequena entrada para:
override fun onBindViewHolder(holder: ViewHolder, position: Int){ return holder.bind(items[position], listener) }
E
itens [position] é a implementação para o operador de índice. É semelhante a
items.get (position) .
Outra linha que pode confundir você:
binding.root.setOnClickListener({ _ -> listener.onItemClick(layoutPosition) })
Você pode substituir o parâmetro por _ se não estiver usando. Legal, hein?
Criamos o adaptador, mas ainda não o aplicamos ao
recyclerView em
MainActivity :
MainActivity.kt class MainActivity : AppCompatActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener { lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) binding.viewModel = viewModel binding.executePendingBindings() binding.repositoryRv.layoutManager = LinearLayoutManager(this) binding.repositoryRv.adapter = RepositoryRecyclerViewAdapter(viewModel.repositories, this) } override fun onItemClick(position: Int) { TODO("not implemented")
Execute o aplicativo
Isso é estranho. O que aconteceu?
- A atividade foi criada, portanto, um novo adaptador também foi criado com repositórios praticamente vazios
- Apertamos o botão
- Chamado loadRepositories, mostrando o progresso
- Após 2 segundos, obtemos os repositórios, o progresso está oculto, mas eles não aparecem. Isso ocorre porque notifyDataSetChanged não é chamado no adaptador
- Quando giramos a tela, uma nova atividade é criada, portanto, um novo adaptador é criado com o parâmetro repositories com alguns dados
Portanto, como
MainViewModel deve notificar
MainActivity sobre novos itens, podemos chamar
notifyDataSetChanged ?
Nós não podemos.
Isso é realmente importante, o
MainViewModel não deve saber nada sobre o
MainActivity .
MainActivity é quem possui uma instância do
MainViewModel , portanto, ele deve escutar alterações e notificar o
Adaptador sobre as alterações.
Mas como fazer isso?
Podemos observar os
repositórios ; portanto, após alterar os dados, podemos alterar nosso adaptador.
O que há de errado com essa decisão?
Vejamos o seguinte caso:
- Em MainActivity, observamos repositórios: quando ocorre uma alteração, executamos notifyDataSetChanged
- Apertamos o botão
- Enquanto aguardamos alterações nos dados, o MainActivity pode ser recriado devido a alterações na configuração.
- Nosso MainViewModel ainda está ativo
- Após 2 segundos, o campo repositórios recebe novos itens e notifica o observador de que os dados foram alterados
- O observador tenta executar notifyDataSetChanged no adaptador , que não existe mais, porque A atividade principal foi recriada
Bem, nossa decisão não é boa o suficiente.
Introdução ao LiveData
O LiveData é outro
componente que reconhece o ciclo de vida e é baseado em um observável que conhece o ciclo de vida do View. Portanto, quando uma Atividade é destruída devido a
uma alteração na configuração , o
LiveData sabe disso, e também remove o observador da Atividade destruída.
Implementamos no
MainViewModel :
MainViewModel.kt class MainViewModel : ViewModel() { var repoModel: RepoModel = RepoModel() val text = ObservableField("old data") val isLoading = ObservableField(false) var repositories = MutableLiveData<ArrayList<Repository>>() fun loadRepositories() { isLoading.set(true) repoModel.getRepositories(object : OnRepositoryReadyCallback { override fun onDataReady(data: ArrayList<Repository>) { isLoading.set(false) repositories.value = data } }) } }
e comece a observar MainActivity:
MainActivity.kt class MainActivity : LifecycleActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener { private lateinit var binding: ActivityMainBinding private val repositoryRecyclerViewAdapter = RepositoryRecyclerViewAdapter(arrayListOf(), this) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) binding.viewModel = viewModel binding.executePendingBindings() binding.repositoryRv.layoutManager = LinearLayoutManager(this) binding.repositoryRv.adapter = repositoryRecyclerViewAdapter viewModel.repositories.observe(this, Observer<ArrayList<Repository>> { it?.let{ repositoryRecyclerViewAdapter.replaceData(it)} }) } override fun onItemClick(position: Int) { TODO("not implemented")
O que significa a palavra? Se uma função tiver apenas um parâmetro, o acesso a esse parâmetro poderá ser obtido usando a palavra-chave it. Então, suponha que tenhamos uma expressão lambda para multiplicar por 2:
((a) -> 2 * a)
Pode ser substituído da seguinte maneira:
(it * 2)
Se você iniciar o aplicativo agora, poderá garantir que tudo funcione
...
Por que eu prefiro o MVVM ao MVP?
- Não há interface chata para o View, pois ViewModel não tem referência a View
- Não há interface chata para o Presenter, e isso não é necessário
- Muito mais fácil de lidar com alterações de configuração
- Usando o MVVM, temos menos código para Atividade, Fragmentos etc.
...
Padrão de repositório
Como eu disse anteriormente,
Model é apenas um nome abstrato para a camada em que estamos preparando os dados. Geralmente contém repositórios e classes de dados. Cada classe de entidade (dados) possui uma classe de
repositório correspondente. Por exemplo, se tivermos as classes
User e
Post , também devemos ter
UserRepository e
PostRepository . Todos os dados vêm de lá. Nunca devemos chamar uma instância de Preferências compartilhadas ou banco de dados de View ou ViewModel.
Portanto, podemos renomear nosso RepoModel para
GitRepoRepository , onde o
GitRepo virá do repositório Github e o
Repositório virá do padrão Repository.
RepoRepositories.kt class GitRepoRepository { fun getGitRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { var arrayList = ArrayList<Repository>() arrayList.add(Repository("First", "Owner 1", 100, false)) arrayList.add(Repository("Second", "Owner 2", 30, true)) arrayList.add(Repository("Third", "Owner 3", 430, false)) Handler().postDelayed({ onRepositoryReadyCallback.onDataReady(arrayList) }, 2000) } } interface OnRepositoryReadyCallback { fun onDataReady(data: ArrayList<Repository>) }
Ok,
MainViewModel obtém a lista de repositórios do
Gitub do
GitRepoRepsitories , mas de onde obter o
GitRepoRepositories ?
Você pode chamar o
cliente ou o
banco de dados diretamente no repositório a partir da instância, mas ainda não é a melhor prática. Seu aplicativo deve ser modular o máximo possível. E se você decidir usar clientes diferentes para substituir o Volley pelo Retrofit? Se você tiver algum tipo de lógica interna, será difícil refatorar. Seu repositório não precisa saber qual cliente você está usando para recuperar os dados remotos.
- A única coisa que o repositório precisa saber é que os dados chegam remotamente ou localmente. Não é necessário saber como obtemos esses dados remotos ou locais.
- O único viewmodel necessário são os dados
- A única coisa que o View deve fazer é mostrar esses dados.
Quando eu comecei a desenvolver no Android, fiquei pensando como os aplicativos funcionam offline e como a sincronização de dados funciona. Uma boa arquitetura de aplicativos nos permite fazer isso com facilidade. Por exemplo, quando
loadRepositories no
ViewModel é chamado se houver uma conexão com a Internet, o
GitRepoRepositories pode receber dados de uma fonte de dados remota e salvá-los em uma fonte de dados local. Quando o telefone está offline, o
GitRepoRepository pode receber dados do armazenamento local. Portanto, os
repositórios devem ter instâncias de
RemoteDataSource e
LocalDataSource e o processamento lógico de onde esses dados devem vir.
Adicione uma
fonte de dados local :
GitRepoLocalDataSource.kt class GitRepoLocalDataSource { fun getRepositories(onRepositoryReadyCallback: OnRepoLocalReadyCallback) { var arrayList = ArrayList<Repository>() arrayList.add(Repository("First From Local", "Owner 1", 100, false)) arrayList.add(Repository("Second From Local", "Owner 2", 30, true)) arrayList.add(Repository("Third From Local", "Owner 3", 430, false)) Handler().postDelayed({ onRepositoryReadyCallback.onLocalDataReady(arrayList) }, 2000) } fun saveRepositories(arrayList: ArrayList<Repository>){
Aqui temos dois métodos: o primeiro, que retorna dados locais falsos, e o segundo, para armazenamento de dados fictícios.Adicione uma fonte de dados remota :GitRepoRemoteDataSource.kt class GitRepoRemoteDataSource { fun getRepositories(onRepositoryReadyCallback: OnRepoRemoteReadyCallback) { var arrayList = ArrayList<Repository>() arrayList.add(Repository("First from remote", "Owner 1", 100, false)) arrayList.add(Repository("Second from remote", "Owner 2", 30, true)) arrayList.add(Repository("Third from remote", "Owner 3", 430, false)) Handler().postDelayed({ onRepositoryReadyCallback.onRemoteDataReady(arrayList) }, 2000) } } interface OnRepoRemoteReadyCallback { fun onRemoteDataReady(data: ArrayList<Repository>) }
Existe apenas um método que retorna dados remotos falsos .Agora podemos adicionar alguma lógica ao nosso repositório:GitRepoRepository.kt class GitRepoRepository { val localDataSource = GitRepoLocalDataSource() val remoteDataSource = GitRepoRemoteDataSource() fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { remoteDataSource.getRepositories( object : OnRepoRemoteReadyCallback { override fun onDataReady(data: ArrayList<Repository>) { localDataSource.saveRepositories(data) onRepositoryReadyCallback.onDataReady(data) } }) } } interface OnRepositoryReadyCallback { fun onDataReady(data: ArrayList<Repository>) }
Assim, compartilhando fontes, salvamos facilmente os dados localmente.E se você precisar apenas de dados da rede, ainda precisará usar o modelo de repositório? Sim
Isso facilita o teste de código, outros desenvolvedores podem entender melhor seu código e você pode suportá-lo mais rapidamente!...
Wrappers do Android Manager
E se você quiser verificar sua conexão com a Internet no GitRepoRepository para saber de onde solicitar dados? Já dissemos que não devemos colocar nenhum código relacionado ao Android no ViewModel e Model . Como lidar com esse problema?Vamos escrever um invólucro para uma conexão à Internet:NetManager.kt (Uma solução semelhante se aplica a outros gerentes, por exemplo, ao NfcManager) class NetManager(private var applicationContext: Context) { private var status: Boolean? = false val isConnectedToInternet: Boolean? get() { val conManager = applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val ni = conManager.activeNetworkInfo return ni != null && ni.isConnected } }
Este código só funcionará se adicionarmos permissão para manifestar: <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
Mas como criar uma instância no Repositório, se não temos o contexto ( contexto A )? Podemos solicitá-lo no construtor:GitRepoRepository.kt class GitRepoRepository (context: Context){ val localDataSource = GitRepoLocalDataSource() val remoteDataSource = GitRepoRemoteDataSource() val netManager = NetManager(context) fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { remoteDataSource.getRepositories(object : OnRepoRemoteReadyCallback { override fun onDataReady(data: ArrayList<Repository>) { localDataSource.saveRepositories(data) onRepositoryReadyCallback.onDataReady(data) } }) } } interface OnRepositoryReadyCallback { fun onDataReady(data: ArrayList<Repository>) }
Criamos antes da nova instância GitRepoRepository no ViewModel. Como agora podemos ter o NetManager no ViewModel quando precisamos de contexto para o NetManager ? Você pode usar o AndroidViewModel na biblioteca de componentes compatíveis com o ciclo de vida, que possui um contexto . Este é o contexto do aplicativo, não a Atividade.MainViewModel.kt class MainViewModel : AndroidViewModel { constructor(application: Application) : super(application) var gitRepoRepository: GitRepoRepository = GitRepoRepository(NetManager(getApplication())) val text = ObservableField("old data") val isLoading = ObservableField(false) var repositories = MutableLiveData<ArrayList<Repository>>() fun loadRepositories() { isLoading.set(true) gitRepoRepository.getRepositories(object : OnRepositoryReadyCallback { override fun onDataReady(data: ArrayList<Repository>) { isLoading.set(false) repositories.value = data } }) } }
Nesta linha constructor(application: Application) : super(application)
nós definimos um construtor para MainViewModel . Isso é necessário porque o AndroidViewModel solicita uma instância do aplicativo em seu construtor. Portanto, em nosso construtor, chamamos o super método que chama o construtor AndroidViewModel , do qual herdamos.Nota: podemos nos livrar de uma linha se o fizermos: class MainViewModel(application: Application) : AndroidViewModel(application) { ... }
E agora que temos a instância do NetManager no GitRepoRepository , podemos verificar a conexão com a Internet:GitRepoRepository.kt class GitRepoRepository(val netManager: NetManager) { val localDataSource = GitRepoLocalDataSource() val remoteDataSource = GitRepoRemoteDataSource() fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { netManager.isConnectedToInternet?.let { if (it) { remoteDataSource.getRepositories(object : OnRepoRemoteReadyCallback { override fun onRemoteDataReady(data: ArrayList<Repository>) { localDataSource.saveRepositories(data) onRepositoryReadyCallback.onDataReady(data) } }) } else { localDataSource.getRepositories(object : OnRepoLocalReadyCallback { override fun onLocalDataReady(data: ArrayList<Repository>) { onRepositoryReadyCallback.onDataReady(data) } }) } } } } interface OnRepositoryReadyCallback { fun onDataReady(data: ArrayList<Repository>) }
Assim, se tivermos uma conexão com a Internet, receberemos os dados excluídos e os salvaremos localmente. Se não tivermos uma conexão com a Internet, obteremos dados locais.Nota de Kotlin : a instrução let verifica nulo e retorna um valor dentro dele .Em um dos seguintes artigos, escreverei sobre injeção de dependência, quão ruim é criar instâncias de repositório no ViewModel e como evitar o uso do AndroidViewModel. Também vou escrever sobre um grande número de problemas que estão agora em nosso código. Deixei-os por um motivo ...Estou tentando mostrar os problemas para que você possa entender por que todas essas bibliotecas são populares e por que você deve usá-las.PS Eu mudei minha mente sobre o mapeador ( mapeadores ). Decidi cobrir isso nos seguintes artigos.