Skip to content

Android Paging 3 and Paging in AdapterDelegates

Since there seemed to be very few articles on the Android Paging library and especially on pagination in the AdapterDelegates library, I am writing my own.

The Paging library allows you to organize pagination of data in lists in both directions, and can also show a loader at the bottom of the list and some other things.

Google recommends using AsyncPagingDataDiffer, but I will give an example with PagingDataAdapter, which represents a wrapper over AsyncPagingDataDiffer and implements RecyclerView.Adapter.

I will not make a RemoteMediator – in most tasks it is not needed, we can add it to the article later

The code will contain comments on what is needed and for what. If you find an error, please write to RomanKryvolapov on Telegram. I write everything in a notepad, because I am too lazy to create a project

Let’s say we have a list with user categories and users

// interface for convenience, or Any
interface UserOrCategory

// models for the application domain layer
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

network models

// interface for converting network model to application model
interface DomainMapper<T : Any> {
    fun mapToDomainModel(): T
}

// network models for the application data layer
// assume that in case of failure the api returns a text error
data class UsersAndCategoriesNetworkModel(
    private val list: List<Any>,
    private val error: String?,
) : DomainMapper<List<UserOrCategory>> {
// As a result, we should get a list containing users and categories 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",
    )
}

Next we need to create a PagingSource in the data layer

// getUsersAndCategories in networkApi - suspend function that returns UsersAndCategoriesNetworkModel
// for example
@FormUrlEncoded
@POST("get_users")
suspend fun getUsersAndCategories(
    @Field("page_number") pageNumber: Int,
    @Field("page_size") pageSize: Int
): Response<UsersAndCategoriesNetworkModel>

// or write the fields to the map and pass it as body
@POST("get_users")
suspend fun getUsersAndCategories(
    @Body body: Map<String, String>
): Response<UsersAndCategoriesNetworkModel>

// data source for the application data layer, in theory the call to getUsersAndCategories
// could be done through the repository, but here I did it directly. Also, the data source can theoretically be not only NetworkApi but anything
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> {
		// the page number is in key, if it is null then we also pass the initial page number to the class
		// initialPageNumber for api, since it can be, for example, 0 or 1
        val pageNumber: Int = params.key ?: initialPageNumber
        return try {
            val response = networkApi.getUsersAndCategories(pageNumber, pageSize)
			// if the text is not empty, then an error occurred
            if (!response?.body()?.error.isNullOrEmpty()) {
                LoadResult.Error(throwable = java.lang.Exception())
            } else {
                val userAndCategoryList = response.body()?.mapToDomainModel()
				// what will be in pageNumber for the next page
                val nextPageNumber: Int? = if (!userAndCategoryList.isNullOrEmpty()) pageNumber + 1 else null
				// and for the previous one
                val prevPageNumber: Int? = if (pageNumber > initialPageNumber) pageNumber - 1 else null
				// and here is the data for the pager
                LoadResult.Page(
                    data = userAndCategoryList.orEmpty(),
                    prevKey = prevPageNumber,
                    nextKey = nextPageNumber
                )
            }
		// it would be better to specify a network error here rather than 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)
    }

Next we move on to the ViewModel

// we will inject 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 is also thrown here via "remoteMediator =" if it is used
    ) {
        UserAndCategoryDataSource(
            pageSize = pageSize,
            initialPageNumber = initialPageNumber,
            networkApi = networkApi,
        )
    }.flow
        .flowOn(Dispatchers.IO)
        .stateIn(viewModelScope, SharingStarted.Lazily, PagingData.empty())
        .cachedIn(viewModelScope)
}

Next is a fragment

// first a small extension for flow
fun <T> Flow<T>.launchWhenStarted(scope: LifecycleCoroutineScope) {
    scope.launchWhenStarted { [email protected]() }
}

// next is a fragment
class UsersFragment : BaseFragment() {
    companion object {
		// then all constants for the pager
        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 is not needed in this example, but you may need to use it to cancel a task
    private var loadStateJob: Job? = null
    private val binding by viewBinding(FragmentUsersBinding::bind)
    private val viewModel: UserAndCategoryViewModel by sharedViewModel()

    // here we initialize the first adapter UserAndCategoryAdapter, which will show the list
    private val userAndCategoryAdapter = UserAndCategoryAdapter(
        onUserClicked = {
		// logic for clicking on user, could use listiner instead or any other way
        }
    )

    override fun getLayout() = R.layout.fragment_users
    override fun viewReady() {
        binding.run {
			// with withLoadStateHeaderAndFooter you can set a second StatementsLoaderAdapter that shows loading and error
            recyclerViewUsersAndCategories.adapter =
                userAndCategoryAdapter.withLoadStateHeaderAndFooter(
                    header = StatementsLoaderAdapter(),
                    footer = StatementsLoaderAdapter()
                )
            subscribeToLoadState()
            subscribeToDataSource()
        }
    }

    private fun subscribeToLoadState() {
		// instead of loadStateFlow you can use 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
				// and then any logic for the states, I advise logging the states to see how they work
				// in general there are CombinedLoadStates: refresh, prepend, append, source, mediator, and LoadState states: Loading, NotLoading, Error
            }
        }
    }.launchWhenStarted(lifecycleScope)

    private fun subscribeToDataSource() {
		// pass all constants
        viewModel.getAccountStatementsDataSource(
            pageSize = PAGE_SIZE,
            prefetchDistance = PREFETCH_DISTANCE,
            initialLoadSize = INITIAL_LOAD_SIZE,
            initialPageNumber = INITIAL_STATEMENTS_PAGE_NUMBER,
        ).onEach { data ->
			// throw the date into the adapter
            userAndCategoryAdapter.submitData(data)
        }.launchWhenStarted(lifecycleScope)
    }
}

next is an adapter for displaying the list

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

    // I will use viewBinding here
    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>() {

        // here is some logic for comparing elements
        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
        }

        // here is some logic for comparing whether the element has been updated or not
        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
        }
    }
}

and then an adapter for displaying states

// here for the sake of variety I didn't use 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")
    }
}

Now, regarding pagination in the popular AdapterDelegates library by Hannes Dorfmann
(I finished this part of the article 2 years after the previous part and slightly changed the approach)

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

https://github.com/sockeqwe/AdapterDelegates

Making a 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, here is an example of using an adapter with only one element type:

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

add to 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>>
    
    // this piece is copied from another class for example
    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)
        )
    }

}

I will also specify the class used for the data, but you can use any approach you like, this is just one example

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
}

The code is certainly not optimal and depends on the context of the project, but I think the basic principle is clear and can be rewritten to suit your needs if necessary.

Copyright: Roman Kryvolapov