-
-
Save FishHawk/6e4706646401bea20242bdfad5d86a9e to your computer and use it in GitHub Desktop.
| !simple paged list for jetpack compose |
| // Model layer: remotePagingList() | |
| package com.fishhawk.lisu.data.network.base | |
| import kotlinx.coroutines.Dispatchers | |
| import kotlinx.coroutines.channels.Channel | |
| import kotlinx.coroutines.channels.awaitClose | |
| import kotlinx.coroutines.flow.Flow | |
| import kotlinx.coroutines.flow.callbackFlow | |
| import kotlinx.coroutines.flow.flowOn | |
| import kotlinx.coroutines.flow.receiveAsFlow | |
| import kotlinx.coroutines.launch | |
| sealed interface RemoteListAction<out T> { | |
| data class Mutate<T>(val transformer: (MutableList<T>) -> MutableList<T>) : RemoteListAction<T> | |
| object Reload : RemoteListAction<Nothing> | |
| object RequestNextPage : RemoteListAction<Nothing> | |
| } | |
| typealias RemoteListActionChannel<T> = Channel<RemoteListAction<T>> | |
| suspend fun <T> RemoteListActionChannel<T>.mutate(transformer: (MutableList<T>) -> MutableList<T>) { | |
| send(RemoteListAction.Mutate(transformer)) | |
| } | |
| suspend fun <T> RemoteListActionChannel<T>.reload() { | |
| send(RemoteListAction.Reload) | |
| } | |
| suspend fun <T> RemoteListActionChannel<T>.requestNextPage() { | |
| send(RemoteListAction.RequestNextPage) | |
| } | |
| class RemoteList<T>( | |
| private val actionChannel: RemoteListActionChannel<T>, | |
| val value: Result<PagedList<T>>?, | |
| ) { | |
| suspend fun mutate(transformer: (MutableList<T>) -> MutableList<T>) = | |
| actionChannel.mutate(transformer) | |
| suspend fun reload() = actionChannel.reload() | |
| suspend fun requestNextPage() = actionChannel.requestNextPage() | |
| } | |
| data class PagedList<T>( | |
| val list: List<T>, | |
| val appendState: Result<Unit>?, | |
| ) | |
| data class Page<Key : Any, T>( | |
| val data: List<T>, | |
| val nextKey: Key?, | |
| ) | |
| fun <Key : Any, T> remotePagingList( | |
| startKey: Key, | |
| loader: suspend (Key) -> Result<Page<Key, T>>, | |
| onStart: ((actionChannel: RemoteListActionChannel<T>) -> Unit)? = null, | |
| onClose: ((actionChannel: RemoteListActionChannel<T>) -> Unit)? = null, | |
| ): Flow<RemoteList<T>> = callbackFlow { | |
| val dispatcher = Dispatchers.IO.limitedParallelism(1) | |
| val actionChannel = Channel<RemoteListAction<T>>() | |
| var listState: Result<Unit>? = null | |
| var appendState: Result<Unit>? = null | |
| var value: MutableList<T> = mutableListOf() | |
| var nextKey: Key? = startKey | |
| onStart?.invoke(actionChannel) | |
| suspend fun mySend() { | |
| send( | |
| RemoteList( | |
| actionChannel = actionChannel, | |
| value = listState?.map { | |
| PagedList( | |
| appendState = appendState, | |
| list = value, | |
| ) | |
| }, | |
| ) | |
| ) | |
| } | |
| fun requestNextPage() = launch(dispatcher) { | |
| nextKey?.let { key -> | |
| appendState = null | |
| mySend() | |
| loader(key) | |
| .onSuccess { | |
| value.addAll(it.data) | |
| nextKey = it.nextKey | |
| listState = Result.success(Unit) | |
| appendState = Result.success(Unit) | |
| mySend() | |
| } | |
| .onFailure { | |
| if (listState?.isSuccess != true) | |
| listState = Result.failure(it) | |
| appendState = Result.failure(it) | |
| mySend() | |
| } | |
| } | |
| } | |
| var job = requestNextPage() | |
| launch(dispatcher) { | |
| actionChannel.receiveAsFlow().flowOn(dispatcher).collect { action -> | |
| when (action) { | |
| is RemoteListAction.Mutate -> { | |
| value = action.transformer(value) | |
| mySend() | |
| } | |
| is RemoteListAction.Reload -> { | |
| job.cancel() | |
| listState = null | |
| appendState = null | |
| value.clear() | |
| nextKey = startKey | |
| mySend() | |
| job = requestNextPage() | |
| } | |
| is RemoteListAction.RequestNextPage -> { | |
| if (!job.isActive) job = requestNextPage() | |
| } | |
| } | |
| } | |
| } | |
| launch(dispatcher) { | |
| Connectivity.instance?.interfaceName?.collect { | |
| if (job.isActive) { | |
| job.cancel() | |
| job = requestNextPage() | |
| } | |
| } | |
| } | |
| awaitClose { | |
| onClose?.invoke(actionChannel) | |
| } | |
| } |
| // An example about how to use remotePagingList in repository. | |
| private val daoFlow: StateFlow<Result<NetworkDao>?> // NetworkDao describe interfaces provided by server | |
| suspend fun search( | |
| providerId: String, | |
| keywords: String, | |
| ) = daoFlow.filterNotNull().flatMapLatest { | |
| remotePagingList( | |
| startKey = 0, | |
| loader = { page -> | |
| it.mapCatching { dao -> dao.search(providerId, page, keywords) } | |
| .map { Page(it, if (it.isEmpty()) null else page + 1) } | |
| }, | |
| onStart = { providerMangaListActionChannels.add(it) }, | |
| onClose = { providerMangaListActionChannels.remove(it) }, | |
| ) | |
| } |
| // An example about how to use remotePagingList in view model. | |
| private val _keywords = MutableStateFlow("") | |
| val keywords = _keywords.asStateFlow() | |
| private val _mangas = | |
| keywords | |
| .flatMapLatest { repository.search(it) } | |
| .stateIn(viewModelScope, SharingStarted.Eagerly, null) | |
| val mangas = | |
| _mangas | |
| .filterNotNull() | |
| .map { it.value } | |
| .stateIn(viewModelScope, SharingStarted.Eagerly, null) | |
| fun reload() { | |
| viewModelScope.launch { | |
| _mangas.value?.reload() | |
| } | |
| } | |
| fun requestNextPage() { | |
| viewModelScope.launch { | |
| _mangas.value?.requestNextPage() | |
| } | |
| } |
| // An heavily simplified example about how to use manga in compose with a lot of. | |
| // There are potential performance issues that can be solved by reducing the frequency of calls to onRequestNextPage. | |
| @Composable | |
| fun ComposeExample() { | |
| val mangaListResult by viewModel.mangas.collectAsState() | |
| StateView( | |
| result = mangaListResult, | |
| onRetry = { onAction(LibraryAction.Reload) }, | |
| modifier = Modifier | |
| .padding(paddingValues) | |
| .fillMaxSize(), | |
| ) { mangaList -> | |
| MangaList( | |
| mangaList = mangaList, | |
| onRequestNextPage = { onAction(LibraryAction.RequestNextPage) }, | |
| ) | |
| } | |
| } | |
| @Composable | |
| fun MangaList( | |
| mangaList: PagedList<MangaDto>, | |
| onRequestNextPage: () -> Unit, | |
| ) { | |
| var maxAccessed by remember { mutableStateOf(0) } | |
| LazyColumn { | |
| itemsIndexed(mangaList.list) { index, manga -> | |
| if (index > maxAccessed) { | |
| maxAccessed = index | |
| if ( | |
| mangaList.appendState?.isSuccess == true && | |
| maxAccessed < mangaList.list.size + 30 | |
| ) { | |
| onRequestNextPage() | |
| } | |
| } | |
| MangaCard(manga = manga) | |
| } | |
| fun itemFullWidth(content: @Composable () -> Unit) { | |
| item(span = { GridItemSpan(maxCurrentLineSpan) }) { Box {} } | |
| item(span = { GridItemSpan(maxCurrentLineSpan) }) { content() } | |
| } | |
| mangaList.appendState | |
| ?.onFailure { itemFullWidth { ErrorItem(it) { onRequestNextPage() } } } | |
| ?: itemFullWidth { LoadingItem() } | |
| } | |
| } |
What is Connectivity in RemoteList.kt file? I am getting error on that.
Please, create a sample project for this, I'm having confusions with these gists, some properties are not found, maybe you put it thinking that I am smart enough, but it is not) Please, make one sample project on your github.
What is Connectivity in RemoteList.kt file? I am getting error on that.
Connectivity is my helper class that watches the network connection change. It has nothing to do with paging.
I used this code in https://github.com/FishHawk/lisu-android, although it's not a simple project.
What is Connectivity in RemoteList.kt file? I am getting error on that.
Connectivityis my helper class that watches the network connection change. It has nothing to do with paging.I used this code in
https://github.com/FishHawk/lisu-android, although it's not a simple project.
Thank you very much, really good sample project.
i suggest to put the following code into LaunchEffect or other effect handlers:
it is on line 30 in file 4