Долго я искал в сети способ сделать так, чтобы данные при построении списка 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 мне сказать особо нечего. В макете Активности пишем его код или вставляем при помощи Визуального Дизайнера.
<androidx.recyclerview.widget.RecyclerView android:id="@+id/rcView" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" />
1.3 Адаптер
С адаптером дела обстоят несколько сложнее. Он должен быть описан при помощи двух классов, наследующихся от RecyclerView.Adapter и RecyclerView.ViewHolder соответственно. Первый, как я понял, отвечает за работу всего списка. А второй — создается для каждого элемента в отдельности и отрисовывает его.
Покажу на примере адаптера, отвечающего за отображение списка консультаций из календаря. В качестве параметра при создании объекта класса AdapterTimetable я передаю данные для построения списка в форме ArrayList<ListMeetings>
class ListMeetings { var clientName = "" var idClient = 0 var start : Long = 0 var end : Long = 0 lateinit var uri: Uri var form = 0 var format = 0 var isParentsExist = false // расчитывается из даты var day = 0 var month = 0 var dayOfWeek = 0 var startTime = "" var endTime = "" var duration = 0 }
Сам код адаптера с некоторыми сокращениями выглядит следующим образом:
class AdapterTimetable( private var listItems: ArrayList<ListMeetings> ) : RecyclerView.Adapter<AdapterTimetable.MyHolder>() { 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<ListMeetings>){ // обновляем список 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) следующим образом:
android { ... buildFeatures { viewBinding = true } }
В классе MyHolder единственная функция drawItem заполняет содержимым компоненты макета каждого элемента списка. В качестве параметра она получает данные типа ListMeetings.
В классе адаптера переопределяются три функции: onCreateViewHolder, onBindViewHolder и getItemCount. Первая «раздувает» макет элемента списка (inflate) при его создании. Вторая — наполняет элемент содержимым. А третья — возвращает количество элементов списка.
Также в адаптере должны присутствовать еще две функции: updateAdapter и removeItem. Первая обновляет содержимое списка, а вторая удаляет из него один элемент.
Надеюсь, что мои столь краткие комментарии достаточны, чтобы понять, как работает вышеприведенный код. Подробнее почитать о том, как работает RecyclerView вы можете, например, на сайте Александра Климова: http://developer.alexanderklimov.ru/android/views/recyclerview-kot.php
1.4 Функция инициализации адаптера
private fun initAdapter(){ el.rcView.layoutManager = LinearLayoutManager(requireContext()) adapter = AdapterTimetable(ArrayList()) el.rcView.adapter = adapter }
Адаптер используем в активности или фрагменте, который связан с макетом, содержащим RecyclerView. Указываем, что для отображения элементов списка будет использоваться LinearLayoutManager (элементы будут располагаться вертикально один под другим). Создаем adapter и присваеваем его нашему компоненту Recyclerview (rcView).
1.5 Функция заполнения списка
fun fillAdapter(){ val list = calManager.readMeetingsList() if (list.isNotEmpty()) adapter.updateAdapter(list) }
Здесь пока все просто — загружаем данные из Базы Данных (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:
override fun onResume() { super.onResume() fillAdapter() }
Из кода видно, что изменились и вызовы функций calManager.readMeetingsList (она теперь возвращает только список консультаций с датой больше заданной и определенного количества) и adapter.updateAdapter (эта функция теперь содержит еще параметр clear — очищать ли список).
Вокруг блока кода, работающего с данными стоят строчки adapter.startLoading() и adapter.setLoaded() Это установка флага загрузки. Она необходима, чтобы при прокрутке списка не вызывалась слишком часто функция fillAdapter (подробнее смотрите далее).
2.2 Модернизация функции updateAdapter
fun updateAdapter(items: ArrayList<ListMeetings>, clear: Boolean = true){ if (clear) listItems.clear() listItems.addAll(items) notifyDataSetChanged() }
При обновлении списка теперь учитывается нужно ли его очистить или нет. Если нет, то новые элементы просто добавляются в конец списка.
2.3 Подгрузка данных и флаг загрузки
class AdapterTimetable( private var listItems: ArrayList<ListMeetings> ) : RecyclerView.Adapter<AdapterTimetable.MyHolder>() { 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 { fun onLoadMore() }
Сначала про флаг загрузки. В классе адаптера вводим булеву переменную 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 <= lastVisibleItem + Const.RC_ITEM_BUFFER / 2) { adapter.loadMore?.onLoadMore() } } }) // переопределяем функцию onLoadMore adapter.setLoadMore(object : MyLoadMore { override fun onLoadMore() { fillAdapter(adapter.getLastItemDate(), Const.RC_ITEM_BUFFER, false, false) } }) }
К созданию адаптера добавляем две вещи: слушатель прокрутки (addOnScrollListener) и переопределение функции onLoadMore.
В слушателе прокрутки проверяем флаг загрузки и последний видимый элемент. Если положение списка близко к концу (RC_ITEM_BUFFER / 2), то подгружаем элементы при помощи модернизированной функции fillAdapter, указав в параметрах дату крайней консультации, размер пакета подгрузки и выключив очистку списка.
Ответ
Получилось вполне рабочее решение. Я его в таком виде в сети не встречал. Делюсь. Возможно, где-то в чем-то я перемудрил или не учел некоторые возможности, о которых просто пока понятия не имею. Буду рад вашим комментариям и предложениям. Есть вопрос про подгузку данных. Как вы думаете, насколько необходимо ее осуществлять при работе с БД на устройстве? Может, просто подтягивать все данные и грузить их целиком в список?
Приложение над которым я сейчас работаю: «Учет клиентов» https://play.google.com/store/apps/details?id=ru.keytomyself.customeraccounting
Собираюсь добавить возможность интеграции с Гугл Календарем. В связи с этим тоже возникает множество вопросов про списки RecyclerView. Там ведь повторяющиеся события, исключения ипрочие сложности…
ссылка на оригинал статьи https://habr.com/ru/post/647037/
Добавить комментарий