---
title: "Android Paging 3 та Paging in AdapterDelegates"
url: "https://romankryvolapov.com/uk/android-paging-3/"
description: "Пагінація в Android на Paging 3: PagingSource, PagingDataAdapter, адаптер станів завантаження та підключення до AdapterDelegates."
language: uk
updated: 2026-01-21
---
Оскільки статей про бібліотеку Android Paging і особливо про пагінацію в бібліотеці AdapterDelegates мені здалося досить мало- пишу свою.

Бібліотека Paging дозволяє організувати пагінацію даних у списках в обидва боки, а також вміє показувати loader внизу списку та деякі інші штуки.

Гугл рекомендує використовувати AsyncPagingDataDiffer, але я наведу приклад із PagingDataAdapter, який являє собою wrapper над AsyncPagingDataDiffer та імплементує RecyclerView.Adapter.

Не робитиму RemoteMediator- у більшості завдань він не потрібен, можливо потім доповню статтю і ним

У коді будуть коментарі, що і для чого потрібно. Якщо знайдете помилку- напишіть будь ласка на телеграм RomanKryvolapov. Пишу все в блокноті, оскільки лінь створювати проєкт

## Domain та Network моделі

Припустимо, у нас є список із категоріями користувачів та користувачами

```kotlin
// інтерфейс для зручності, або Any
interface UserOrCategory

// моделі для domain шару застосунку
data class UserData(
    val id: String,
    val title: String,
    val description: String,
    val avatar: String,
) : UserOrCategory

data class CategoryData(
    val id: String,
    val title: String,
    val error: String
) : UserOrCategory
```

мережеві моделі

```kotlin
// інтерфейс для перетворення мережевої моделі в модель для застосунку
interface DomainMapper<T : Any> {
    fun mapToDomainModel(): T
}

// мережеві моделі для data шару застосунку
// припустимо, що в разі невдачі api видає текстову помилку error
data class UsersAndCategoriesNetworkModel(
    private val list: List<Any>,
    private val error: String?,
) : DomainMapper<List<UserOrCategory>> {
// У підсумку ми маємо отримати list, у якому знаходяться користувачі та категорії List<UserOrCategory>
    override fun mapToDomainModel() = list.map { userOrCategory ->
        if (userOrCategory.type == "user") {
            (userOrCategory as UserNetworkModel).mapToDomainModel()
        } else if (userOrCategory.type == "category") {
            (userOrCategory as CategoryNetworkModel).mapToDomainModel()
        }
    } ?: emptyList()
}

data class UserNetworkModel(
    private val id: String?,
    private val type: String?,
    private val title: String?,
    private val description: String?,
    private val avatar: String?,
) : DomainMapper<UserData> {
    override fun mapToDomainModel() = UserData(
        id = id ?: "",
        title = title ?: "Unknown",
        description = description ?: "No description",
        avatar = avatar ?: "",
    )
}

data class CategoryNetworkModel(
    private val id: String?,
    private val type: String?,
    private val title: String?,
) : DomainMapper<CategoryData> {
    override fun mapToDomainModel() = CategoryData(
        id = id ?: "",
        title = title ?: "Unknown",
    )
}
```

## PagingSource у data шарі

далі нам необхідно в data шарі зробити PagingSource

```kotlin
// getUsersAndCategories в networkApi - suspend функція, яка повертає UsersAndCategoriesNetworkModel
// наприклад
@FormUrlEncoded
@POST("get_users")
suspend fun getUsersAndCategories(
    @Field("page_number") pageNumber: Int,
    @Field("page_size") pageSize: Int
): Response<UsersAndCategoriesNetworkModel>

// або записуємо поля в мапу і передаємо як body
@POST("get_users")
suspend fun getUsersAndCategories(
    @Body body: Map<String, String>
): Response<UsersAndCategoriesNetworkModel>

// джерело даних для data шару застосунку, за ідеєю виклик getUsersAndCategories
// можна було б зробити через репозиторій, але тут зробив напряму. Також джерелом даних теоретично може бути не лише NetworkApi, а будь-що
class UserAndCategoryDataSource(
    private val pageSize: Int,
    private val initialPageNumber: Int,
    private val networkApi: NetworkApi,
) : PagingSource<Int, TransactionHistory>() {
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, UserOrCategory> {
		// номер сторінки знаходиться в key, якщо він null, то в клас ми також передаємо початковий номер сторінки
     	// initialPageNumber для api, оскільки він може бути наприклад 0 або 1
        val pageNumber: Int = params.key ?: initialPageNumber
        return try {
            val response = networkApi.getUsersAndCategories(pageNumber, pageSize)
			// якщо текст не порожній, то прийшла помилка
            if (!response?.body()?.error.isNullOrEmpty()) {
                LoadResult.Error(throwable = java.lang.Exception())
            } else {
                val userAndCategoryList = response.body()?.mapToDomainModel()
				// що буде в pageNumber для наступної сторінки
                val nextPageNumber: Int? = if (!userAndCategoryList.isNullOrEmpty()) pageNumber + 1 else null
				// і для попередньої
                val prevPageNumber: Int? = if (pageNumber > initialPageNumber) pageNumber - 1 else null
				// а тут дані для пейджера
                LoadResult.Page(
                    data = userAndCategoryList.orEmpty(),
                    prevKey = prevPageNumber,
                    nextKey = nextPageNumber
                )
            }
		// тут краще б вказати мережеву помилку, а не Exception
        } catch (e: Exception) {
            LoadResult.Error(throwable = e)
        }
    }

    override fun getRefreshKey(state: PagingState<Int, UserOrCategory>): Int? {
        val anchorPosition = state.anchorPosition ?: return null
        val page = state.closestPageToPosition(anchorPosition) ?: return null
        return page.prevKey?.plus(1) ?: page.nextKey?.minus(1)
    }
```

## ViewModel

далі переходимо до ViewModel

```kotlin
// networkApi будемо інжектити
class UserAndCategoryViewModel(private val networkApi: NetworkApi) : ViewModel() {
    fun getUsersAndCategories(
        pageSize: Int,
        prefetchDistance: Int,
        initialLoadSize: Int,
        initialPageNumber: Int,
    ): Flow<PagingData<UserOrCategory>> = Pager(
        config = PagingConfig(
            pageSize = pageSize,
            enablePlaceholders = false,
            prefetchDistance = prefetchDistance,
            initialLoadSize = initialLoadSize,
        )
	// сюди також закидається RemoteMediator через "remoteMediator =", якщо він використовується
    ) {
        UserAndCategoryDataSource(
            pageSize = pageSize,
            initialPageNumber = initialPageNumber,
            networkApi = networkApi,
        )
    }.flow
        .flowOn(Dispatchers.IO)
        .stateIn(viewModelScope, SharingStarted.Lazily, PagingData.empty())
        .cachedIn(viewModelScope)
}
```

## Fragment

Далі фрагмент

```kotlin
// спочатку невеликий екстеншн для flow
fun <T> Flow<T>.launchWhenStarted(scope: LifecycleCoroutineScope) {
    scope.launchWhenStarted { this@launchWhenStarted.collect() }
}

// далі фрагмент
class UsersFragment : BaseFragment() {
    companion object {
		// далі всі константи для пейджера
        private const val PAGE_SIZE = 10
        private const val PREFETCH_DISTANCE = 3
        private const val INITIAL_LOAD_SIZE = 10
        private const val INITIAL_STATEMENTS_PAGE_NUMBER = 1
        fun newInstance() = UsersFragment()
    }

    // у межах цього прикладу loadStateJob не потрібна, але може знадобитися її використати, щоб скасовувати таску
    private var loadStateJob: Job? = null
    private val binding by viewBinding(FragmentUsersBinding::bind)
    private val viewModel: UserAndCategoryViewModel by sharedViewModel()

    // тут ініціалізуємо перший адаптер UserAndCategoryAdapter, який показуватиме список
    private val userAndCategoryAdapter = UserAndCategoryAdapter(
        onUserClicked = {
		// логіка  для кліка по користувачу, можна було б використати listiner замість цього або будь-який інший спосіб
        }
    )

    override fun getLayout() = R.layout.fragment_users
    override fun viewReady() {
        binding.run {
			// за допомогою withLoadStateHeaderAndFooter можна задати другий адаптер StatementsLoaderAdapter, який показує loading і error
            recyclerViewUsersAndCategories.adapter =
                userAndCategoryAdapter.withLoadStateHeaderAndFooter(
                    header = StatementsLoaderAdapter(),
                    footer = StatementsLoaderAdapter()
                )
            subscribeToLoadState()
            subscribeToDataSource()
        }
    }

    private fun subscribeToLoadState() {
		// замість loadStateFlow можна використовувати addLoadStateListener
        userAndCategoryAdapter.loadStateFlow.onEach { state ->
            loadStateJob?.cancel()
            loadStateJob = lifecycleScope.launchWhenStarted {
                val isLoading = state.refresh == LoadState.Loading
                val isEmpty = statementsAdapter.itemCount < 1
                val isError = state.refresh is LoadState.Error
				// і далі будь-яка логіка для станів, раджу залогувати стани, щоб подивитися, як вони працюють
				// взагалі є такі CombinedLoadStates: refresh, prepend, append, source, mediator, і стани LoadState: Loading, NotLoading, Error
            }
        }
    }.launchWhenStarted(lifecycleScope)

    private fun subscribeToDataSource() {
		// передаємо всі константи
        viewModel.getAccountStatementsDataSource(
            pageSize = PAGE_SIZE,
            prefetchDistance = PREFETCH_DISTANCE,
            initialLoadSize = INITIAL_LOAD_SIZE,
            initialPageNumber = INITIAL_STATEMENTS_PAGE_NUMBER,
        ).onEach { data ->
			// закидаємо дату в адаптер
            userAndCategoryAdapter.submitData(data)
        }.launchWhenStarted(lifecycleScope)
    }
}
```

## Адаптер для показу списку

далі адаптер для показу списку

```kotlin
class UserAndCategoryAdapter(private val onUserClicked: (String) -> Unit) :
    PagingDataAdapter<UserOrCategory, RecyclerView.ViewHolder>(DiffCallback) {
    companion object {
        private enum class Type(val value: Int) {
            USER(0),
            CATEGORY(1),
        }
    }

    // тут використовуватиму viewBinding
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) {
        Type.USER.value -> UserHolder(
            LayoutUserBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        )
        else -> CategoryHolder(
            LayoutCategoryBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        )
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (holder) {
            is CategoryHolder -> holder.bind(
                categoryData = getItem(position) as CategoryData
            )
            is UserHolder -> holder.bind(
                userData = getItem(position) as UserData
                        onUserClicked = { onUserClicked(it) }
            )
        }
    }

    override fun getItemViewType(position: Int) = when (getItem(position)) {
        is UserData -> Type.USER.value
        else -> Type.CATEGORY.value
    }

    class CategoryHolder(private val binding: LayoutCategoryBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(categoryData: CategoryData) {
            binding.category.setSimpleText(categoryData.title)
        }
    }

    class UserHolder(private val binding: LayoutUserBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(userData: UserData, onUserClicked: (String) -> Unit) {
            binding.run {
                title.setSimpleText(userData.title)
                description.setSimpleText(userData.description)
                avatar.load(userData.avatar)
            }
        }
    }

    private object DiffCallback : DiffUtil.ItemCallback<UserOrCategory>() {

        // тут якась логіка для порівняння елементів
        override fun areItemsTheSame(oldItem: UserOrCategory, newItem: UserOrCategory): Boolean {
            if (oldItem is UserData && newItem is UserData && oldItem.id == newItem.id) {
                return true
            } else if (oldItem is CategoryData && newItem is CategoryData && oldItem.id == newItem.id) {
                return true
            }
            return false
        }

        // тут- якась логіка для порівняння, оновлено елемент чи ні
        override fun areContentsTheSame(oldItem: UserOrCategory, newItem: UserOrCategory): Boolean {
            if (oldItem is UserData && newItem is UserData && oldItem.title == newItem.title) {
                return true
            } else if (oldItem is CategoryData && newItem is CategoryData && oldItem.category == newItem.category) {
                return true
            }
            return false
        }
    }
}
```

## Адаптер для показу станів

і далі адаптер для показу станів

```kotlin
// тут заради різноманітності не використовував ViewBinding
class StatementsLoaderAdapter : LoadStateAdapter<RecyclerView.ViewHolder>() {
    companion object {
        private enum class State(val value: Int) {
            ERROR(1),
            PROGRESS(0),
        }
    }

    class ProgressViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        fun bind(loadState: LoadState) { }
    }

    class ErrorViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        private val errorMessage = view.findViewById<TextView>(R.id.errorMessage)
        fun bind(loadState: LoadState) {
            if (loadState is LoadState.Error) {
                errorMessage.setSimpleText(loadState.error.localizedMessage)
            }
        }
    }

    override fun getStateViewType(loadState: LoadState) = when (loadState) {
        LoadState.Loading -> State.PROGRESS.value
        is LoadState.NotLoading -> error("Not supported")
        is LoadState.Error -> State.ERROR.value
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, loadState: LoadState) {
        when (holder) {
            is ProgressViewHolder -> holder.bind(loadState)
            is ErrorViewHolder -> holder.bind(loadState)
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState) = when (loadState) {
        LoadState.Loading -> ProgressViewHolder(
            LayoutInflater.from(parent.context).inflate(R.layout.layout_paging_progress, parent, false)
        )
        is LoadState.Error -> ErrorViewHolder(
            LayoutInflater.from(parent.context).inflate(R.layout.item_paging_error, parent, false)
        )
        is LoadState.NotLoading -> error("Not supported")
    }
}
```

## Пагінація в бібліотеці AdapterDelegates

**Тепер що стосується пагінації в популярній бібліотеці AdapterDelegates від Hannes Dorfmann\
(цю частину статті дописав через 2 роки після попередньої частини і трохи змінив підхід)**

[https://hannesdorfmann.com/android/adapter-delegates](https://hannesdorfmann.com/android/adapter-delegates)

[https://github.com/sockeqwe/AdapterDelegates](https://github.com/sockeqwe/AdapterDelegates)

Робимо DataSource

```kotlin
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.onEach
import androidx.paging.PositionalDataSource


class UserAndCategoryDataSource(
    private val showLoader: (Unit) -> Unit,
    private val hideLoader: (Unit) -> Unit,
    private val viewModelScope: CoroutineScope,
    private val showErrorState: (String?) -> Unit,
    private val userAndCategoryUiMapper: UserAndCategoryUiMapper,
    private val getUserAndCategoryUseCase: GetUserAndCategoryUseCase,
) : PositionalDataSource<UserAndCategoryUi>() {

    companion object {
        private const val TAG = "UserAndCategoryDataSourceTag"
        private const val CURSOR_SIZE = 20
    }

    private var cursor: String? = null

    override fun loadInitial(
        params: LoadInitialParams,
        callback: LoadInitialCallback<ApplicationUi>
    ) {
        logDebug("loadInitial", TAG)
        showLoader.invoke(Unit)
        number = -1
        loadNext {
            logDebug("loadInitial onResult size: ${it.size}", TAG)
            callback.onResult(
                it,
                0,
                it.size
            )
        }
    }

    override fun loadRange(
        params: LoadRangeParams,
        callback: LoadRangeCallback<ApplicationUi>
    ) {
        logDebug("loadRange", TAG)
        loadNext {
            logDebug("loadRange onResult size: ${it.size}", TAG)
            callback.onResult(it)
        }
    }

    private fun loadNext(callback: (List<UserAndCategoryUi>) -> Unit) {
        getUserAndCategoryUseCase.invoke(
            cursor = cursor,
            size = CURSOR_SIZE,
        ).onEach { result ->
            result.onLoading {
                logDebug("loadNext onLoading", TAG)
            }.onSuccess { model, _, _ ->
                logDebug("loadNext onSuccess", TAG)
                if (model.content == null || model.cursor == null) {
                    showErrorState.invoke("Data from server is empty")
                    hideLoader.invoke(Unit)
                    return@onEach
                }
                if (!model.content.isNullOrEmpty()) {
                    callback(applicationsUiMapper.mapList(model.content!!))
                    cursor = model.cursor!!
                }
                delay(HIDE_LOADER_LONG_DELAY)
                hideLoader.invoke(Unit)
            }.onFailure { _, message, _, _ ->
                logError("loadNext onFailure", message, TAG)
                showErrorState.invoke(message)
                hideLoader.invoke(Unit)
            }
        }.launchInScope(viewModelScope)
    }

}
```

Adapter, тут приклад використання адаптера лише з одним типом елемента:

```kotlin
import com.hannesdorfmann.adapterdelegates4.paging.PagedListDelegationAdapter
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject


class UserAndCategoryMeAdapter :
    PagedListDelegationAdapter<UserAndCategoryUi>(DefaultDiffUtilCallback()),
    KoinComponent {

    private val userAndCategoryDelegate: UserAndCategoryDelegate by inject()

    var clickListener: ClickListener? = null
        set(value) {
            field = value
            userAndCategoryDelegate.openClickListener= { model ->
                clickListener?.onOpenClicked(
                    model = model,
                )
            }
        }

    init {
        delegatesManager.apply {
            addDelegate(userAndCategoryDelegate)
        }
    }

    interface ClickListener {
        fun onOpenClicked(model: UserAndCategoryUi)
    }

}
```

Delegate:

```kotlin
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.hannesdorfmann.adapterdelegates4.AbsFallbackAdapterDelegate


class UserAndCategoryDelegate : AbsFallbackAdapterDelegate<MutableList<UserAndCategoryUi>>() {

    var openClickListener: ((model: UserAndCategoryUi) -> Unit)? = null

    override fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder {
        return ViewHolder(parent.inflateBinding(ListItemUserAndCategoryBinding::inflate))
    }

    override fun onBindViewHolder(
        items: MutableList<UserAndCategoryUi>,
        position: Int,
        holder: RecyclerView.ViewHolder,
        payloads: MutableList<Any>
    ) {
        (holder as ViewHolder).bind(items[position])
    }

    private inner class ViewHolder(
        private val binding: ListItemUserAndCategoryBinding
    ) : RecyclerView.ViewHolder(binding.root) {

        fun bind(model: UserAndCategoryUi) {
            binding.tvName.text = model.name
            binding.rootLayout.onClickThrottle {
                openClickListener?.invoke(model)
            }
        }
    }
}
```

додаємо до ViewModel

```kotlin
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import androidx.paging.DataSource
import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList
import org.koin.core.component.inject


class UserAndCategoryViewModel : BaseViewModel() {

    companion object {
        private const val TAG = "UserAndCategoryViewModelTag"
        const val CURSOR_SIZE = 20
    }

    private val userAndCategoryUiMapper: UserAndCategoryUiMapper by inject()
    private val getUserAndCategoryUseCase: GetUserAndCategoryUseCase by inject()

    private val dataSourceFactory: DataSource.Factory<Int, UserAndCategoryUi>

    val adapterListLiveData: LiveData<PagedList<UserAndCategoryUi>>

    // цей шматок скопійовано з іншого класу для прикладу
    private var sortingModel: SortingModel by Delegates.observable(
        SortingModel(
            sortBy = SortingByEnum.DEFAULT,
            sortDirection = SortingDirectionEnum.ASC
        )
    ) { _, _, _ ->
        updateSortingModel()
        dataSourceFactory.create()
        adapterListLiveData.value?.dataSource?.invalidate()
    }

    init {
        dataSourceFactory = object : DataSource.Factory<Int, UserAndCategoryUi>() {
            override fun create(): DataSource<Int, UserAndCategoryUi> {
                return UserAndCategoryDataSource(
                    viewModelScope = viewModelScope,
                    userAndCategoryUiMapper = userAndCategoryUiMapper,
                    getUserAndCategoryUseCase = getUserAndCategoryUseCase,
                    showLoader = {
                        hideErrorState()
                        showLoader()
                    },
                    hideLoader = {
                        hideLoader()
                    },
                    showErrorState = { showErrorState(descriptionString = it) },
                )
            }
        }
        val config = PagedList.Config.Builder()
            .setEnablePlaceholders(false)
            .setPageSize(CURSOR_SIZE)
            .build()
        adapterListLiveData = LivePagedListBuilder(dataSourceFactory, config)
            .setBoundaryCallback(object : PagedList.BoundaryCallback<UserAndCategoryUi>() {
                override fun onZeroItemsLoaded() {
                    super.onZeroItemsLoaded()
                    logDebug("boundaryCallback onZeroItemsLoaded", TAG)
                    hideLoader()
                }

                override fun onItemAtEndLoaded(itemAtEnd: UserAndCategoryUi) {
                    super.onItemAtEndLoaded(itemAtEnd)
                    logDebug("boundaryCallback onItemAtEndLoaded", TAG)
                    hideLoader()
                }

                override fun onItemAtFrontLoaded(itemAtFront: UserAndCategoryUi) {
                    super.onItemAtFrontLoaded(itemAtFront)
                    logDebug("boundaryCallback onItemAtFrontLoaded", TAG)
                    hideLoader()
                }
            })
            .build()
    }

    fun refreshScreen() {
        logDebug("refreshScreen", TAG)
        adapterListLiveData.value?.dataSource?.invalidate()
    }

    override fun updateSortingModel() {
        logDebug("updateSortingModel", TAG)
        _currentSortingCriteriaData.value = getSortingTitleByModel(
            sortingModelUiMapper.map(sortingModel)
        )
    }

}
```

Наведу і клас для даних, який використовується, але ви можете застосувати будь-який підхід, який вам подобається, це просто один із прикладів

```kotlin
enum class ErrorType {
    EXCEPTION,
    SERVER_ERROR,
    ERROR_IN_LOGIC,
    SERVER_DATA_ERROR,
    NO_INTERNET_CONNECTION,
}

data class ResultEmittedData<out T>(
    val model: T?,
    val error: Any?,
    val status: Status,
    val message: String?,
    val responseCode: Int?,
    val errorType: ErrorType?,
) {

    enum class Status {
        SUCCESS,
        ERROR,
        LOADING,
    }

    companion object {
        fun <T> success(
            model: T,
            message: String?,
            responseCode: Int?,
        ): ResultEmittedData<T> =
            ResultEmittedData(
                error = null,
                model = model,
                errorType = null,
                message = message,
                status = Status.SUCCESS,
                responseCode = responseCode,
            )

        fun <T> loading(
            model: T?,
            message: String? = null,
        ): ResultEmittedData<T> =
            ResultEmittedData(
                model = model,
                error = null,
                errorType = null,
                message = message,
                responseCode = null,
                status = Status.LOADING,
            )

        fun <T> error(
            model: T?,
            error: Any?,
            message: String?,
            responseCode: Int?,
            errorType: ErrorType?,
        ): ResultEmittedData<T> =
            ResultEmittedData(
                model = model,
                error = error,
                message = message,
                errorType = errorType,
                status = Status.ERROR,
                responseCode = responseCode,
            )
    }
}

inline fun <T : Any> ResultEmittedData<T>.onLoading(
    action: (
        message: String?,
    ) -> Unit
): ResultEmittedData<T> {
    if (status == ResultEmittedData.Status.LOADING) action(
        message
    )
    return this
}

inline fun <T : Any> ResultEmittedData<T>.onSuccess(
    action: (
        model: T,
        message: String?,
        responseCode: Int?,
    ) -> Unit
): ResultEmittedData<T> {
    if (status == ResultEmittedData.Status.SUCCESS && model != null) action(
        model,
        message,
        responseCode,
    )
    return this
}

inline fun <T : Any> ResultEmittedData<T>.onFailure(
    action: (
        model: Any?,
        message: String?,
        responseCode: Int?,
        errorType: ErrorType?,
    ) -> Unit
): ResultEmittedData<T> {
    if (status == ResultEmittedData.Status.ERROR) action(
        model,
        message,
        responseCode,
        errorType
    )
    return this
}
```

```kotlin
import com.google.gson.JsonSyntaxException
import kotlinx.coroutines.withContext
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.RequestBody
import org.json.JSONObject
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.qualifier.named
import java.net.UnknownHostException
import javax.net.ssl.SSLPeerUnverifiedException


abstract class BaseRepository : KoinComponent {

    companion object {
        private const val TAG = "BaseRepositoryTag"
    }

    protected suspend fun <T> getResult(
        call: suspend () -> retrofit2.Response<T>
    ): ResultEmittedData<T> {
        return try {
            val response = call()
            val responseCode = response.code()
            val successCode = when (responseCode) {
                200,
                201,
                202,
                203,
                204,
                205,
                206,
                207,
                208,
                226 -> true

                else -> false
            }
            val responseMessage = response.message()
            val responseBody = response.body() ?: getEmptyResponse()
            when {
                successCode && responseBody !is EmptyResponse -> {
                    dataSuccess(
                        model = responseBody,
                        message = responseMessage,
                        responseCode = responseCode,
                    )
                }

                successCode -> {
                    logDebug("getResult successCode", TAG)
                    dataSuccess(
                        model = getEmptyResponse(),
                        message = responseMessage,
                        responseCode = responseCode,
                    )
                }

                responseCode == 401 -> {
                    logError("getResult responseCode == 401", TAG)
                    startAuthorization()
                    dataError(
                        model = responseBody,
                        message = responseMessage,
                        responseCode = responseCode,
                        error = responseBody.toString(),
                        errorType = ErrorType.SERVER_ERROR,
                    )
                }

                else -> {
                    logError("getResult conditions else", TAG)
                    dataError(
                        model = responseBody,
                        error = null, // TODO
                        responseCode = responseCode,
                        message = responseMessage,
                        errorType = ErrorType.SERVER_ERROR,
                    )
                }

            }
        } catch (exception: UnknownHostException) {
            logError(
                "getResult Exception is UnknownHostException, message: ${exception.message} stackTrace: ${exception.stackTrace}",
                exception,
                TAG
            )
            dataError(
                model = null,
                error = null,
                responseCode = null,
                message = "No internet connection",
                errorType = ErrorType.NO_INTERNET_CONNECTION,
            )
        } catch (exception: SSLPeerUnverifiedException) {
            logError(
                "getResult Exception is SSLPeerUnverifiedException, message: ${exception.message} stackTrace: ${exception.stackTrace}\",",
                exception,
                TAG
            )
            dataError(
                model = null,
                error = null,
                responseCode = null,
                errorType = ErrorType.EXCEPTION,
                message = "Error while receiving data from server, encryption incorrect",
            )
        } catch (exception: JsonSyntaxException) {
            logError(
                "getResult Exception is JsonSyntaxException, message: ${exception.message} stackTrace: ${exception.stackTrace}\",",
                exception,
                TAG
            )
            dataError(
                model = null,
                error = null,
                responseCode = null,
                errorType = ErrorType.EXCEPTION,
                message = "Error while receiving data from server, data format incorrect",
            )
        } catch (exception: java.io.EOFException) {
            logError(
                "getResult Exception is EOFException, message: ${exception.message} stackTrace: ${exception.stackTrace}\",",
                exception,
                TAG
            )
            dataError(
                model = null,
                error = null,
                responseCode = null,
                errorType = ErrorType.EXCEPTION,
                message = exception.message ?: exception.toString(),
            )
        } catch (exception: Throwable) {
            logError(
                "getResult Exception is other, message: ${exception.message} stackTrace: ${exception.stackTrace}\",",
                exception,
                TAG
            )
            dataError(
                model = null,
                error = null,
                responseCode = null,
                errorType = ErrorType.EXCEPTION,
                message = exception.message ?: exception.toString(),
            )
        }
    }

    private fun <T> dataError(
        model: T?,
        error: Any?,
        responseCode: Int?,
        message: String?,
        errorType: ErrorType?,
    ): ResultEmittedData<T> = ResultEmittedData.error(
        model = model,
        error = error,
        message = message,
        errorType = errorType,
        responseCode = responseCode,
    )

    private fun <T> dataSuccess(
        model: T,
        message: String?,
        responseCode: Int,
    ): ResultEmittedData<T> = ResultEmittedData.success(
        model = model,
        message = message,
        responseCode = responseCode,
    )

    private fun startAuthorization() {
        logDebug("start authorization", TAG)
    }

}

class EmptyResponse

@Suppress("UNCHECKED_CAST")
fun <T> getEmptyResponse(): T {
    return EmptyResponse() as T
}
```

Код звісно не оптимальний і залежить від контексту проєкту, але думаю основний принцип зрозумілий і за необхідності можна переписати під свої потреби.

[ Copyright: Roman Kryvolapov ](https://t.me/RomanKryvolapov)
