Skip to content

Instantly share code, notes, and snippets.

@FishHawk
Last active September 27, 2024 16:44
Show Gist options
  • Save FishHawk/6e4706646401bea20242bdfad5d86a9e to your computer and use it in GitHub Desktop.
Save FishHawk/6e4706646401bea20242bdfad5d86a9e to your computer and use it in GitHub Desktop.
If your project does not have a deep dependency on the paging3 library, I recommend implementing a simple paging library yourself to avoid all the weird features of paging3. The paging library needs the view layer and the model layer to work together. It doesn't make sense to encapsulate these together.
!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() }
}
}
@Yerlan1Fit
Copy link

i suggest to put the following code into LaunchEffect or other effect handlers:

            if (index > maxAccessed) {
                maxAccessed = index
                if (
                    mangaList.appendState?.isSuccess == true &&
                    maxAccessed < mangaList.list.size + 30
                ) {
                    onRequestNextPage()
                }
            }

it is on line 30 in file 4

@ilhomsoliev
Copy link

What is Connectivity in RemoteList.kt file? I am getting error on that.

@ilhomsoliev
Copy link

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.

@FishHawk
Copy link
Author

FishHawk commented Jul 26, 2023

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.

@ilhomsoliev
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment