Новостные ленты, маркетплейсы с тысячами товаров и ленты социальных сетей объединяет одна проблема: в них слишком много данных, чтобы загружать всё сразу. Телефон начнёт тормозить, а интернет — расходоваться впустую. Решение придумали в Google — библиотека Paging 3 загружает только ту порцию данных, которую пользователь видит прямо сейчас, и догружает следующую, когда он докручивает до конца списка. В этом уроке мы разберёмся, как добавить такую умную подгрузку в приложение на Kotlin прямо в Android Studio.
Зачем нужен Paging и как он работает
Представьте, что у вас есть коробка с десятью тысячами фотографий. Показывать их все одновременно — плохая затея. Вместо этого вы достаёте из коробки первую пачку из двадцати штук, показываете пользователю, а когда он просматривает их до конца — достаёте следующую пачку. Paging 3 делает ровно это, только вместо фотографий — любые элементы списка.
Библиотека состоит из трёх главных частей: PagingSource (отвечает за получение данных, например с сервера), PagingData (контейнер с порцией элементов) и PagingDataAdapter (специальный адаптер для RecyclerView, который понимает, когда нужно запросить следующую порцию). Все они работают вместе, а мы лишь описываем правила.
Добавляем зависимости
Откройте build.gradle.kts уровня модуля app и добавьте строки для Paging 3. На момент написания статьи актуальна версия 3.3.0, которая совместима с Kotlin 2.x.
dependencies {
implementation("androidx.paging:paging-runtime-ktx:3.3.0")
implementation("androidx.paging:paging-compose:3.3.0") // если нужен Compose
}
После синхронизации Gradle библиотека готова к работе. Никаких дополнительных разрешений в манифесте не требуется.
Создаём PagingSource
PagingSource — это класс, который знает, откуда брать данные. В нашем примере мы сделаем простой источник, генерирующий строки с номерами: «Элемент 1», «Элемент 2» и так далее. В реальном приложении здесь будет запрос к серверу или базе данных Room.
class NumbersPagingSource : PagingSource<Int, String>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, String> {
return try {
val page = params.key ?: 0
val pageSize = params.loadSize
val start = page * pageSize
val items = (start until start + pageSize).map { "Элемент $it" }
LoadResult.Page(
data = items,
prevKey = if (page > 0) page - 1 else null,
nextKey = if (items.size == pageSize) page + 1 else null
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, String>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey
}
}
}
Метод load вызывается библиотекой, когда нужна следующая порция данных. Параметр params.key — это номер страницы, params.loadSize — количество элементов в порции. Мы вычисляем, какие строки нужно вернуть, и возвращаем их вместе с ключами предыдущей и следующей страниц. Если следующей страницы нет (данные кончились), возвращаем null — библиотека поймёт, что список загружен до конца.
Создаём Pager и PagingData
Pager — это объект, который объединяет PagingSource и конфигурацию (размер страницы). Обычно его создают в ViewModel, чтобы данные переживали поворот экрана.
class MainViewModel : ViewModel() {
val pager = Pager(PagingConfig(pageSize = 20)) {
NumbersPagingSource()
}
}
Адаптер для RecyclerView
Вместо обычного RecyclerView.Adapter используем PagingDataAdapter. Его главное отличие — метод submitData, который принимает PagingData и автоматически запрашивает новые порции при прокрутке.
class NumbersAdapter : PagingDataAdapter<String, NumbersAdapter.ViewHolder>(DIFF_CALLBACK) {
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val textView: TextView = view.findViewById(android.R.id.text1)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(android.R.layout.simple_list_item_1, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
getItem(position)?.let { item ->
holder.textView.text = item
}
}
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
}
}
}
}
DiffCallback помогает адаптеру эффективно обновлять только изменившиеся элементы, не перерисовывая весь список. Для простых строк сравнение прямое, для сложных объектов — по уникальному идентификатору.
Собираем всё в Activity
В MainActivity.kt получим ViewModel, адаптер и свяжем их. Flow pager.flow отдаёт порции PagingData, которые мы собираем с помощью collectLatest и передаём адаптеру.
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
private lateinit var adapter: NumbersAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this)
adapter = NumbersAdapter()
recyclerView.adapter = adapter
viewModel = ViewModelProvider(this)[MainViewModel::class.java]
lifecycleScope.launch {
viewModel.pager.flow.collectLatest { pagingData ->
adapter.submitData(pagingData)
}
}
}
}
Запустите приложение. Вы увидите список из двадцати элементов. Прокрутите вниз — он продолжит расти. Библиотека сама вызывает load у PagingSource, когда вы приближаетесь к концу.
Полезные советы
- Не создавайте Pager внутри composable-функции. Используйте
collectAsLazyPagingItems()в Compose или храните Pager во ViewModel — иначе при перерисовке создастся новый объект и данные потеряются. - Room поддерживает Paging из коробки. Если у вас база данных Room, просто верните
PagingSourceиз DAO-метода с типомFlow<PagingSource<Int, Entity>>— всё остальное библиотека сделает сама. - Добавьте обработку ошибок. В PagingSource оборачивайте код в try-catch и возвращайте
LoadResult.Errorс исключением. Тогда в UI можно показать кнопку «Повторить».
Что мы освоили
Этот урок в Android Studio на языке Kotlin показал, как работать с большими списками без ущерба для производительности. Вы создали PagingSource, настроили Pager, написали адаптер и увидели, как библиотека автоматически подгружает новые порции при прокрутке. Теперь вы можете добавить бесконечную ленту в своё приложение — хоть с серверными данными, хоть с локальной базой. Попробуйте заменить NumbersPagingSource на реальные запросы к API через Retrofit — это логичный следующий шаг.