Уже примерно после 3-его самописного адаптера, в котором надо было реализовывать логику запоминания выбранного элемента, у меня появились мысли, что должно же быть какое-то решение, которое уже включит в себя всё необходимое. Особенно, если в процессе разработки приходилось менять возможность выбора только одного элемента на множественный выбор. После изучения подхода MVVM и полноценного погружения в него, упомянутый выше вопрос встал намного более заметно. Тем более, что сам адаптер находится на уровне View, в то время как информация о выбранных элементах зачастую крайне необходима для ViewModel. Возможно, я провёл за поиском ответов в интернете недостаточное количество времени, но, в любом случае, готового решения я не нашёл. Однако в одном из проектов мне пришла идея реализации, которая вполне могла бы быть универсальной, поэтому мне захотелось поделиться ею. Замечание. Хоть для MVVM на Android было бы логично и уместно сделать реализацию с выставленным наружу LiveData, на данном этапе я ещё не готов к написанию кода с его использованием. Так что это пока только на перспективу. Зато итоговое решение получилось без зависимостей от Android, что потенциально даёт возможность использовать его на любой платформе, где может работать kotlin. Для решения поставленной задачи был составлен общий интерфейс SelectionManager: По умолчанию уже имеется 3 различные реализации: Наверно, с последним будет больше всего вопросов, так что попробую дальше уже показывать на примере. В адаптер предполагается добавить объект SelectionManager в качестве зависимости через конструктор. В примере не буду заморачиваться с логикой обработки нажатия на элемент, так что просто условимся, что за назначение слушателя клика целиком отвечает холдер (без деталей). Далее, чтобы эта магия заработала, в адаптере необходимо совершить 3 следующих шага: В метод bind холдера передать коллбэк, который вызовет selectionManager.selectPosition(position) для отображаемого элемента. Так же здесь вам скорее всего понадобится менять отображение (чаще всего только фон) в зависимости от того, является ли элемент выбранным на данный момент — для этого можно вызывать selectionManager.isPositionSelected(position). Для того, чтобы адаптер своевременно обновлял нажатые элементы, необходимо подписаться на соответствующее действие. Причём не забудьте, что результат, возвращённый методом подписки, нужно обязательно сохранить. Отмечу, что в данном случае значение параметра isSelected не важно, так как при любом изменении внешний вид элемента меняется. Но вам ничто не мешает добавить дополнительную обработку, для которой это значение важно. На предыдущем шаге я не просто так говорил, что результат выполнения метода необходимо сохранить — возвращается объект, который очищает подписку для избежания утечек. После окончания работы к этому объекту обязательно нужно обратиться. Для адаптера магии достаточно, перейдём к ViewModel. Инициализация SelectionManager крайне проста: Здесь можно по аналогии с адаптер подписаться на изменения (например, чтобы делать кнопку "Удалить" недоступной, когда не выбран ни один элемент), но можно и по нажатию какой-либо итоговой кнопки (например, "Загрузить выбранное") получить список всех выбранных. И вот тут выходит на передний план один из минусов моего решения: на текущем этапе объект способен хранить только позиции элементов. То есть, чтобы получить именно выбранные объекты, а не их позиции, потребуется дополнительная логика с использованием источника данных, подключенного к адаптеру (увы, пока только так). Но, надеюсь, вы справитесь с этим. Далее останется только связать адаптер с вьюмоделью. Это уже на уровне активити. Для кого-то это может быть и так достаточно понятным, но я хочу дополнительно отметить, что при такой реализации получается легко управлять способом выбора в адаптерах. Сейчас адаптер может выбирать только один элемент, но если в TestViewModel изменить инициализацию свойства selectionManager, весь остальной код заработает "по-новому", не потребовав при этом никаких изменений. То есть, поставьте val selectionManager: SelectionManager = MultipleSelection(), и теперь адаптер позволяет выбирать сколько угодно элементов. А если у вас есть какой-то базовый класс адаптера на всё приложение, вы можете не бояться таким же образом включить в него SelectionManager. Ведь специально для адаптеров, которые вовсе не предполагают выбор элементов, есть реализация NoneSelection — что с ним ни делай, он никогда не будет иметь выбранных элементов и никогда не вызовет ни один из листенеров. Нет, он не бросает исключений — он просто игнорирует все вызовы, но адаптеру этого знать совершенно не нужно. Так же бывают случаи, при которых изменение выбора элемента сопровождается дополнительными операциями (например, подгрузкой детальной информации), до успешного завершения которых применение изменений ведёт к некорректному состоянию. Специально для этих случаев я добавил механизм перехвата. Для добавления перехватчика нужно вызвать метод addSelectionInterceptor (опять же надо сохранить результат и обратиться к нему после завершения работы). Один из параметров у перехватчика в примере callback: () -> Unit — до тех пор, пока он не будет вызван, изменения применены не будут. То есть, в случае отсутствия сети подгрузка детальной информации с сервера не сможет завершиться успешно, соответственно состояние используемого selectionManager не изменится. Если это именно то поведение, к которому вы стремитесь — этот метод вам нужен. При необходимости можно подключить сколько угодно перехватчиков. В этом случае вызов callback() первого перехватчика запустить обработку второго. И только callback() у последнего из них наконец вызовет изменение состояния selectionManager-а. Исходные коды можете найти по ссылке — GitHub
Так же проект доступен для внедрения через gradle — ru.ircover.selectionmanager:core:1.0.0
Следующая статья — про решение на LiveData