MVVM und Auswahl von Elementen im Adapter

Bereits nach dem 3. samopisny-Adapter, in dem die Logik des Speicherns des ausgewählten Elements implementiert werden musste, hatte ich die Überlegung, dass es eine Lösung geben sollte, die bereits alles Notwendige enthält. Insbesondere, wenn Sie während des Entwicklungsprozesses die Möglichkeit ändern mussten, nur ein Element für die Mehrfachauswahl auszuwählen.


Nach dem Studium des MVVM-Ansatzes und dem vollständigen Eintauchen in ihn stellte sich die oben genannte Frage viel deutlicher. Darüber hinaus befindet sich der Adapter selbst auf der View , während für das ViewModel häufig Informationen zu den ausgewählten Elementen äußerst ViewModel .


Vielleicht habe ich nicht genug Zeit damit verbracht, im Internet nach Antworten zu suchen, aber auf jeden Fall habe ich keine fertige Lösung gefunden. In einem der Projekte hatte ich jedoch eine Implementierungsidee, die durchaus universell sein könnte. Deshalb wollte ich sie teilen.


Bemerkung . Obwohl es für MVVM unter Android logisch und angemessen wäre, eine Implementierung mit LiveData verfügbar zu machen, bin ich zum LiveData Zeitpunkt nicht bereit, Code damit zu schreiben. Das ist also nur für die Zukunft. Die endgültige Lösung kam jedoch ohne Android Abhängigkeiten aus, sodass sie möglicherweise auf jeder Plattform eingesetzt werden kann, auf der kotlin funktionieren kann.


SelectionManager


Um dieses Problem zu lösen, wurde die allgemeine SelectionManager Oberfläche kompiliert:


 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 } 

Standardmäßig gibt es bereits 3 verschiedene Implementierungen:


  • MultipleSelection - Mit dem Objekt können Sie beliebig viele Elemente aus der Liste auswählen.
  • SingleSelection - Mit einem Objekt können Sie nur ein Element auswählen.
  • NoneSelection - Das Objekt erlaubt es überhaupt nicht, Elemente auszuwählen.

Wahrscheinlich wird es bei letzterem die meisten Fragen geben, deshalb werde ich versuchen, dies bereits an einem Beispiel zu zeigen.


Adapter


Das SelectionManager Objekt soll dem Adapter als Abhängigkeit über den Konstruktor hinzugefügt werden.


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

In diesem Beispiel werde ich mich nicht mit der Logik des Verarbeitens des Klicks auf ein Element befassen, daher stimmen wir nur darin überein, dass der Inhaber (ohne Details) vollständig für die Ernennung des Klick-Listeners verantwortlich ist.


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

Damit diese Magie funktioniert, muss der Adapter die folgenden 3 Schritte ausführen:


1. onBindViewHolder


Übergeben Sie einen Rückruf an die bind Methode des Inhabers, die selectionManager.selectPosition(position) für das angezeigte Element aufruft. Auch hier müssen Sie höchstwahrscheinlich die Anzeige ändern (meist nur den Hintergrund), je nachdem, ob das Element aktuell ausgewählt ist - Sie können hierfür selectionManager.isPositionSelected(position) aufrufen.


 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


Damit der Adapter die gedrückten Elemente rechtzeitig aktualisiert, müssen Sie die entsprechende Aktion abonnieren. Vergessen Sie nicht, dass das von der Abonnementmethode zurückgegebene Ergebnis gespeichert werden muss.


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

Ich isSelected fest, dass in diesem Fall der Wert des isSelected Parameters nicht wichtig ist, da sich bei jeder Änderung das Erscheinungsbild des Elements ändert. Nichts hindert Sie daran, zusätzliche Verarbeitung hinzuzufügen, für die dieser Wert wichtig ist.


3. selectionDisposable


Im vorherigen Schritt habe ich nicht nur gesagt, dass das Ergebnis der Methode gespeichert werden muss - es wird ein Objekt zurückgegeben, das das Abonnement löscht, um Undichtigkeiten zu vermeiden. Nach Abschluss der Arbeiten muss dieses Objekt konsultiert werden.


 fun destroy() { selectionDisposable.dispose() } 

ViewModel


Damit der Adapter von Magic ausreicht, werden wir auf ViewModel übergehen. Die Initialisierung des SelectionManager denkbar einfach:


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

Analog zum Adapter können Sie hier die Änderungen abonnieren (z. B. um den Zugriff auf die Schaltfläche "Löschen" zu unterbinden, wenn keine Elemente ausgewählt sind). Sie können jedoch auch auf eine Zusammenfassungsschaltfläche klicken (z. B. "Ausgewählte herunterladen"), um eine Liste aller ausgewählten Elemente zu erhalten .


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

Und hier tritt einer der Nachteile meiner Lösung in den Vordergrund: Zum gegenwärtigen Zeitpunkt kann das Objekt nur die Positionen der Elemente speichern. Das heißt, um genau die ausgewählten Objekte und nicht deren Positionen zu erhalten, ist zusätzliche Logik erforderlich, die die an den Adapter angeschlossene Datenquelle verwendet (bisher leider nur). Aber hoffentlich können Sie damit umgehen.


Weiterhin bleibt nur der Adapter mit dem Ansichtsmodell zu verbinden. Dies ist bereits auf der Aktivitätsebene.


 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ät


Für einige mag dies durchaus verständlich sein, aber ich möchte zusätzlich darauf hinweisen, dass es sich bei dieser Implementierung als einfach herausstellt, die Auswahlmethode in den Adaptern zu steuern. Jetzt kann der Adapter nur ein Element auswählen. Wenn TestViewModel die Initialisierung der selectionManager Eigenschaft in TestViewModel , TestViewModel der Rest des Codes "auf neue Weise", ohne dass Änderungen erforderlich sind. Das heißt, setzen Sie val selectionManager: SelectionManager = MultipleSelection() , und jetzt können Sie mit dem Adapter so viele Elemente auswählen, wie Sie möchten.


Und wenn Sie eine Art Basisadapterklasse für die gesamte Anwendung haben, können Sie sich nicht scheuen, den SelectionManager auf die gleiche Weise einzubeziehen. Insbesondere für Adapter, die überhaupt keine Auswahl von Elementen implizieren, gibt es eine Implementierung von NoneSelection - unabhängig davon, was Sie damit tun, werden die ausgewählten Elemente niemals vorhanden sein und keiner der Listener wird angerufen. Nein, er wirft keine Ausnahmen - er ignoriert einfach alle Anrufe, aber der Adapter muss das überhaupt nicht wissen.


Abfangjäger


Es gibt auch Fälle, in denen das Ändern der Auswahl eines Elements mit zusätzlichen Vorgängen einhergeht (z. B. dem Laden detaillierter Informationen), bis der erfolgreiche Abschluss des Anwendens der Änderungen zu einem inkorrekten Zustand führt. Speziell für diese Fälle habe ich einen Abfangmechanismus hinzugefügt.


Um einen Interceptor hinzuzufügen, müssen Sie die Methode addSelectionInterceptor (erneut müssen Sie das Ergebnis speichern und nach Abschluss darauf zugreifen). Einer der Parameter des Interceptors im callback: () -> Unit Beispiel callback: () -> Unit - Bis zum Aufruf werden die Änderungen nicht übernommen. Das heißt, wenn kein Netzwerk vorhanden ist, kann das Laden detaillierter Informationen vom Server nicht erfolgreich abgeschlossen werden. Dementsprechend ändert sich der Status des verwendeten selectionManager Managers nicht. Wenn dies genau das Verhalten ist, das Sie anstreben, benötigen Sie diese Methode.


 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() } } } 

Bei Bedarf können Sie so viele Abfangjäger anschließen, wie Sie möchten. In diesem Fall beginnt der callback() -Aufruf des ersten Interceptors mit der Verarbeitung des zweiten. Und nur callback() im letzten von ihnen bewirkt schließlich eine Änderung des selectionManager Status.


Aussichten


  1. Die Verwendung von Disposable zum Löschen von Abonnements ist effektiv, jedoch nicht so praktisch wie LiveData . Die erste Verbesserung besteht darin, die Funktionen von android.arch.lifecycle für ein komfortableres Arbeiten zu nutzen. Höchstwahrscheinlich handelt es sich hierbei um ein separates Projekt, um die Plattformabhängigkeit nicht zu erhöhen.
  2. Wie ich bereits sagte, erwies es sich als unpraktisch, eine Liste ausgewählter Objekte zu erhalten. Ich möchte auch versuchen, ein Objekt zu implementieren, das auf die gleiche Weise mit einem Datencontainer arbeiten kann. Gleichzeitig kann es sich um eine Datenquelle für den Adapter handeln.

Referenzen


Sie finden die Quellcodes unter dem Link - GitHub
Das Projekt kann auch über gradle - ru.ircover.selectionmanager:core:1.0.0 implementiert werden

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


All Articles