MVVM y selección de elementos en el adaptador

Ya después del tercer adaptador samopisny en el que era necesario implementar la lógica de memorizar el elemento seleccionado, pensé que debería haber alguna solución que ya incluyera todo lo necesario. Especialmente si durante el proceso de desarrollo tuvo que cambiar la capacidad de seleccionar solo un elemento para la opción múltiple.


Después de estudiar el enfoque MVVM y sumergirse por completo en él, la pregunta antes mencionada surgió de manera mucho más marcada. Además, el adaptador en sí está en el nivel de View , mientras que la información sobre los elementos seleccionados a menudo es extremadamente necesaria para ViewModel .


Quizás no pasé suficiente tiempo buscando respuestas en Internet, pero, en cualquier caso, no encontré una solución preparada. Sin embargo, en uno de los proyectos se me ocurrió una idea de implementación que bien podría ser universal, por lo que quería compartirla.


Observación Aunque sería lógico y apropiado para MVVM en Android hacer una implementación con LiveData , en este momento no estoy listo para escribir código con él. Entonces esto es solo para el futuro. Pero la solución final resultó ser sin dependencias de Android , lo que potencialmente hace posible su uso en cualquier plataforma donde kotlin pueda funcionar.


SelectionManager


Para resolver este problema, se compiló la interfaz general SelectionManager :


 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 } 

Por defecto, ya hay 3 implementaciones diferentes:


  • MultipleSelection : el objeto le permite seleccionar tantos elementos como desee de la lista;
  • SingleSelection : un objeto le permite seleccionar solo un elemento;
  • NoneSelection : el objeto no permite seleccionar elementos en absoluto.

Probablemente, con este último habrá la mayoría de las preguntas, por lo que trataré de mostrar un ejemplo.


Adaptador


Se supone que debe agregar el objeto SelectionManager al adaptador como una dependencia a través del constructor.


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

En el ejemplo, no me molestaré con la lógica de procesar el clic en un elemento, por lo que solo aceptamos que el titular (sin detalles) es completamente responsable de la designación del oyente de clics.


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

Además, para que esta magia funcione, el adaptador debe realizar los siguientes 3 pasos:


1. onBindViewHolder


Pase una devolución de llamada al método de bind del titular, que llamará a selectionManager.selectPosition(position) para el elemento mostrado. También aquí, lo más probable es que necesite cambiar la pantalla (a menudo solo el fondo) dependiendo de si el elemento está actualmente seleccionado; puede llamar a selectionManager.isPositionSelected(position) para esto.


 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


Para que el adaptador actualice los elementos presionados de manera oportuna, debe suscribirse a la acción adecuada. Y no olvide que el resultado devuelto por el método de suscripción debe guardarse.


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

Observo que en este caso el valor del parámetro isSelected no isSelected importante, ya que con cualquier cambio cambia la apariencia del elemento. Pero nada le impide agregar procesamiento adicional, para lo cual este valor es importante.


3. selectionDisposable


En el paso anterior, no solo dije que el resultado del método debe guardarse: se devuelve un objeto que borra la suscripción para evitar fugas. Después de terminar el trabajo, este objeto debe ser consultado.


 fun destroy() { selectionDisposable.dispose() } 

ViewModel


Para que el adaptador de magia sea suficiente, pasaremos a ViewModel . Inicializar el SelectionManager extremadamente simple:


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

Aquí, por analogía con el adaptador, puede suscribirse a los cambios (por ejemplo, para hacer que el botón "Eliminar" sea inaccesible cuando no hay elementos seleccionados), pero también puede hacer clic en un botón de resumen (por ejemplo, "Descargar seleccionados") para obtener una lista de todos los seleccionados .


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

Y aquí destaca uno de los inconvenientes de mi solución: en la etapa actual, el objeto solo puede almacenar las posiciones de los elementos. Es decir, para obtener exactamente los objetos seleccionados, y no sus posiciones, se requerirá una lógica adicional utilizando la fuente de datos conectada al adaptador (por desgracia, hasta ahora solo). Pero espero que puedas manejarlo.


Además, solo queda conectar el adaptador con el modelo de vista. Esto ya está en el nivel de actividad.


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

Flexibilidad


Para algunos, esto puede ser bastante comprensible, pero también quiero señalar que con esta implementación resulta fácil controlar el método de selección en los adaptadores. Ahora el adaptador puede seleccionar solo un elemento, pero si cambia la inicialización de la propiedad selectionManager en TestViewModel , el resto del código funcionará "de una manera nueva" sin requerir ningún cambio. Es decir, establezca val selectionManager: SelectionManager = MultipleSelection() , y ahora el adaptador le permite seleccionar tantos elementos como desee.


Y si tiene algún tipo de clase de adaptador base para toda la aplicación, no puede temer incluir el SelectionManager de la misma manera. De hecho, especialmente para los adaptadores que no implican una elección de elementos, existe una implementación de NoneSelection ; no importa lo que haga con él, nunca tendrá los elementos seleccionados y nunca llamará a ninguno de los oyentes. No, él no lanza excepciones, simplemente ignora todas las llamadas, pero el adaptador no necesita saberlo en absoluto.


Interceptor


También hay casos en los que cambiar la selección de un elemento va acompañado de operaciones adicionales (por ejemplo, cargar información detallada), antes de que la aplicación exitosa de los cambios conduzca a un estado incorrecto. Especialmente para estos casos, agregué un mecanismo de intercepción.


Para agregar un interceptor, debe llamar al método addSelectionInterceptor (nuevamente, debe guardar el resultado y acceder a él una vez completado). Uno de los parámetros del interceptor en el ejemplo de callback: () -> Unit : hasta que se llame, los cambios no se aplicarán. Es decir, en ausencia de una red, la carga de información detallada del servidor no podrá completarse con éxito, por lo tanto, el estado del selectionManager utilizado no cambiará. Si este es exactamente el comportamiento que busca, necesita este método.


 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 es necesario, puede conectar tantos interceptores como desee. En este caso, la callback() del primer interceptor comienza a procesar el segundo. Y solo callback() en el último de ellos finalmente causará un cambio en el estado selectionManager .


Perspectivas


  1. El uso de Disposable para borrar suscripciones es efectivo, pero no tan conveniente como LiveData . La primera mejora en la línea es usar las capacidades de android.arch.lifecycle para un trabajo más conveniente. Lo más probable es que este sea un proyecto separado, para no agregar dependencia de la plataforma al actual.
  2. Como dije, obtener una lista de objetos seleccionados resultó ser un inconveniente. También quiero intentar implementar un objeto que pueda funcionar con un contenedor de datos de la misma manera. Al mismo tiempo, podría ser una fuente de datos para el adaptador.

Referencias


Puede encontrar los códigos fuente en el enlace - GitHub
El proyecto también está disponible para su implementación a través de gradle - ru.ircover.selectionmanager:core:1.0.0

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


All Articles