- Android Studio. Kotlin. Динамическая подгрузка данных в список RecyclerView
- Задача
- Решение
- 1. Настройка RecyclerView для отображения списка
- 1.1 Макет элементов списка
- 1.2 RecyclerView в макете Активности
- 1.3 Адаптер
- 1.4 Функция инициализации адаптера
- 1.5 Функция заполнения списка
- 2. Динамическая подгрузка данных
- 2.1 Модернизация функции заполнения списка
- 2.2 Модернизация функции updateAdapter
- 2.3 Подгрузка данных и флаг загрузки
- 2.4 Модернизация функции initAdapter
- Ответ
Android Studio. Kotlin. Динамическая подгрузка данных в список RecyclerView
Долго я искал в сети способ сделать так, чтобы данные при построении списка RecyclerView не загружались целиком, а подгружались по мере его пролистывания пользователем. Несколько совершенно разных решений находил на StackOverflow. Пробовал применить — работало, но каждый раз, как-то криво и не надежно. После нескольких месяцев работы над проектом в режиме «Когда все дела сделаны и дети слезли с шеи», я наконец достиг, как мне кажется, идеального решения, чем и хочу поделиться в этой статье.
Задача
Мне нужно было отобразить для пользователя моего приложения список клиентов, консультаций и расходов из Базы Данных приложения в разных фрагментах. Один грамотный программист Баз Данных, по совместительству — мой шурин, объяснил мне что лучше отображать не все данные сразу, а только те, которые видны пользователю и реализовать возможность подгружать данные из БД по мере необходимости.
Решение
1. Настройка RecyclerView для отображения списка
В нескольких местах в сети прочел, что компонент ListView уже морально устарел. Подробно описывать работу RecyclerView не буду, дам лишь несколько кусков кода в качестве примера с короткими комментариями. Для работы со списком необходимы:
- Единый макет для элементов спика (rc_timetable.xml).
- Компонент RecyclerView в макете Активности (androidx.recyclerview.widget.RecyclerView).
- Адаптер, отвечающий за отображение элементов списка (RecyclerView.Adapter)
- Функция инициализации адаптера (fun initAdapter).
- Функция заполнения списка (fun fillAdapter).
1.1 Макет элементов списка
Макет элемента списка ничем не отличается от макетов экранов приложения. Я использую ConstraintLayout, в котором размещаю все, что мне необходимо показать пользователю в качестве отдельного элемента. Не забываю указать родительскому контейнеру layout_height = wrap_content.
1.2 RecyclerView в макете Активности
Про добавление компонета RecyclerView мне сказать особо нечего. В макете Активности пишем его код или вставляем при помощи Визуального Дизайнера.
1.3 Адаптер
С адаптером дела обстоят несколько сложнее. Он должен быть описан при помощи двух классов, наследующихся от RecyclerView.Adapter и RecyclerView.ViewHolder соответственно. Первый, как я понял, отвечает за работу всего списка. А второй — создается для каждого элемента в отдельности и отрисовывает его.
Покажу на примере адаптера, отвечающего за отображение списка консультаций из календаря. В качестве параметра при создании объекта класса AdapterTimetable я передаю данные для построения списка в форме ArrayList
Сам код адаптера с некоторыми сокращениями выглядит следующим образом:
class AdapterTimetable( private var listItems: ArrayList ) : RecyclerView.Adapter() < private lateinit var el: RcTimetableBinding class MyHolder( itemView: View, private val el: RcTimetableBinding, ) : RecyclerView.ViewHolder(itemView) < fun drawItem(item: ListMeetings) < . // указываем время Встречи el.tvStartTime.text = item.startTime // указываем название Услуги el.tvService.text = "Консультация" // указываем тему Встречи el.tvTopic.text = "Тема Встречи" . >> override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyHolder < val inflater = LayoutInflater.from(parent.context) el = RcTimetableBinding.inflate(inflater,parent,false) return MyHolder(el.root, context, el) >override fun onBindViewHolder(holder: MyHolder, position: Int) < // рисуем элемент списка holder.drawItem(listItems[position]) >override fun getItemCount(): Int < return listItems.size >fun updateAdapter(items: ArrayList) < // обновляем список listItems.clear() listItems.addAll(items) notifyDataSetChanged() >fun removeItem(pos: Int, calManager: CalManager) < // удаляем элемент из списка calManager.deleteMeeting(listItems[pos].uri) // удаляем встречу из календаря listItems.removeAt(pos) // удаляем элемент из списка с позиции pos notifyItemRangeChanged(0,listItems.size) // указываем адаптеру новый диапазон элементов notifyItemRemoved(pos) // указываем адаптеру, что один элемент удалился >>
Поясню вкратце вышеприведенный код.
Для обращения к компонентам макета из кода программы я использую некий viewBinding. Эксперты в сети советуют его вместо findViewById. Мне он понравился. Удобно обращаться к компонентам макета через одну переменную (в моей программе — это private val el: RcTimetableBinding). Подключается viewBinding в build.Gradle (Module) следующим образом:
В классе MyHolder единственная функция drawItem заполняет содержимым компоненты макета каждого элемента списка. В качестве параметра она получает данные типа ListMeetings.
В классе адаптера переопределяются три функции: onCreateViewHolder, onBindViewHolder и getItemCount. Первая «раздувает» макет элемента списка (inflate) при его создании. Вторая — наполняет элемент содержимым. А третья — возвращает количество элементов списка.
Также в адаптере должны присутствовать еще две функции: updateAdapter и removeItem. Первая обновляет содержимое списка, а вторая удаляет из него один элемент.
Надеюсь, что мои столь краткие комментарии достаточны, чтобы понять, как работает вышеприведенный код. Подробнее почитать о том, как работает RecyclerView вы можете, например, на сайте Александра Климова: http://developer.alexanderklimov.ru/android/views/recyclerview-kot.php
1.4 Функция инициализации адаптера
Адаптер используем в активности или фрагменте, который связан с макетом, содержащим RecyclerView. Указываем, что для отображения элементов списка будет использоваться LinearLayoutManager (элементы будут располагаться вертикально один под другим). Создаем adapter и присваеваем его нашему компоненту Recyclerview (rcView).
1.5 Функция заполнения списка
Здесь пока все просто — загружаем данные из Базы Данных (calManager.readMeetingsList) и обновляем список новыми данными (adapter.updateAdapter).
2. Динамическая подгрузка данных
Теперь предстоит модернизировать код, добавив возможность подгружать данные по мере пролистывания списка. Вносить изменения придется во блоки кода, описанные выше. Начну с функции заполнения списка данными.
2.1 Модернизация функции заполнения списка
fun fillAdapter(startDate: Long = 0, count: Int = Const.RC_ITEM_BUFFER, clear: Boolean = true) < // указываем в адаптере, что начинаем загрузку данных adapter.startLoading() val list = calManager.readMeetingsList(startDate, count) if (list.isNotEmpty()) adapter.updateAdapter(list, clear) // указываем, что загрузка данных закончена adapter.setLoaded() >
Надо сказать, что для отображения списка консультаций при запросе из Базы Данных я упорядочиваю их по возрастанию даты. И теперь функция fillAdapter принимает следующие параметры:
- startDate — начальная дата, с которой берутся консультации.
- _count — размер пакета данных, количество консультаций, которые будут отображаться.
- clear — очищать или не очищать список.
Видно, что если вызвать функцию fillAdapter без параметров, то по умолчанию данные будут браться с самого начала, их количество будет равно некой константе RC_ITEM_BUFFER (в моем случае — 50) и список будет очищаться. Подобный вызов функции происходит в onResume:
Из кода видно, что изменились и вызовы функций calManager.readMeetingsList (она теперь возвращает только список консультаций с датой больше заданной и определенного количества) и adapter.updateAdapter (эта функция теперь содержит еще параметр clear — очищать ли список).
Вокруг блока кода, работающего с данными стоят строчки adapter.startLoading() и adapter.setLoaded() Это установка флага загрузки. Она необходима, чтобы при прокрутке списка не вызывалась слишком часто функция fillAdapter (подробнее смотрите далее).
2.2 Модернизация функции updateAdapter
fun updateAdapter(items: ArrayList, clear: Boolean = true)
При обновлении списка теперь учитывается нужно ли его очистить или нет. Если нет, то новые элементы просто добавляются в конец списка.
2.3 Подгрузка данных и флаг загрузки
class AdapterTimetable( private var listItems: ArrayList ) : RecyclerView.Adapter() < private lateinit var el: RcTimetableBinding var loadMore : MyLoadMore? = null var isLoading = false . fun setLoadMore(loadMore: MyLoadMore?) < this.loadMore = loadMore >fun startLoading() < isLoading = true >fun setLoaded() < isLoading = false >. fun getLastItemDate(): Long < return if (listItems.size >0) listItems[listItems.size - 1].start else 0 > > interface MyLoadMore
Сначала про флаг загрузки. В классе адаптера вводим булеву переменную isLoading. Если она установлена в true, то значит происходит загрузка элементов и пока функция fillAdapter не доступна.
Подгрузка данных будет осуществляться при помощи функции onLoadMore, которая определяется через интерфейс MyLoadMore. Установливать ее содержимое будем из активности или фрагмента, связанного с RecyclerView при помощи функции setLoadMore Честно говоря, сам не понял, что сказал — для меня это уже слишком. Объясняю, как могу, ибо сам понимаю с трудом. Но смысл в том, чтобы иметь возможность вынести эту функцию за пределы адаптера в активность.
Ну и фунция, возвращающая дату последней консультации в списке, пригодится нам далее при подгрузке данных.
2.4 Модернизация функции initAdapter
private fun initAdapter() < el.rcView.layoutManager = LinearLayoutManager(requireContext()) adapter = AdapterTimetable(ArrayList()) el.rcView.adapter = adapter // при прокрутке запускаем onLoadMore val layoutManager = el.rcView.layoutManager as LinearLayoutManager el.rcView.addOnScrollListener(object: RecyclerView.OnScrollListener() < override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) < super.onScrolled(recyclerView, dx, dy) val totalItemCount = layoutManager.itemCount val lastVisibleItem = layoutManager.findLastVisibleItemPosition() if (!adapter.isLoading && totalItemCount > >) // переопределяем функцию onLoadMore adapter.setLoadMore(object : MyLoadMore < override fun onLoadMore() < fillAdapter(adapter.getLastItemDate(), Const.RC_ITEM_BUFFER, false, false) >>) >
К созданию адаптера добавляем две вещи: слушатель прокрутки (addOnScrollListener) и переопределение функции onLoadMore.
В слушателе прокрутки проверяем флаг загрузки и последний видимый элемент. Если положение списка близко к концу (RC_ITEM_BUFFER / 2), то подгружаем элементы при помощи модернизированной функции fillAdapter, указав в параметрах дату крайней консультации, размер пакета подгрузки и выключив очистку списка.
Ответ
Получилось вполне рабочее решение. Я его в таком виде в сети не встречал. Делюсь. Возможно, где-то в чем-то я перемудрил или не учел некоторые возможности, о которых просто пока понятия не имею. Буду рад вашим комментариям и предложениям. Есть вопрос про подгузку данных. Как вы думаете, насколько необходимо ее осуществлять при работе с БД на устройстве? Может, просто подтягивать все данные и грузить их целиком в список?
Собираюсь добавить возможность интеграции с Гугл Календарем. В связи с этим тоже возникает множество вопросов про списки RecyclerView. Там ведь повторяющиеся события, исключения ипрочие сложности.