Поскольку статей по библиотеке 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
}Код конечно не оптимален и зависит от контекста проекта, но думаю основной принцип понятен и при необходимости можно переписать под свои нужды.