-
-
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.
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.
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