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

    В своей предыдущей статье я рассказал о первой попытке написать библиотеку для простого и удобного выбора элементов из списка в 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'

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое