Android recycler view kotlin

Android Studio. Kotlin. Динамическая подгрузка данных в список RecyclerView

Долго я искал в сети способ сделать так, чтобы данные при построении списка RecyclerView не загружались целиком, а подгружались по мере его пролистывания пользователем. Несколько совершенно разных решений находил на StackOverflow. Пробовал применить — работало, но каждый раз, как-то криво и не надежно. После нескольких месяцев работы над проектом в режиме «Когда все дела сделаны и дети слезли с шеи», я наконец достиг, как мне кажется, идеального решения, чем и хочу поделиться в этой статье.

Задача

Мне нужно было отобразить для пользователя моего приложения список клиентов, консультаций и расходов из Базы Данных приложения в разных фрагментах. Один грамотный программист Баз Данных, по совместительству — мой шурин, объяснил мне что лучше отображать не все данные сразу, а только те, которые видны пользователю и реализовать возможность подгружать данные из БД по мере необходимости.

Скриншот главного экрана моего приложения

Решение

1. Настройка RecyclerView для отображения списка

В нескольких местах в сети прочел, что компонент ListView уже морально устарел. Подробно описывать работу RecyclerView не буду, дам лишь несколько кусков кода в качестве примера с короткими комментариями. Для работы со списком необходимы:

  • Единый макет для элементов спика (rc_timetable.xml).
  • Компонент RecyclerView в макете Активности (androidx.recyclerview.widget.RecyclerView).
  • Адаптер, отвечающий за отображение элементов списка (RecyclerView.Adapter)
  • Функция инициализации адаптера (fun initAdapter).
  • Функция заполнения списка (fun fillAdapter).
Читайте также:  Javascript library user interface

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. Там ведь повторяющиеся события, исключения ипрочие сложности.

Источник

Оцените статью