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 provides a wrapper over AsyncPagingDataDiffer and implements RecyclerView.Adapter.
I won't make RemoteMediator - it's not needed in most tasks, we can add it to the article later
The code will contain comments on what is needed and for what purpose. If you find an error, please write to telegram RomanKryvolapov. I write everything in a notebook, because I am too lazy to create a project
Domain and Network Models
Let's say we have a list with user categories and users
// interface for convenience, or Anyinterface UserOrCategory
// models for the application domain layerdata 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) : UserOrCategorynetwork models
// interface for converting a network model into an application modelinterface DomainMapper<T : Any> { fun mapToDomainModel(): T}
// network models for the application data layer// Let's assume that in case of failure the api returns a text errordata 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", )}PagingSource in Data Layer
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, ideally a call to getUsersAndCategories// it could be done via a repository, but here I did it directly. Also, the data source theoretically can be not only NetworkApi but anythingclass 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, as 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) }ViewModel
Next we move on to the ViewModel
// networkApi we will injectclass 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)}Fragment
Next is a fragment
// first a small extension for flowfun <T> Flow<T>.launchWhenStarted(scope: LifecycleCoroutineScope) { scope.launchWhenStarted { this@launchWhenStarted.collect() }}
// next fragmentclass UsersFragment : BaseFragment() { companion object { // below are all the 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 click on user could use listiner instead or any other way } )
override fun getLayout() = R.layout.fragment_users override fun viewReady() { binding.run { // withLoadStateHeaderAndFooter you can set a second adapter 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 states, I advise logging 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() { // we pass all constants viewModel.getAccountStatementsDataSource( pageSize = PAGE_SIZE, prefetchDistance = PREFETCH_DISTANCE, initialLoadSize = INITIAL_LOAD_SIZE, initialPageNumber = INITIAL_STATEMENTS_PAGE_NUMBER, ).onEach { data -> // we throw the date into the adapter userAndCategoryAdapter.submitData(data) }.launchWhenStarted(lifecycleScope) }}Adapter for Displaying the List
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>() {
// there is some logic here 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 is 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 } }}Adapter for Displaying States
and then an adapter for displaying states
// here for the sake of variety I didn't use 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") }}Pagination in AdapterDelegates Library
Now, regarding pagination in the popular AdapterDelegates library by Hannes Dorfmann
(I finished writing 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.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, here is an example of using an adapter with only one element type:
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) } } }}add to 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>>
// 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.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}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.