Что такое Paging3 (MVVM, Flow, DataBinding, Hilt)?

Введение

Несомненно, Recyclerview является одним из наиболее используемых компонентов при разработке приложений для android. Мы используем Recylerview для отображения списков данных на одной или нескольких страницах приложения. Когда размер этих наборов данных увеличивается, нам необходимо обратить внимание на эффективное использование системных ресурсов и плавное продвижение производительности пользовательского интерфейса. В этой статье мы узнаем эффективный и правильный способ отображения больших наборов данных в recylerview. Темы, которые мы изучим в содержании статьи, будут следующими:

  • Что такое пагинация и зачем мы ее используем?
  • Что такое Paging3 и какие преимущества он предлагает?
  • Понимание и внедрение Paging3

Что такое пагинация и почему мы ее используем?

Рассмотрим такие приложения, как Instagram, Twitter или Facebook. На страницах основного потока они показывают огромные, даже бесконечные данные в виде списка. Вместо того чтобы загружать эти большие данные одним махом, когда пользователь прокручивает страницу, чтобы увидеть больше контента, отображаемого на экране, он делает это, загружая новые данные в список. Вот ответ на вопрос о том, что такое пагинация, на самом деле это логика. Процесс загрузки набора данных по частям, вместо загрузки всего набора данных сразу, при отображении больших наборов данных в списке.

Так зачем нам нужен такой процесс или что этот метод делает для нас?

  • Эффективное использование пропускной способности сети и системных ресурсов вашего приложения
  • Более быстрое отображение данных на странице
  • Меньшее использование памяти
  • Не расходовать ресурсы на бесполезные данные

Как понять и реализовать Paging3?

Paging3 – это библиотека jetpack, которая позволяет нам легко загружать большие наборы данных из источника данных (локального, удаленного, файлового… и т.д.). Она загружает данные постепенно, сокращая использование сетевых и системных ресурсов. Она написана на языке Kotlin и работает в координации с другими библиотеками Jetpack. Он поддерживает Flow, LiveData и RxJava наряду с Kotlin Coroutine. Она также обеспечивает поддержку многих функций, которые необходимо реализовывать вручную, когда требуется загрузить данные:

  • Следит за ключами, которые будут использоваться для получения следующей и предыдущей страницы.
  • Автоматически запрашивает нужную страницу, если пользователь прокрутил список до конца.
  • Гарантирует, что несколько запросов не будут запущены одновременно.
  • Позволяет кэшировать данные: если вы используете Kotlin, это делается в CoroutineScope; если вы используете Java, это можно сделать с помощью LiveData.
  • Отслеживает состояние загрузки и позволяет отображать его в элементе списка RecyclerView или в другом месте пользовательского интерфейса, а также легко повторять неудачные загрузки.
  • Позволяет выполнять такие распространенные операции, как map или filter над отображаемым списком, независимо от того, используете ли вы Flow, LiveData, RxJava Flowable или Observable.
  • Обеспечивает простой способ реализации разделителей списка.

Понимание и реализация Paging3

Для этого эпизода я создал пример репо. Это репо получает информацию о пользователях из бесплатного API, который создает случайных пользователей. С помощью этого репо мы познакомимся с компонентами Paging3 и научимся его использовать.


1. Добавление зависимостей

Давайте начнем с внедрения необходимых зависимостей. Paging 3 также поддерживает rxjava, guava и jetpack compose.
При желании вы можете добавить свои зависимости по ссылке в соответствии с вашими потребностями.

dependencies {

    .....

    // OkHttp interceptor
    implementation "com.squareup.okhttp3:logging-interceptor:$okHttp_interceptor_version"

    // Retrofit
    implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
    implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"

    //Paging
    implementation "androidx.paging:paging-runtime-ktx:$paging_version"

    //Hilt
    implementation "com.google.dagger:hilt-android:$hilt_version"
    kapt "com.google.dagger:hilt-android-compiler:$hilt_version"

    //Glide
    implementation "com.github.bumptech.glide:glide:$glide_version"
    annotationProcessor "com.github.bumptech.glide:compiler:$glide_version"

    //Coroutines
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_core_version"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_android_version"

    //ktx
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_ktx_version"
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_ktx_version"
    implementation "androidx.activity:activity-ktx:$activity_ktx_version"

    }

Вход в полноэкранный режим Выход из полноэкранного режима

2. Сетевые операции

Начнем с процесса получения информации о пользователе с удаленного сервера.

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    private const val CONNECT_TIMEOUT = 20L
    private const val READ_TIMEOUT = 60L
    private const val WRITE_TIMEOUT = 120L


    @Provides
    @Singleton
    fun provideOkHttpClient(@ApplicationContext context: Context): OkHttpClient {
        val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
            level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY
            else HttpLoggingInterceptor.Level.NONE
        }
        return OkHttpClient.Builder()
            .addInterceptor(httpLoggingInterceptor)
            .addInterceptor(NetworkConnectionInterceptor(context))
            .connectTimeout(CONNECT_TIMEOUT, SECONDS)
            .readTimeout(READ_TIMEOUT, SECONDS)
            .writeTimeout(WRITE_TIMEOUT, SECONDS)
            .build()
    }

    @Provides
    @Singleton
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .baseUrl(BuildConfig.API_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .client(okHttpClient)
            .build()
    }


    @Provides
    @Singleton
    fun provideUserService(retrofit: Retrofit): UserService {
        return retrofit.create(UserService::class.java)
    }

}

Войти в полноэкранный режим Выход из полноэкранного режима

Создадим классы Retrofit и Okhttp как синглтон.
Используя метод инъекции зависимостей в Hilt, мы обеспечим необходимые зависимости классов SOLID способом. Далее определим наш класс сервиса и классы модели.

interface UserService {

    @GET(".")
    suspend fun getUsers(
        @Query("page") page: Int,
        @Query("results") results: Int,
    ): UserResponse

}

Вход в полноэкранный режим Выход из полноэкранного режима

Здесь полезно понять два параметра, требуемых api.
Они определяют номер страницы, запрашиваемой в каждом запросе, и количество элементов, которые должны быть загружены одновременно. Логично, что мы будем загружать новую страницу, когда пользователь дойдет до конца каждого списка, поэтому здесь мы будем извлекать новые данные в соответствии с номером страницы.

data class UserResponse(
    val results: ArrayList<UserModel>
)

data class UserModel(
    val name: NameModel,
    val email: String,
    val phone: String,
    val picture: PictureModel
)

data class NameModel(
    val title: String,
    val first: String,
    val last: String
)

data class PictureModel(
    val thumbnail: String
)

Вход в полноэкранный режим Выход из полноэкранного режима

3.PagingSource

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

class UserPagingDataSource(private val userService: UserService) :
    PagingSource<Int, UserModel>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, UserModel> {
        val page = params.key ?: STARTING_PAGE_INDEX
        return try {
            val response = userService.getUsers(page, params.loadSize)
            LoadResult.Page(
                data = response.results,
                prevKey = if (page == STARTING_PAGE_INDEX) null else page.minus(1),
                nextKey = if (response.results.isEmpty()) null else page.plus(1)
            )
        } catch (exception: Exception) {
            return LoadResult.Error(exception)
        }
    }


    override fun getRefreshKey(state: PagingState<Int, UserModel>): Int? {
        return state.anchorPosition?.let { anchorPosition ->
            state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
                ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
        }
    }

    companion object {
        private const val STARTING_PAGE_INDEX = 1
    }

}

Вход в полноэкранный режим Выход из полноэкранного режима

Класс PagingSource – это компонент, который поставляется с Paging3. Этот класс представляет собой абстрактный общий класс, который отвечает за источник данных, выводимых на страницу, и за то, как получить данные из этого источника. В этой статье мы используем класс PagingSource, потому что мы предоставляем данные только из одного источника (удаленного, локального, файла … и т.д.). Если бы мы работали в структуре, которая основана как на удаленном, так и на локальном источнике (где мы имеем в виду процесс записи данных из удаленного в локальный и прохождение через один источник), нам пришлось бы использовать RemoteMediator.
Поскольку в этой статье мы не будем упоминать концепцию RemoteMediator, при желании вы можете получить необходимую информацию по ссылке.

Примечание: Вы можете использовать RxPagingSource с rxJava или ListenableFuturePagingSource для guava.

Класс PaginSource имеет два типа параметров в качестве общих.

key : **В качестве ключа, наш API сервис основан на индексе, поэтому мы укажем его как Int.
**значение: Вы должны указать его как тип данных, которые вы будете загружать. Для нашего примера мы можем указать его как UserModel.

Функция Load – это основная функция, отвечающая за загрузку данных к нам. Эта функция, как только пользователь достигает конца списка, берет следующий ключ и асинхронно отправляет запрос на новый список, и это делается автоматически библиотекой Paging. Кроме того, эта функция является приостанавливающей и предоставляет нам хорошую структуру для выполнения сетевых запросов в фоновом режиме.

В качестве параметра она принимает класс LoadParams с именем params, который содержит номер страницы, которую нужно загрузить, и количество элементов, которые нужно загрузить. Отсюда мы сможем легко отправить наш запрос с правильной страницей и количеством элементов для загрузки.Во время первой загрузки ключ будет null (если вы не укажете начальное значение). В данном случае мы также указали STARTING_PAGE_INDEX . По умолчанию при первой загрузке Paging3 загружает LOADSIZE*3 элемента. Таким образом, пользователь увидит достаточно элементов при первой загрузке списка, и это не вызовет слишком много сетевых запросов, если пользователь не будет прокручивать страницу.

Нам также необходимо указать LoadResult в качестве возвращаемого типа. Этот класс является закрытым классом, который хранит состояние нашего запроса. Если запрос успешен, мы можем вернуть данные как LoadResult.Page, а для последующих запросов мы можем вернуть объект, указав prevKey и nextKey. Если возникла проблема, мы можем вернуть ошибку с помощью LoadRestlt.Error.

Абстрактный метод getRefreshKey, который нам нужно переопределить. Ключ обновления используется для последующих вызовов обновления PagingSource.load() (первый вызов – это начальная загрузка, которая использует initialKey, предоставленный Pager). Обновление происходит всякий раз, когда библиотека Paging хочет загрузить новые данные для замены текущего списка, например, при пролистывании для обновления или при аннулировании из-за обновления базы данных, изменения конфигурации, смерти процесса и т.д. Как правило, последующие вызовы обновления должны возобновить загрузку данных, сосредоточенных вокруг PagingState.anchorPosition, который представляет собой индекс последнего доступа.

4.Репозиторий

@Singleton
class UserRepositoryImpl @Inject constructor(
    private val userService: UserService
) : UserRepository {
    override fun getUsers(): Flow<PagingData<UserModel>> {
        return Pager(
            config = PagingConfig(
                pageSize = NETWORK_PAGE_SIZE
            ),
            pagingSourceFactory = { UserPagingDataSource(userService) }
        ).flow
    }


    companion object {
        const val NETWORK_PAGE_SIZE = 20
    }
}

Вход в полноэкранный режим Выход из полноэкранного режима

После написания класса PagingSource, который будет получать данные, нам нужен Pager, который будет предоставлять данные в виде потока. Данные, возвращаемые из PagingSource, возвращаются с контейнером PagingData. Мы должны указать классу Pager, из какого источника и как будут получены данные. Он ожидает от нас 3 параметра:

  • config : PagingConfigclass задает параметры, касающиеся загрузки содержимого из PagingSource, например, как далеко вперед загружать, размер запроса для начальной загрузки и другие. Единственный обязательный параметр, который вы должны определить, это размер страницы – сколько элементов должно быть загружено на каждой странице. По умолчанию Paging сохраняет в памяти все загруженные страницы. Чтобы не тратить память при прокрутке страницы пользователем, задайте параметр maxSize в PagingConfig. По умолчанию Paging будет возвращать нулевые элементы в качестве заполнителя для содержимого, которое еще не загружено, если Paging может подсчитать незагруженные элементы и если флаг конфигурации enablePlaceholders равен true.
  • initalKey: Вы можете задать начальный ключ для первого запроса, который будет сделан, когда PagingSource будет начальным. В нашем примере, поскольку мы не указываем начальное значение, оно будет определено как null. Но на стороне PagingSource мы обработаем состояние null и присвоим ему начальное значение.
  • pagingSourceFactory: Функция, определяющая, как создать источник подкачки. В нашем случае мы создадим новый UserPagingDataSource.

Наконец, мы можем сказать .flow, чтобы обеспечить поток PagedData класса Pager. Таким образом, как только пользователь дойдет до конца списка, PagingSource автоматически и асинхронно отправит запрос на следующую страницу, и тогда постраничные данные будут каждый раз представляться нам в виде потока.

Примечание: у нас есть 4 типа для передачи PagingData на другие уровни нашего приложения:

  • Kotlin Flow – используйте Pager.flow.
  • LiveData – используйте Pager.liveData.
  • RxJava Flowable – используйте Pager.flowable.
  • RxJava Observable – используйте Pager.observable.

5.ViewModel

@HiltViewModel
class UserViewModel @Inject constructor(userRepository: UserRepository) : ViewModel() {
    val userItemsUiStates = userRepository.getUsers()
        .map { pagingData ->
            pagingData.map { userModel -> UserItemUiState(userModel) }
        }.cachedIn(viewModelScope)
}
Вход в полноэкранный режим Выход из полноэкранного режима

После успешного создания стороны, которая будет получать данные, перейдем к ui стороне. Сначала запросим данные пользователей из хранилища на стороне viewModel. Возвращаемая структура будет PagingData, и давайте сопоставим ее с необходимым классом модели в каждом элементе, который мы будем использовать в recylerview. Поскольку мы возвращаемся к Flow, библиотека Paging предоставляет нам гибкость и здесь, и мы можем выполнять такие операции, как фильтрация, сопоставление … и т.д.

Оператор cachedIn() делает поток данных разделяемым и кэширует загруженные данные с предоставленным CoroutineScope. При любом изменении конфигурации он будет предоставлять существующие данные вместо того, чтобы получать их с нуля. Это также предотвратит утечку памяти.

6.Адаптер

Одним из компонентов, которые предлагает нам Paging3, является PagingDataAdapter. Этот адаптер представляет собой специальный класс, унаследованный от RecyclerView.Adapter для отображения данных пейджинга на его основе. Он имеет структуру, очень похожую на ListAdapter, и добавляет его в RecyclerView, вычисляя новый входящий список асинхронно на фоновой стороне с помощью DiffUtil.ItemCallback наиболее оптимизированным способом.

class UsersAdapter @Inject constructor() :
    PagingDataAdapter<UserItemUiState, UserViewHolder>(Comparator) {

    override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        getItem(position)?.let { userItemUiState -> holder.bind(userItemUiState) }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {

        val binding = inflate<ItemUserBinding>(
            LayoutInflater.from(parent.context),
            R.layout.item_user,
            parent,
            false
        )

        return UserViewHolder(binding)
    }

    object Comparator : DiffUtil.ItemCallback<UserItemUiState>() {
        override fun areItemsTheSame(oldItem: UserItemUiState, newItem: UserItemUiState): Boolean {
            return oldItem.getPhone() == newItem.getPhone()
        }

        override fun areContentsTheSame(
            oldItem: UserItemUiState,
            newItem: UserItemUiState
        ): Boolean {
            return oldItem == newItem
        }
    }

}

class UserViewHolder(private val binding: ItemUserBinding) : RecyclerView.ViewHolder(binding.root) {
    fun bind(userItemUiState: UserItemUiState) {
        binding.executeWithAction {
            this.userItemUiState = userItemUiState
        }
    }
}


Вход в полноэкранный режим Выход из полноэкранного режима
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="userItemUiState"
            type="com.huawei.pagingexampleproject.ui.UserItemUiState" />
    </data>

    <com.google.android.material.card.MaterialCardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="4dp"
        app:cardCornerRadius="4dp"
        app:cardElevation="4dp">


        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <com.google.android.material.imageview.ShapeableImageView
                android:id="@+id/ivPhoto"
                imageUrl="@{userItemUiState.imageUrl}"
                android:layout_width="60dp"
                android:layout_height="60dp"
                android:layout_gravity="center_horizontal"
                android:layout_marginBottom="8dp"
                android:adjustViewBounds="true"
                android:scaleType="centerCrop"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toStartOf="@+id/glStart"
                app:layout_constraintTop_toTopOf="@+id/glTop"
                app:shapeAppearanceOverlay="@style/circle"
                app:srcCompat="@drawable/ic_launcher_background" />

            <com.google.android.material.textview.MaterialTextView
                android:id="@+id/tvName"
                style="@style/user_card_text_style"
                android:text="@{userItemUiState.name}"
                app:layout_constraintBottom_toTopOf="@+id/tvMail"
                app:layout_constraintEnd_toEndOf="@id/glEnd"
                app:layout_constraintStart_toEndOf="@id/ivPhoto"
                app:layout_constraintTop_toTopOf="@+id/ivPhoto"
                tools:text="Jhon Doe" />

            <com.google.android.material.textview.MaterialTextView
                android:id="@+id/tvMail"
                style="@style/user_card_text_style"
                android:text="@{userItemUiState.mail}"
                app:layout_constraintBottom_toTopOf="@+id/tvPhone"
                app:layout_constraintEnd_toEndOf="@id/glEnd"
                app:layout_constraintStart_toEndOf="@id/ivPhoto"
                app:layout_constraintTop_toBottomOf="@+id/tvName"
                tools:text="jon.doe@gmail.com" />

            <com.google.android.material.textview.MaterialTextView
                android:id="@+id/tvPhone"
                style="@style/user_card_text_style"
                android:text="@{userItemUiState.phone}"
                app:layout_constraintBottom_toBottomOf="@+id/ivPhoto"
                app:layout_constraintEnd_toEndOf="@id/glEnd"
                app:layout_constraintStart_toEndOf="@id/ivPhoto"
                app:layout_constraintTop_toBottomOf="@+id/tvMail"
                tools:text="0532 123 12 12" />


            <androidx.constraintlayout.widget.Guideline
                android:id="@+id/glStart"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                app:layout_constraintGuide_begin="8dp" />


            <androidx.constraintlayout.widget.Guideline
                android:id="@+id/glEnd"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                app:layout_constraintGuide_end="8dp" />

            <androidx.constraintlayout.widget.Guideline
                android:id="@+id/glTop"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:orientation="horizontal"
                app:layout_constraintGuide_begin="8dp" />

        </androidx.constraintlayout.widget.ConstraintLayout>

    </com.google.android.material.card.MaterialCardView>
</layout>
Вход в полноэкранный режим Выход из полноэкранного режима
data class UserItemUiState(private val userModel: UserModel) : BaseUiState() {

    fun getImageUrl() = userModel.picture.thumbnail

    fun getName() = "${userModel.name.first} ${userModel.name.last}"

    fun getPhone() = userModel.phone

    fun getMail() = userModel.email

}
Вход в полноэкранный режим Выход из полноэкранного режима
fun <T : ViewDataBinding> T.executeWithAction(action: T.() -> Unit) {
    action()
    executePendingBindings()
}

Войти в полноэкранный режим Выход из полноэкранного режима

Если мы перейдем к части Adapter, мы отобразили наши данные UserModel, поступающие на сторону viewmodel как UserItemUiState для каждого элемента в recylerview. Поэтому мы указываем тип данных PagingData, которые будут поступать в адаптер, как UserItemUiState. Объект Comprator для изменений тоже. Функция executeWithAction на стороне ViewHolder также является функцией расширения, определенной для того, чтобы не писать executePendingBindings каждый раз. По сути, это присвоение переменных для dataBinding.

7. Рендеринг данных

После подготовки адаптера пришло время отправить входящие данные в адаптер и завершить рендеринг пользовательского интерфейса.

@AndroidEntryPoint
class UserActivity : AppCompatActivity() {
    private lateinit var binding: ActivityUserBinding
    private val viewModel: UserViewModel by viewModels()

    @Inject
    lateinit var userAdapter: UsersAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setBinding()
        setListener()
        setAdapter()
        collectLast(viewModel.userItemsUiStates, ::setUsers)
    }

    private fun setBinding() {
        binding = DataBindingUtil.setContentView(this, R.layout.activity_user)
    }

    private fun setListener() {
        binding.btnRetry.setOnClickListener { userAdapter.retry() }
    }


    private fun setAdapter() {
        collect(flow = userAdapter.loadStateFlow
            .distinctUntilChangedBy { it.source.refresh }
            .map { it.refresh },
            action = ::setUsersUiState
        )
        binding.rvUsers.adapter = userAdapter.withLoadStateFooter(FooterAdapter(userAdapter::retry))
    }

    private fun setUsersUiState(loadState: LoadState) {
        binding.executeWithAction {
            usersUiState = UsersUiState(loadState)
        }
    }

    private suspend fun setUsers(userItemsPagingData: PagingData<UserItemUiState>) {
        userAdapter.submitData(userItemsPagingData)
    }

}

Вход в полноэкранный режим Выход из полноэкранного режима

fun <T> LifecycleOwner.collectLast(flow: Flow<T>, action: suspend (T) -> Unit) {
    lifecycleScope.launch {
        repeatOnLifecycle(Lifecycle.State.STARTED) {
            flow.collectLatest(action)
        }
    }
}


fun <T> LifecycleOwner.collect(flow: Flow<T>, action: suspend (T) -> Unit) {
    lifecycleScope.launch {
        repeatOnLifecycle(Lifecycle.State.STARTED) {
            flow.collect {
                action.invoke(it)
            }
        }
    }
}

Вход в полноэкранный режим Выход из полноэкранного режима

Прежде всего, нам нужно собрать поток, который будет поступать из вью-модели. Мы определили расширения LifecycleOwner, чтобы быть осведомленными о жизненном цикле и многократно используемыми. Вы можете получить информацию о концепциях для потока по этой ссылке и для repeatOnLifecyle по этой ссылке.

Чтобы обрабатывать состояние загрузки данных на стороне пользовательского интерфейса, библиотека Paging отслеживает состояние запросов на загрузку данных в пейдже и раскрывает его через класс LoadState. Ваше приложение может зарегистрировать слушателя в PagingDataAdapter для получения информации о текущем состоянии и соответствующего обновления пользовательского интерфейса. Эти состояния предоставляются из адаптера, поскольку они синхронны с пользовательским интерфейсом. Это означает, что ваш слушатель получает обновления, когда загрузка страницы была применена к пользовательскому интерфейсу.

LoadState может находиться в 3 состояниях:

  • LoadState.NotLoading: Если нет активной операции загрузки и нет ошибок.
  • LoadState.Loading: Если есть активная операция загрузки.
  • LoadState.Error: Если возникла ошибка.

Для получения информации о состоянии загрузки можно использовать loadStateFlow или addLoadStateListener(), предоставляемые через PagingDataAdapter. Эти механизмы предоставляют доступ к объекту CombinedLoadStates, который содержит информацию о поведении LoadState для каждого типа загрузки.

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="usersUiState"
            type="com.huawei.pagingexampleproject.ui.UsersUiState" />

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ui.UserActivity">

        <com.google.android.material.appbar.MaterialToolbar
            android:id="@+id/topAppbar"
            style="@style/Widget.MaterialComponents.Toolbar.Primary"
            android:layout_width="0dp"
            android:layout_height="?attr/actionBarSize"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:title="@string/user_list" />

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rvUsers"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:visibility="@{usersUiState.listVisibility}"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/topAppbar"
            tools:listitem="@layout/item_user" />

        <ProgressBar
            android:id="@+id/progressBar"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:visibility="@{usersUiState.progressBarVisibility}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/topAppbar" />


        <Button
            android:id="@+id/btnRetry"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/retry"
            android:visibility="@{usersUiState.errorVisibility}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/topAppbar" />

        <TextView
            android:id="@+id/tvError"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="@{usersUiState.getErrorMessage(context)}"
            android:visibility="@{usersUiState.errorVisibility}"
            app:layout_constraintEnd_toEndOf="@+id/btnRetry"
            app:layout_constraintStart_toStartOf="@+id/btnRetry"
            app:layout_constraintTop_toBottomOf="@+id/btnRetry"
            tools:text="Internet Connection Failed" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Вход в полноэкранный режим Выход из полноэкранного режима
data class UsersUiState(
    private val loadState: LoadState
) : BaseUiState() {

    fun getProgressBarVisibility() = getViewVisibility(isVisible = loadState is LoadState.Loading)

    fun getListVisibility() = getViewVisibility(isVisible = loadState is LoadState.NotLoading)

    fun getErrorVisibility() = getViewVisibility(isVisible = loadState is LoadState.Error)

    fun getErrorMessage(context: Context) = if (loadState is LoadState.Error) {
        loadState.error.localizedMessage ?: context.getString(R.string.something_went_wrong)
    } else ""
}

Войти в полноэкранный режим Выход из полноэкранного режима

Давайте обработаем ситуации загрузки, успеха или ошибки, наблюдая за этими изменениями в ui части. Во-первых, мы можем использовать параметр source, который дает нам CombinedState, для прослушивания состояния источника пейджинга. Таким образом, мы можем обработать состояние загрузки, когда будет брошен первый запрос. Установим состояние, которое мы прослушиваем, через PagingDataAdapter с привязкой к базе данных над модельным классом UsersUiState.

Другой компонент адаптера, предлагаемый библиотекой paging, – LoadStateAdapter. Этот адаптер предоставляет доступ к состоянию загрузки текущего списка. Когда пользователь достигает конца списка с помощью пользовательского ViewHolder, давайте предпримем действия в соответствии со статусом загрузки. Здесь мы можем добавить его в качестве заголовка или нижнего колонтитула. Мы можем добавить оба варианта вместе. Подробную информацию вы можете найти по ссылке.

class FooterAdapter(
    private val retry: () -> Unit
) : LoadStateAdapter<FooterViewHolder>() {
    override fun onBindViewHolder(holder: FooterViewHolder, loadState: LoadState) {
        holder.bind(loadState)
    }

    override fun onCreateViewHolder(
        parent: ViewGroup,
        loadState: LoadState
    ): FooterViewHolder {
        val itemPagingFooterBinding = inflate<ItemPagingFooterBinding>(
            LayoutInflater.from(parent.context),
            R.layout.item_paging_footer,
            parent,
            false
        )
        return FooterViewHolder(itemPagingFooterBinding, retry)
    }

}


class FooterViewHolder(
    private val binding: ItemPagingFooterBinding,
    retry: () -> Unit
) : RecyclerView.ViewHolder(binding.root) {

    init {
        binding.btnRetry.setOnClickListener { retry.invoke() }
    }

    fun bind(loadState: LoadState) {
        binding.executeWithAction {
            footerUiState = FooterUiState(loadState)
        }
    }
}

Вход в полноэкранный режим Выход из полноэкранного режима
data class FooterUiState(private val loadState: LoadState) : BaseUiState() {

    fun getLoadingVisibility() = getViewVisibility(isVisible = loadState is LoadState.Loading)

    fun getErrorVisibility() = getViewVisibility(isVisible = loadState is LoadState.Error)

    fun getErrorMessage(context: Context) = if (loadState is LoadState.Error) {
        loadState.error.localizedMessage ?: context.getString(R.string.something_went_wrong)
    } else ""
}

Войти в полноэкранный режим Выход из полноэкранного режима
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="footerUiState"
            type="com.huawei.pagingexampleproject.common.FooterUiState" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="10dp">

        <ProgressBar
            android:id="@+id/progressBar"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:visibility="@{footerUiState.loadingVisibility}" />

        <Button
            android:id="@+id/btnRetry"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="@string/retry"
            android:visibility="@{footerUiState.errorVisibility}" />

        <TextView
            android:id="@+id/tvError"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginTop="4dp"
            android:text="@{footerUiState.getErrorMessage(context)}"
            android:visibility="@{footerUiState.errorVisibility}"
            tools:text="Internet Connection Failed" />

    </LinearLayout>
</layout>
Войти в полноэкранный режим Выход из полноэкранного режима

После подготовки этой структуры мы можем установить наш адаптер recylerview с помощью функции withLoadStateFooter(). Фактически, эта структура возвращает нам ConcatAdapter. Он уведомляет LoadStateAdapter о статусе добавления в список через PagingDataAdapter, и мы можем показать статус загрузки новых добавленных постраничных данных, когда пользователь достигнет конца списка, пользовательским способом через FooterAdapter, который мы создали. Кроме того, с помощью структуры ConcatAdapter мы показываем мультивидовое представление на повторном представлении.

Функция withLoadStateFooter ожидает от нас параметр типа LoadStateAdapter. Здесь мы передаем FooterAdapter, который мы создали сами.FooterAdapter также ожидает функцию retry в структуре. Эта структура также предоставляется через pagingDataAdapter. Если при загрузке произойдет ошибка, когда будет достигнут конец страницы, появится кнопка retry с сообщением об ошибке. Достаточно вызвать функцию retry над созданным нами pagingDataAdapater, чтобы отправить запрос на повторную загрузку страницы. Это опять же предоставлено нам библиотекой paging3.


Заключение

В этой статье я попытался объяснить метод пагинации, используемый при работе с большими наборами данных, и Paging3, который предлагается разработчикам android для простой реализации этого метода. Благодаря своим преимуществам, Paging3 предоставляет нам большое удобство и позволяет показывать большие наборы данных пользователю наиболее оптимизированным способом. Компоненты библиотеки Paging разработаны таким образом, чтобы вписываться в рекомендуемую архитектуру приложений Android, чисто интегрироваться с другими компонентами Jetpack и обеспечивать первоклассную поддержку Kotlin. Увидимся в следующей статье. 👋👋


Ознакомьтесь с репо, которое мы написали:

https://github.com/oguz-sahin/PagingExampleProject


https://developer.android.com/topic/libraries/architecture/paging/v3-overview

https://developer.android.com/codelabs/android-paging#

Оцените статью
Procodings.ru
Добавить комментарий