Roman Kryvolapov Engineering Blog

Android Paging 3 та Paging in AdapterDelegates

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

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

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

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

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

Domain та Network моделі

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

// інтерфейс для зручності, або 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",
)
}

PagingSource у data шарі

далі нам необхідно в 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

далі переходимо до 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)
}

Fragment

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

// спочатку невеликий екстеншн для 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)
}
}

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

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

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

Тепер що стосується пагінації в популярній бібліотеці 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