Перейти к содержимому

Android Paging 3 и Paging in AdapterDelegates

Поскольку статей по библиотеке Android Paging и особенно по пагинации в библиотеке AdapterDelegates мне показалось довольно мало- пишу свою.

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

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

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

В коде будут комментарии, что и для чего нужно. Если найдете ошибку- напишите пожалуйста на телеграм RomanKryvolapov. Пишу все в блокноте, так как лень создавать проект

Допустим, у нас есть список с категориями пользователей и пользователями

// интерфейс для удобства, или 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

сетевые модели

// интерфейс для преобразования сетевой модели в модель для приложения
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",
    )
}

далее нам необходимо в data слое сделать PagingSource

// 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

// 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)
}

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

// сначала небольшой екстеншн для flow
fun <T> Flow<T>.launchWhenStarted(scope: LifecycleCoroutineScope) {
    scope.launchWhenStarted { [email protected]() }
}

// далее фрагмент
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)
    }
}

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

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
        }
    }
}

и далее адаптер для показа состояний

// здесь ради разнообразия не использовал 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 от Hannes Dorfmann
(эту часть статьи дописал через 2 года после предыдущей части и немного изменил подход)

https://hannesdorfmann.com/android/adapter-delegates

https://github.com/sockeqwe/AdapterDelegates

Делаем DataSource

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, здесь пример использования адаптера с только одним типом элемента:

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:

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

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)
        )
    }

}

Укажу и используемый класс для данных, но вы можете использовать любой подход, который вам нравится, это просто один из примеров

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
}
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