Как стать автором
Обновить

MVVM и выбор элементов в адаптере — LiveData

Время на прочтение6 мин
Количество просмотров2.5K

В своей предыдущей статье я рассказал о первой попытке написать библиотеку для простого и удобного выбора элементов из списка в Android с учётом подхода MVVM. В прошлый раз решение не было привязано к платформе, поэтому конечной цели я не достиг.


Спустя несколько месяцев, когда я достаточно подумал, попрокрастинировал и поработал, у меня уже получилось решение, более подходящее именно для Android, так как основано на LiveData. Прошу всех интересующихся ознакомиться.


Внесённые изменения


Прежде чем говорить о новой библиотеке, для начала по возможности кратко опишу изменения в том, о чём говорил в прошлой статье.


Наследование перехватчиков


Первое, что мне показалось странным в собственном решение, так это общий интерфейс, обязывающий реализовывать логику перехватчиков — метод addSelectionInterceptor — таким образом я по сути не дал унаследовать свою стандартную реализацию этого метода для новых классов. Поэтому указанный метод я вынес в отдельный интерфейс — InterceptableSelectionManager, который наследует общий интерфейс. В итоге пользователи могут выбрать вариант, в котором их не вынуждают работать с перехватчиками, так как этот функционал, как мне кажется, всё-таки не очень часто востребован.


Ну и говоря о наследовании написанной мной логики, сделан базовый абстрактный класс BaseInterceptableSelectionManager, в котором только перехватчики и описаны. Нравится — используйте на здоровье. В любом случае, вы всегда можете использовать интерфейс и всё с нуля реализовать по-своему.


Более точное именование


Да, я изменил имя метода интерфейса в проекте, который объявил готовым и выложил в общий доступ. Да, я понимаю, что гореть мне в аду за такие выходки. В своё оправдание могу лишь сказать, что имя на самом деле неточно описывало происходящее в нём действие. Речь о методе, который в предыдущей версии назывался selectPosition, а теперь он clickPosition.


Дело в том, что прежний вариант говорил о том, что после выполнения переданная позиция должна оставаться выбранной в любом случае, в то время как внутренняя работа классов меняла состояние выделения для этого элемента. Я принял решение, что данная непрозрачность должна быть срочно устранена. Искренне надеюсь, что эта попытка исправить собственную ошибку найдёт понимание среди пользователей (если вдруг были те, кто пробовал предыдущую версию).


Ну и здесь же скажу, что я так же добавил в интерфейс метод deselectPosition, который снимает выделение с элемента, если оно было. Тут уже с именем и поведением полное совпадение.


Новые возможности


Теперь о новье. Начну с того, что добавлено в предыдущую библиотеку, а в конце уже расскажу о новой.


Источник данных


Как я указывал в предыдущей статье, одним из недостатков моего решения было то, что оно оперирует исключительно положением выбранных элементов, из-за чего для получения непосредственно выбранных элементов пользователю пришлось бы самостоятельно писать логику их получения. Я нашёл решение, как исправить этот недочёт — создать класс SelectableDataSource.


class SelectableDataSource<T>(private var dataSource: ArrayList<T>,
                              private val selectionManager: SelectionManager)
    : SelectionManager by selectionManager

Что хотел бы здесь отметить:


  1. Класс принимает в конструктор SelectionManager, который будет отвечать за логику выбора элементов. То есть, для изменения с единичного выбора на множественный нужно всего лишь в конструктор передать экземпляр другого класса. Всё остальное будет работать без изменений.


  2. Класс сам по себе реализует интерфейс SelectionManager, что позволяет использовать его в тех же случаях, когда использовались более простые экземпляры без источника данных.


  3. В качестве источника данных используется ArrayList, передаваемый первым параметров в конструктор. Так как класс позволяет изменять источник (о чём будет ниже), есть дополнительный конструктор, в который элементы уже передавать не нужно — это будет интерпретировано как пустой ArrayList.


    constructor(selectionManager: SelectionManager) : this(arrayListOf(), selectionManager)

  4. Для изменения источника данных есть специальный метод setDataSource.


    fun setDataSource(dataSource: ArrayList<T>, changeMode: ChangeDataSourceMode)

    Про замену самого списка элементов говорить нечего, а вот об обработке уже выделенных элементов стоит рассказать. Есть несколько возможных вариантов режима работы:


    • ChangeDataSourceMode.ClearAllSelection — самый простой, при котором просто всё выделение сбрасывается полностью. Этот режим выбран по умолчанию, так что если он вам подходит, можете второй параметр в метод вовсе не передавать;
    • ChangeDataSourceMode.HoldSelectedPositions — выделение будет оставлено согласно позициям в массиве до изменений. То есть, если элемент в позиции 2 был выделен до вызова метода, то после него элемент в той же позиции 2 останется выделенным. Разумеется, это справедливо только для случаев, когда после изменений есть хотя бы 3 элемента;
    • ChangeDataSourceMode.HoldSelectedItems — будет оставлено выделение именно на выбранных элементах (если они остались). Как мне видится, наиболее часто необходимый режим. В случае, когда работа ведётся с элементами вашего кастомного класса, не забудьте переопределить метод Equals.

  5. Так как в описываемом классе наконец появилась привязка к источнику данных, в нём я добавил логику с ArrayIndexOutOfBoundsException. Поэтому вызов clickPosition с неверной позицией сгенерирует исключение, а в случае изменения количества элементов после вызова setDataSource слушатели будут оповещены только о тех позициях, которые остались в новом источнике. И тут важно отметить, чтобы вы корректно назначали своих слушателей, ибо при попытке отслеживания SelectionManager'а, который был передан в конструктор, отсеивания более недоступных позиций не произойдёт — он же ничего об источнике данных не знает.


    val selectionManager: SelectionManager
    val dataSource = SelectableDataSource<User>(selectionManager)
    selectionManager.registerSelectionChangeListener { position: Int, isSelected: Boolean -> ...} //опасно
    dataSource.registerSelectionChangeListener { position: Int, isSelected: Boolean -> ...} //безопасно

  6. Внимательные читали могли заметить, что в самом начале статьи я говорил о разделении интерфейсов SelectionManager'а, а тут воспользовался только одним из них. Так вот, для перехватываемого варианта есть отдельный класс InterceptableSelectableDataSource. Из отличий только, собственно, реализация интерфейса с перехватчиками, а всё остальное без изменений.


    class InterceptableSelectableDataSource<T>(dataSource: ArrayList<T>,
                         private val selectionManager: InterceptableSelectionManager)
    : SelectableDataSource<T>(dataSource, selectionManager),
        InterceptableSelectionManager


LiveDataSource


И вот она — кульминация статьи. Наконец-то я созрел для того, чтобы уложить всё необходимое в LiveData. При инициализации нужно просто передать InterceptableSelectionManager, который вам необходим.


val users = LiveDataSource<User>(MultipleSelection())

Для изменения источника данных можно использовать метод setDataSource, в точности повторяющий сигнатуру описанного выше класса SelectableDataSource.


val newValues: ArrayList<User>
users.setDataSource(newValues)
//или
users.setDataSource(newValues, ChangeDataSourceMode.HoldSelectedItems)

Теперь об отслеживании изменений. Для наблюдения за изменениями источника данных есть свойство allItems, которое тоже является LiveData.


viewModel.users.allItems.observe(this, Observer { items: ArrayList<User> -> ... })
//this - ссылка на текущую Activity или любой другой LifecycleOwner

Для отслеживания изменений списка выбранных элементов можно совершить аналогичное действие со свойством selectedItems.


viewModel.users.selectedItems.observe(this, Observer { selectedItems: ArrayList<User> -> ... })

В том случае, если нужно следить не за изменением полного списка выбранный элементов, а за конкретными изменениями выделения, есть методы observeSelectionChange и observeItemSelectionChange. Их различие в том, что первый оперирует положением элементов, а второй — непосредственно самими элементами.


viewModel.users.observeSelectionChange(this) { position: Int, isSelected: Boolean -> ... }
viewModel.users.observeItemSelectionChange(this) { user: User, isSelected: Boolean -> ... }

Перспективы


Да, я всё ещё не закончил и опять вижу, что можно было бы доработать.


  1. Пожалуй, логичным продолжением вижу создание базового класса для адаптера на основе RecyclerView.Adapter, который помимо простого интерфейса подписки на мою LiveData будет включать в себя наиболее часто используемую логику работы с линейным списком элементов. Пока нет полной уверенности, что получится что-то универсальное и удобное в использовании, но надежды я не теряю.
  2. На данный момент я даже не анализировал своё решение на потокобезопасность. Подозреваю, что стоило бы.
  3. Сейчас класс LiveDataSource не имеет возможности простой замены вложенного в него SelectionManager'а, что иногда могло бы быть полезным. Ведь есть распространённый подход, когда в обычном режиме выбирается только один элемент, а по долгому тапу переключается на множественный выбор. Подумаю, можно ли что-то с этим сделать.

Сылки


Исходные коды для библиотек:



Ссылки в Gradle:
implementation 'ru.ircover.selectionmanager:core:1.1.0'
implementation 'ru.ircover.selectionmanager:livesource:1.0.0'

Теги:
Хабы:
Всего голосов 3: ↑3 и ↓0+3
Комментарии0

Публикации

Истории

Работа

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань