Android Paging 3 и Paging в AdapterDelegates
Тъй като статиите за библиотеката Android Paging, и особено за пагинацията в библиотеката AdapterDelegates, ми се сториха доста малко – пиша своя.
Библиотеката Paging позволява да се организира пагинация на данните в списъци в двете посоки, а също така умее да показва loader в долната част на списъка и някои други неща.
Google препоръчва да се използва AsyncPagingDataDiffer, но аз ще дам пример с PagingDataAdapter, който представлява wrapper над AsyncPagingDataDiffer и имплементира RecyclerView.Adapter.
Няма да правя RemoteMediator – в повечето задачи не е нужен, може по-късно да допълня статията и с него
В кода ще има коментари какво за какво е нужно. Ако намерите грешка – пишете, моля, в Telegram на RomanKryvolapov. Пиша всичко в бележник, защото ми е лениво да създавам проект
Domain и Network модели
Да допуснем, че имаме списък с категории потребители и потребители
// интерфейс за удобство, или Anyinterface 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 връща текстова грешка errordata 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>
// или записваме полетата в map и ги предаваме като 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, UserOrCategory>() { 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
След това фрагментът
// първо малък екстеншън за flowfun <T> Flow<T>.launchWhenStarted(scope: LifecycleCoroutineScope) { scope.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 = { // логика за клик по потребител, вместо това можеше да се използва listener или друг начин } )
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 = userAndCategoryAdapter.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.getUsersAndCategories( 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 } }}Адаптер за показване на състоянията
и по-нататък адаптер за показване на състоянията
// тук за разнообразие не използвах ViewBindingclass 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.CoroutineScopeimport kotlinx.coroutines.delayimport kotlinx.coroutines.flow.onEachimport 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.PagedListDelegationAdapterimport org.koin.core.component.KoinComponentimport 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.Viewimport android.view.ViewGroupimport androidx.core.view.isVisibleimport androidx.recyclerview.widget.RecyclerViewimport 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.LiveDataimport androidx.lifecycle.viewModelScopeimport androidx.paging.DataSourceimport androidx.paging.LivePagedListBuilderimport androidx.paging.PagedListimport 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.JsonSyntaxExceptionimport kotlinx.coroutines.withContextimport okhttp3.FormBodyimport okhttp3.OkHttpClientimport okhttp3.RequestBodyimport org.json.JSONObjectimport org.koin.core.component.KoinComponentimport org.koin.core.component.injectimport org.koin.core.qualifier.namedimport java.net.UnknownHostExceptionimport 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}Разбира се, кодът не е оптимален и зависи от контекста на проекта, но мисля, че основният принцип е ясен и при нужда може да се пренапише според вашите нужди.