Last active
September 27, 2024 16:44
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
!simple paged list for jetpack compose |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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) }, | |
) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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() | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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.
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
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.