Bonjour, Habr! Je vous présente la traduction de l'article "
Développement Android moderne avec Kotlin (Partie 2) " de Mladen Rakonjac.
Remarque Cet article est une traduction de la série d'articles de Mladen Rakonjac , date de l'article: 23/09/2017. Github En commençant à lire la première partie de SemperPeritus, j'ai découvert que le reste de la partie n'était pas traduit pour une raison quelconque. Par conséquent, j'attire votre attention sur la deuxième partie. L'article s'est avéré volumineux.
«Il est très difficile de trouver un projet qui couvrirait tout ce qui est nouveau en développement pour Android dans Android Studio 3.0, j'ai donc décidé de l'écrire.»
Dans cet article, nous analyserons les éléments suivants:
- Android Studio 3 beta 1 Partie 1
- Langage de programmation Kotlin, partie 1
- Options de construction, partie 1
- ConstraintLayout Partie 1
- Bibliothèque de liaison de données, partie 1
- Architecture MVVM + référentiel + modèle de gestionnaire Android Manager
- RxJava2 et comment il nous aide dans l'architecture de la partie 3
- Dague 2.11, qu'est-ce que l'injection de dépendances, pourquoi utiliser cette partie 4
- Retrofit (avec Rx Java2)
- Chambre (avec Rx Java2)
Architecture MVVM + référentiel + modèle de gestionnaire Android Manager
Quelques mots sur l'architecture dans le monde Android
Depuis un certain temps, les développeurs Android n'ont utilisé aucune architecture dans leurs projets. Au cours des trois dernières années, beaucoup de battage médiatique a fait son apparition dans la communauté des développeurs Android. Le temps de l'activité de Dieu est passé et Google a publié le référentiel
Android Architecture Blueprints , avec de nombreux exemples et instructions sur diverses approches architecturales. Enfin, sur Google IO '17, ils ont introduit
Android Architecture Components , une collection de bibliothèques conçues pour nous aider à créer un code plus propre et à améliorer les applications.
Le composant indique que vous pouvez tous les utiliser, ou seulement l'un d'eux. Cependant, je les ai tous trouvés vraiment utiles. Plus loin dans le texte et dans les parties suivantes, nous les utiliserons. Tout d'abord, je vais aborder le problème dans le code, puis je vais refactoriser l'utilisation de ces composants et bibliothèques pour voir quels problèmes ils sont censés résoudre.
Il existe deux principaux
modèles architecturaux qui partagent le code GUI:
Il est difficile de dire lequel est le meilleur. Vous devez essayer les deux et décider. Je préfère que
MVVM utilise des composants adaptés au cycle de vie et j'écrirai à ce sujet. Si vous n'avez jamais essayé d'utiliser MVP, il y a des tonnes de bons articles sur Medium à ce sujet.
Qu'est-ce qu'un modèle MVVM?
MVVM est un
modèle architectural qui se développe en tant que Model-View-ViewModel. Je pense que ce nom déroute les développeurs. Si j'étais celui qui a donné son nom, je l'appellerais View-ViewModel-Model, car le
ViewModel est au milieu, connectant
View et
Model .
La vue est une abstraction pour l'
activité , le
fragment ou toute autre vue personnalisée (
vue personnalisée Android ). Veuillez noter qu'il est important de ne pas confondre cette
vue avec la vue Android.
La vue doit être stupide, nous ne devons pas lui écrire de logique.
La vue ne doit pas contenir de données. Il doit stocker une référence à l'instance de
ViewModel et toutes les données dont la
vue a besoin doivent provenir de là. De plus, la
vue doit observer ces données et la disposition doit changer lorsque les données du
ViewModel changent. Pour résumer,
View est responsable des éléments suivants: vue de mise en page pour diverses données et états.
ViewModel est un nom abstrait pour une classe contenant des données et une logique, lorsque ces données doivent être reçues et affichées.
ViewModel stocke l'
état actuel.
ViewModel stocke également un lien vers un ou plusieurs
modèles et en reçoit toutes les données. Elle ne devrait pas savoir, par exemple, d'où proviennent les données, de la base de données ou du serveur. De plus, le
ViewModel n'a besoin de rien savoir sur
View . De plus,
ViewModel ne devrait rien savoir du framework Android.Le modèle est le nom abstrait de la couche qui prépare les données pour le
ViewModel . Il s'agit de la classe dans laquelle nous allons recevoir des données du serveur et les mettre en cache, ou les stocker dans une base de données locale. Notez qu'il ne s'agit pas des mêmes classes que User, Car, Square, ou d'autres classes de modèle qui stockent simplement des données. En règle générale, il s'agit d'une implémentation du modèle de référentiel, que nous examinerons plus tard.
Le modèle ne doit rien savoir de
ViewModel .
MVVM , s'il est implémenté correctement, est un excellent moyen de casser votre code et de le rendre plus testable. Cela nous aide à suivre les principes
SOLID , donc notre code est plus facile à maintenir.
Exemple de code
Maintenant, je vais écrire un exemple simple montrant comment cela fonctionne.
Pour commencer, créons un
modèle simple qui renvoie une ligne:
RepoModel.ktclass RepoModel { fun refreshData() : String { return "Some new data" } }
En règle générale, la réception de données est un appel
asynchrone , nous devons donc l'attendre. Pour simuler cela, j'ai changé la classe comme suit:
RepoModel.kt class RepoModel { fun refreshData(onDataReadyCallback: OnDataReadyCallback) { Handler().postDelayed({ onDataReadyCallback.onDataReady("new data") },2000) } } interface OnDataReadyCallback { fun onDataReady(data : String) }
J'ai créé l'interface
OnDataReadyCallback
avec la méthode
onDataReady
. Et maintenant, la méthode
refreshData
implémente (implémente)
OnDataReadyCallback
. Pour simuler l'attente, j'utilise le
Handler
. Une fois
onDataReady
2 secondes, la méthode
onDataReady
sera appelée sur les classes qui implémentent l'interface
OnDataReadyCallback
.
Créons un
ViewModel :
MainViewModel.kt class MainViewModel { var repoModel: RepoModel = RepoModel() var text: String = "" var isLoading: Boolean = false }
Comme vous pouvez le voir, il existe une instance de
RepoModel
,
text
, qui sera affichée, et la variable
isLoading
, qui stocke l'état actuel. Créons une méthode de
refresh
chargée de récupérer les données:
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) } }
La méthode d'
refresh
appelle
refreshData
sur le
RepoModel
, qui prend une implémentation
OnDataReadyCallback
en arguments. D'accord, mais qu'est-ce qu'un
object
? Chaque fois que vous souhaitez implémenter une interface ou hériter d'une classe étendue sans sous-classement, vous utiliserez une
déclaration d'objet . Et si vous voulez l'utiliser comme classe anonyme? Dans ce cas, vous utilisez l'
expression d'objet :
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 }) } }
Lorsque nous appelons
refresh
, nous devons changer la vue à l'état de
chargement et lorsque les données arrivent, définissez
isLoading
sur
false
.
Nous devons également remplacer le
text
par
ObservableField<String>
et est en cours de
isLoading
ObservableField<Boolean>
.
ObservableField
est une classe de la bibliothèque de liaison de données que nous pouvons utiliser au lieu de créer un objet Observable. Il enveloppe l'objet que nous voulons observer.
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) } }) } }
Notez que j'utilise
val au lieu de
var , car nous ne changerons que la valeur dans le champ, mais pas le champ lui-même. Et si vous souhaitez l'initialiser, utilisez ce qui suit:
initobserv.kt val text = ObservableField("old data") val isLoading = ObservableField(false)
Modifions notre mise en page afin qu'elle puisse observer le
texte et
isLoading . Pour commencer, liez
MainViewModel au lieu de
Repository :
activity_main.xml <data> <variable name="viewModel" type="me.mladenrakonjac.modernandroidapp.MainViewModel" /> </data>
Ensuite:
- Modifier TextView pour observer le texte de MainViewModel
- Ajoutez un ProgressBar, qui ne sera visible que si isLoading true
- Bouton Ajouter, qui, lorsqu'il est cliqué, appellera la méthode d' actualisation de MainViewModel et ne sera cliquable que si 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}" /> ...
Si vous exécutez maintenant, vous obtiendrez une
View.VISIBLE and View.GONE cannot be used if View is not imported
. Eh bien, importons:
main_activity.xml <data> <import type="android.view.View"/> <variable name="viewModel" type="me.fleka.modernandroidapp.MainViewModel" /> </data>
Ok, fini la mise en page. Maintenant, terminez avec la reliure. Comme je l'ai dit,
View
doit avoir une instance de
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() } }
Enfin, nous pouvons exécuter
Vous pouvez voir que les
anciennes données sont remplacées par de
nouvelles données .
C'était un exemple simple de MVVM.
Mais il y a un problème, tournons l'écran
les anciennes données ont remplacé les
nouvelles données . Comment est-ce possible? Jetez un œil au cycle de vie de l'activité:
Cycle de vie de l'activité Lorsque vous avez tourné le téléphone, une nouvelle instance de Activity a été créée et la méthode
onCreate()
été appelée. Jetez un œil à notre activité:
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() } }
Comme vous pouvez le voir, lorsqu'une instance d'activité a été créée, une instance
MainViewModel a également été créée. Est-il bon que nous ayons en quelque sorte la même instance de
MainViewModel pour chaque
MainActivity recréée?
Introduction aux composants compatibles avec le cycle de vie
Parce que de nombreux développeurs sont confrontés à ce problème, les développeurs de l'équipe Android Framework ont décidé de créer une bibliothèque conçue pour aider à résoudre ce problème. La classe
ViewModel en fait partie. C'est la classe dont tout notre ViewModel devrait hériter.
Héritons MainViewModel de
ViewModel des composants sensibles au cycle de vie. Nous devons d'abord ajouter la bibliothèque de
composants prenant en compte le
cycle de vie à notre fichier
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"
Faites de
MainViewModel l' héritier de
ViewModel :
MainViewModel.kt package me.mladenrakonjac.modernandroidapp import android.arch.lifecycle.ViewModel class MainViewModel : ViewModel() { ... }
La méthode
onCreate () de notre MainActivity ressemblera à ceci:
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() } }
Notez que nous n'avons pas créé de nouvelle instance de
MainViewModel . Nous l'obtiendrons en utilisant
ViewModelProviders .
ViewModelProviders est une classe utilitaire qui a une méthode pour obtenir
ViewModelProvider . C'est une question de
portée . Si vous appelez
ViewModelProviders.of (this) dans une activité, votre
ViewModel vivra aussi longtemps que cette activité sera
active (jusqu'à ce qu'elle soit détruite sans recréation). Par conséquent, si vous appelez cela dans un fragment, votre
ViewModel vivra alors que le fragment est vivant, etc. Jetez un œil au schéma:
Cycle de vie de la portée ViewModelProvider est responsable de la création d'une nouvelle instance lors du premier appel ou du renvoi de l'ancienne si votre activité ou fragment est recréé.
Ne vous confondez pas avec
MainViewModel::class.java
A Kotlin, si vous suivez
MainViewModel::class
cela vous
renverra KClass , qui n'est pas la même chose que Class from Java. Donc, si nous écrivons
.java , alors selon la documentation c'est:
Renvoie une instance de classe Java correspondant à cette instance de KClass
Voyons ce qui se passe lorsque vous faites pivoter l'écran
Nous avons les mêmes données qu'avant la rotation de l'écran.
Dans le dernier article, j'ai dit que notre application obtiendrait une liste des référentiels Github et les montrerait. Pour ce faire, nous devons ajouter la fonction
getRepositories , qui renverra une fausse liste de référentiels:
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>) }
Nous devons également avoir une méthode dans
MainViewModel qui appelle
getRepositories de
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 } }) } }
Et enfin, nous devons afficher ces référentiels dans RecyclerView. Pour ce faire, nous devons:
- Créer une disposition rv_item_repository.xml
- Ajouter RecyclerView à la mise en page activity_main.xml
- Créer RepositoryRecyclerViewAdapter
- Installer l'adaptateur à recyclerview
Pour créer
rv_item_repository.xml, j'ai utilisé la bibliothèque CardView, nous devons donc l'ajouter à build.gradle (app):
implementation 'com.android.support:cardview-v7:26.0.1'
Voici à quoi ça ressemble:
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>
L'étape suivante consiste à ajouter RecyclerView à
activity_main.xml . Avant de faire cela, assurez-vous d'ajouter la bibliothèque 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>
Notez que nous avons supprimé certains éléments TextView et maintenant le bouton lance
loadRepositories au lieu de
rafraîchir :
button.xml <Button android:id="@+id/refresh_button" android:onClick="@{() -> viewModel.loadRepositories()}" ... />
Supprimons la méthode d'
actualisation de
MainViewModel et
refreshData de RepoModel comme inutile.
Vous devez maintenant créer un adaptateur pour 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() } } }
Notez que ViewHolder prend une instance de type
RvItemRepositoryBinding , au lieu de
View , afin que nous puissions implémenter la liaison de données dans le ViewHolder pour chaque élément. Ne soyez pas gêné par la fonction monoligne (en ligne):
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(items[position], listener)
Ceci est juste une courte entrée pour:
override fun onBindViewHolder(holder: ViewHolder, position: Int){ return holder.bind(items[position], listener) }
Et
items [position] est l'implémentation de l'opérateur d'index. Il est similaire à
items.get (position) .
Une autre ligne qui peut vous dérouter:
binding.root.setOnClickListener({ _ -> listener.onItemClick(layoutPosition) })
Vous pouvez remplacer le paramètre par _ si vous ne l'utilisez pas. Bien, hein?
Nous avons créé l'adaptateur, mais ne l'avons toujours pas appliqué à
recyclerView dans
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")
Exécutez l'application
C'est bizarre. Que s'est-il passé?
- Une activité a été créée, donc un nouvel adaptateur a également été créé avec des référentiels pratiquement vides
- Nous poussons le bouton
- LoadRepositories appelé, montrant les progrès
- Après 2 secondes, nous obtenons les référentiels, la progression est masquée, mais ils n'apparaissent pas. Cela est dû au fait que notifyDataSetChanged n'est pas appelé dans l'adaptateur
- Lorsque nous faisons pivoter l'écran, une nouvelle activité est créée, donc un nouvel adaptateur est créé avec le paramètre de référentiels avec certaines données
Donc, comme
MainViewModel doit notifier
MainActivity de nouveaux éléments, pouvons-nous appeler
notifyDataSetChanged ?
Nous ne pouvons pas.
Ceci est vraiment important,
MainViewModel ne devrait pas du tout connaître
MainActivity .
MainActivity est celui qui possède une instance de
MainViewModel , il doit donc écouter les modifications et informer
Adapter des modifications.
Mais comment faire?
Nous pouvons observer les
référentiels , donc après avoir changé les données, nous pouvons changer notre adaptateur.
Quel est le problème avec cette décision?
Regardons le cas suivant:
- Dans MainActivity, nous observons les référentiels: quand un changement se produit, nous exécutons notifyDataSetChanged
- Nous poussons le bouton
- Pendant que nous attendons des modifications des données, MainActivity peut être recréé en raison de modifications de configuration.
- Notre MainViewModel est toujours vivant
- Après 2 secondes, le champ des référentiels reçoit de nouveaux éléments et informe l'observateur que les données ont changé
- L'observateur essaie d'exécuter notifyDataSetChanged sur l' adaptateur , qui n'existe plus, car MainActivity a été recréé
Eh bien, notre décision n'est pas assez bonne.
Introduction à LiveData
LiveData est un autre
composant compatible avec le cycle de vie. Il est basé sur un observable qui connaît le cycle de vie de View. Ainsi, lorsqu'une activité est détruite en raison d'
un changement de configuration ,
LiveData le sait, de sorte qu'il supprime également l'observateur de l'activité détruite.
Nous implémentons dans
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 } }) } }
et commencer à observer 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")
Que signifie le mot? Si une fonction n'a qu'un seul paramètre, l'accès à ce paramètre peut être obtenu à l'aide du mot clé it. Supposons donc que nous ayons une expression lambda à multiplier par 2:
((a) -> 2 * a)
Peut être remplacé comme suit:
(it * 2)
Si vous démarrez l'application maintenant, vous pouvez vous assurer que tout fonctionne
...
Pourquoi je préfère MVVM à MVP?
- Il n'y a pas d'interface ennuyeuse pour View, car ViewModel n'a aucune référence à View
- Il n'y a pas d'interface ennuyeuse pour Presenter, et ce n'est pas nécessaire
- Beaucoup plus facile à gérer les changements de configuration
- En utilisant MVVM, nous avons moins de code pour l'activité, les fragments, etc.
...
Modèle de référentiel
Comme je l'ai dit plus tôt,
Model n'est qu'un nom abstrait pour la couche où nous préparons les données. Il contient généralement des référentiels et des classes de données. Chaque classe d'entité (données) a une classe de
référentiel correspondante. Par exemple, si nous avons les classes
User et
Post , nous devons également avoir
UserRepository et
PostRepository . Toutes les données viennent de là. Nous ne devons jamais appeler une instance de préférences partagées ou de base de données depuis View ou ViewModel.
Nous pouvons donc renommer notre RepoModel en
GitRepoRepository , où
GitRepo proviendra du référentiel Github et
Repository proviendra du modèle 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 obtient la liste Github des référentiels de
GitRepoRepsitories , mais d'où obtenir
GitRepoRepositories ?
Vous pouvez appeler le
client ou la base de
données directement sur le référentiel à partir de l'instance, mais ce n'est toujours pas la meilleure pratique. Votre application doit être modulaire autant que possible. Et si vous décidez d'utiliser différents clients pour remplacer Volley par Retrofit? Si vous avez une sorte de logique à l'intérieur, il sera difficile de refactoriser. Votre référentiel n'a pas besoin de savoir quel client vous utilisez pour récupérer les données distantes.
- La seule chose que le référentiel doit savoir est que les données arrivent à distance ou localement. Il n'est pas nécessaire de savoir comment nous obtenons ces données distantes ou locales.
- Le seul modèle de vue requis est les données
- La seule chose que la vue doit faire est d'afficher ces données.
Quand je viens de commencer à développer sur Android, je me demandais comment les applications fonctionnent hors ligne et comment fonctionne la synchronisation des données. Une bonne architecture d'application nous permet de le faire facilement. Par exemple, lorsque
loadRepositories dans le
ViewModel est appelé s'il existe une connexion Internet,
GitRepoRepositories peut recevoir des données d'une source de données distante et les enregistrer dans une source de données locale. Lorsque le téléphone est hors ligne,
GitRepoRepository peut recevoir des données du stockage local. Ainsi, les
référentiels doivent avoir des instances de
RemoteDataSource et
LocalDataSource et le traitement logique d'où ces données doivent provenir.
Ajoutez une
source de données locale :
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>){
Ici, nous avons deux méthodes: la première, qui renvoie de fausses données locales, et la seconde, pour le stockage de données fictives.Ajoutez une source de données distante :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>) }
Il n'y a qu'une seule méthode qui renvoie de fausses données distantes .Maintenant, nous pouvons ajouter un peu de logique à notre référentiel: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>) }
Ainsi, en partageant les sources, nous enregistrons facilement les données localement.Que se passe-t-il si vous n'avez besoin que de données provenant du réseau, vous devez toujours utiliser le modèle de référentiel? Oui
Cela facilite les tests de code, les autres développeurs peuvent mieux comprendre votre code et vous pouvez le prendre en charge plus rapidement!...
Wrappers Android Manager
Que faire si vous souhaitez vérifier votre connexion Internet dans GitRepoRepository pour savoir où demander des données? Nous avons déjà dit que nous ne devrions pas placer de code lié à Android dans ViewModel et Model , alors comment résoudre ce problème?Écrivons un wrapper pour une connexion Internet:NetManager.kt (Une solution similaire s'applique à d'autres gestionnaires, par exemple, à 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 } }
Ce code ne fonctionnera que si nous ajoutons l'autorisation de manifester: <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" />
Mais comment créer une instance dans le référentiel, si nous n'avons pas le contexte ( contexte Le )? Nous pouvons le demander au constructeur: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>) }
Nous avons créé avant la nouvelle instance de GitRepoRepository dans le ViewModel. Comment pouvons-nous maintenant avoir NetManager dans ViewModel lorsque nous avons besoin de contexte pour NetManager ? Vous pouvez utiliser AndroidViewModel à partir de la bibliothèque de composants compatibles avec le cycle de vie, qui a un contexte . Il s'agit du contexte de l'application et non de l'activité.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 } }) } }
Dans cette ligne constructor(application: Application) : super(application)
nous avons défini un constructeur pour MainViewModel . Cela est nécessaire car AndroidViewModel demande une instance de l' application dans son constructeur. Ainsi, dans notre constructeur, nous appelons la super méthode qui appelle le constructeur AndroidViewModel , dont nous héritons.Remarque: nous pouvons nous débarrasser d'une ligne si nous le faisons: class MainViewModel(application: Application) : AndroidViewModel(application) { ... }
Et maintenant que nous avons l'instance NetManager dans le GitRepoRepository , nous pouvons vérifier la connexion 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>) }
Ainsi, si nous avons une connexion Internet, nous recevrons les données supprimées et les enregistrer localement. Si nous n'avons pas de connexion Internet, nous obtiendrons des données locales.Note sur Kotlin : l' opérateur laisse chèques pour nulle et renvoie la valeur dans les IT .Dans l'un des articles suivants, j'écrirai sur l'injection de dépendances, à quel point il est difficile de créer des instances de référentiel dans ViewModel et comment éviter d'utiliser AndroidViewModel. J'écrirai également un grand nombre de problèmes qui se trouvent maintenant dans notre code. Je les ai laissés pour une raison ...J'essaie de vous montrer les problèmes afin que vous puissiez comprendre pourquoi toutes ces bibliothèques sont populaires et pourquoi vous devriez les utiliser.PS J'ai changé d' avis sur le mappeur ( cartographes ). J'ai décidé de couvrir cela dans les articles suivants.