MVVM et sélection d'éléments dans l'adaptateur

Déjà après le 3e adaptateur auto-écrit, dans lequel il était nécessaire de mettre en œuvre la logique de mémorisation de l'élément sélectionné, j'avais pensé qu'il devrait y avoir une solution qui comprend déjà tout le nécessaire. Surtout si pendant le processus de développement, vous deviez changer la possibilité de sélectionner un seul élément pour un choix multiple.


Après avoir étudié l'approche MVVM et y avoir pleinement plongé, la question susmentionnée s'est posée de manière beaucoup plus marquée. De plus, l'adaptateur lui-même est au niveau de la View , tandis que les informations sur les éléments sélectionnés sont souvent extrêmement nécessaires pour le ViewModel .


Peut-être que je n'ai pas passé assez de temps à chercher des réponses sur Internet, mais, en tout cas, je n'ai pas trouvé de solution toute faite. Cependant, dans l'un des projets, j'ai proposé une idée de mise en œuvre qui pourrait bien être universelle, j'ai donc voulu la partager.


Remarque . Bien qu'il soit logique et approprié que MVVM sur Android fasse une implémentation avec LiveData , à ce stade, je ne suis pas prêt à écrire du code en l'utilisant. Ce n'est donc que pour l'avenir. Mais la solution finale s'est avérée sans dépendances Android , ce qui permet potentiellement de l'utiliser sur n'importe quelle plate-forme où kotlin peut fonctionner.


SelectionManager


Pour résoudre ce problème, l'interface générale de SelectionManager été compilée:


 interface SelectionManager { fun clearSelection() fun selectPosition(position: Int) fun isPositionSelected(position: Int): Boolean fun registerSelectionChangeListener(listener: (position: Int, isSelected: Boolean) -> Unit): Disposable fun getSelectedPositions(): ArrayList<Int> fun isAnySelected(): Boolean fun addSelectionInterceptor(interceptor: (position: Int, isSelected: Boolean, callback: () -> Unit) -> Unit): Disposable } 

Par défaut, il existe déjà 3 implémentations différentes:


  • MultipleSelection - l'objet vous permet de sélectionner autant d'éléments que vous le souhaitez dans la liste;
  • SingleSelection - un objet vous permet de sélectionner un seul élément;
  • NoneSelection - l'objet ne permet pas du tout de sélectionner des éléments.

Probablement, avec ce dernier, il y aura la plupart de toutes les questions, donc je vais essayer de montrer déjà sur un exemple.


Adaptateur


Il est censé ajouter l'objet SelectionManager à l'adaptateur en tant que dépendance via le constructeur.


 class TestAdapter(private val selectionManager: SelectionManager) : RecyclerView.Adapter<TestHolder>() { //class body } 

Dans l'exemple, je ne m'embêterai pas avec la logique de traitement du clic sur un élément, nous convenons donc simplement que le titulaire (sans détails) est entièrement responsable de la nomination de l'écouteur de clic.


 class TestHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun bind(item: Any, onItemClick: () -> Unit) { //all bind logic } } 

De plus, pour que cette magie fonctionne, l'adaptateur doit effectuer les 3 étapes suivantes:


1. onBindViewHolder


Passez un rappel à la méthode de bind du titulaire, qui appellera selectionManager.selectPosition(position) pour l'élément affiché. Ici aussi, vous devrez probablement modifier l'affichage (le plus souvent uniquement l'arrière-plan) selon que l'élément est actuellement sélectionné - vous pouvez appeler selectionManager.isPositionSelected(position) pour cela.


 override fun onBindViewHolder(holder: TestHolder, position: Int) { val isItemSelected = selectionManager.isPositionSelected(position) //do whatever you need depending on `isItemSelected` value val item = ... //get current item by `position` value holder.bind(item) { selectionManager.selectPosition(position) } } 

2. registerSelectionChangeListener


Pour que l'adaptateur mette à jour les éléments pressés en temps opportun, vous devez vous abonner à l'action appropriée. Et n'oubliez pas que le résultat renvoyé par la méthode d'abonnement doit être enregistré.


 private val selectionDisposable = selectionManager.registerSelectionChangeListener { position, isSelected -> notifyItemChanged(position) } 

Je note que dans ce cas, la valeur du paramètre isSelected pas importante, car avec tout changement, l'apparence de l'élément change. Mais rien ne vous empêche d'ajouter des traitements supplémentaires, pour lesquels cette valeur est importante.


3. selectionDisposable


À l'étape précédente, je n'ai pas simplement dit que le résultat de la méthode doit être enregistré - un objet est renvoyé qui efface l'abonnement pour éviter les fuites. Après avoir terminé les travaux, cet objet doit être consulté.


 fun destroy() { selectionDisposable.dispose() } 

ViewModel


Pour que l'adaptateur de magie soit suffisant, nous allons passer à ViewModel . L'initialisation de SelectionManager extrêmement simple:


 class TestViewModel: ViewModel() { val selectionManager: SelectionManager = SingleSelection() } 

Ici, par analogie avec l'adaptateur, vous pouvez vous abonner aux modifications (par exemple, pour rendre le bouton "Supprimer" inaccessible quand aucun élément n'est sélectionné), mais vous pouvez également cliquer sur un bouton de résumé (par exemple, "Télécharger la sélection") pour obtenir une liste de tous les éléments sélectionnés .


 fun onDownloadClick() { val selectedPositions: ArrayList<Int> = selectionManager.getSelectedPositions() ... } 

Et ici, l'un des inconvénients de ma solution apparaît: au stade actuel, l'objet ne peut stocker que les positions des éléments. Autrement dit, afin d'obtenir exactement les objets sélectionnés, et non leurs positions, une logique supplémentaire sera requise en utilisant la source de données connectée à l'adaptateur (hélas, jusqu'à présent uniquement). Mais j'espère que vous pourrez y faire face.


De plus, il ne reste plus qu'à connecter l'adaptateur au modèle de vue. C'est déjà au niveau de l'activité.


 class TestActivity: AppCompatActivity() { private lateinit var adapter: TestAdapter private lateinit var viewModel: TestViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //`TestViewModel` initialization adapter = TestAdapter(viewModel.selectionManager) } } 

Flexibilité


Pour certains, cela peut être tout à fait compréhensible, mais je tiens également à noter qu'avec cette implémentation, il s'avère facile de contrôler la méthode de sélection dans les adaptateurs. L'adaptateur ne peut désormais sélectionner qu'un seul élément, mais si TestViewModel modifiez l'initialisation de la propriété selectionManager dans TestViewModel , le reste du code fonctionnera «d'une nouvelle manière» sans nécessiter aucune modification. Autrement dit, définissez val selectionManager: SelectionManager = MultipleSelection() , et maintenant l'adaptateur vous permet de sélectionner autant d'éléments que vous le souhaitez.


Et si vous avez une sorte de classe d'adaptateur de base pour toute l'application, vous ne pouvez pas avoir peur d'inclure le SelectionManager de la même manière. En effet, en particulier pour les adaptateurs qui n'impliquent aucun choix d'éléments, il existe une implémentation de NoneSelection - peu importe ce que vous en faites, il n'aura jamais les éléments sélectionnés et n'appellera jamais aucun des écouteurs. Non, il ne lève pas d'exceptions - il ignore simplement tous les appels, mais l'adaptateur n'a pas besoin de le savoir du tout.


Intercepteur


Il existe également des cas dans lesquels une modification de la sélection d'un élément s'accompagne d'opérations supplémentaires (par exemple, chargement d'informations détaillées), avant la réussite de laquelle l'application des modifications conduit à un état incorrect. Surtout pour ces cas, j'ai ajouté un mécanisme d'interception.


Pour ajouter un intercepteur, vous devez appeler la méthode addSelectionInterceptor (encore une fois, vous devez enregistrer le résultat et y accéder une fois terminé). L'un des paramètres de l'intercepteur dans l'exemple de callback: () -> Unit - jusqu'à ce qu'il soit appelé, les modifications ne seront pas appliquées. Autrement dit, en l'absence de réseau, le chargement d'informations détaillées à partir du serveur ne pourra pas se terminer avec succès, en conséquence, l'état du selectionManager utilisé ne changera pas. Si c'est exactement le comportement que vous recherchez - vous avez besoin de cette méthode.


 val interceptionDisposable = selectionManager.addSelectionInterceptor { position: Int, isSelected: Boolean, callback: () -> Unit -> if(isSelected) { val selectedItem = ... //get current item by `position` value val isDataLoadingSuccessful: Boolean = ... //download data for `selectedItem` if(isDataLoadingSuccessful) { callback() } } } 

Si nécessaire, vous pouvez connecter autant d'intercepteurs que vous le souhaitez. Dans ce cas, l'appel callback() du premier intercepteur commence à traiter le second. Et seul callback() dans le dernier d'entre eux provoquera finalement un changement dans l'état selectionManager .


Perspectives


  1. L'utilisation de Disposable pour effacer les abonnements est efficace, mais pas aussi pratique que LiveData . La première amélioration de la gamme consiste à utiliser les capacités de android.arch.lifecycle pour un travail plus pratique. Il s'agit très probablement d'un projet distinct, afin de ne pas ajouter de dépendance de plateforme au projet actuel.
  2. Comme je l'ai dit, obtenir une liste d'objets sélectionnés s'est avéré peu pratique. Je veux également essayer d'implémenter un objet qui peut fonctionner avec un conteneur de données de la même manière. Dans le même temps, il peut s'agir d'une source de données pour l'adaptateur.

Les références


Vous pouvez trouver les codes source sur le lien - GitHub
Le projet peut également être implémenté via gradle - ru.ircover.selectionmanager:core:1.0.0

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


All Articles