Développement Android moderne sur Kotlin. 2e partie

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.

image

«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:

  1. Android Studio 3 beta 1 Partie 1
  2. Langage de programmation Kotlin, partie 1
  3. Options de construction, partie 1
  4. ConstraintLayout Partie 1
  5. Bibliothèque de liaison de données, partie 1
  6. Architecture MVVM + référentiel + modèle de gestionnaire Android Manager
  7. RxJava2 et comment il nous aide dans l'architecture de la partie 3
  8. Dague 2.11, qu'est-ce que l'injection de dépendances, pourquoi utiliser cette partie 4
  9. Retrofit (avec Rx Java2)
  10. 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:

  • MVP
  • MVVM

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.kt
class 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é
image

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
image

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") //To change body of created functions use File | Settings | File Templates. } } 


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") //To change body of created functions use File | Settings | File Templates. } } 


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


Schéma
image

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>){ //todo save repositories in DB } } interface OnRepoLocalReadyCallback { fun onLocalDataReady(data: 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.

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


All Articles