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>() {
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) {
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)
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)
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 = ...
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
- 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. - 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