-
Notifications
You must be signed in to change notification settings - Fork 9
#39 add test, flow, core modules + up version #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 3 commits
2a322d3
a2e94d2
2f149bb
b6d6245
b935e41
7ebb18c
0f4105b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| /* | ||
| * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. | ||
| */ | ||
|
|
||
| plugins { | ||
| id("dev.icerock.moko.gradle.multiplatform.mobile") | ||
| id("dev.icerock.moko.gradle.publication") | ||
| id("dev.icerock.moko.gradle.stub.javadoc") | ||
| id("dev.icerock.moko.gradle.detekt") | ||
| } | ||
|
|
||
| kotlin { | ||
| jvm() | ||
| } | ||
|
|
||
| dependencies { | ||
| commonMainImplementation(libs.coroutines) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <manifest package="dev.icerock.moko.paging.core" /> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| /* | ||
| * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. | ||
| */ | ||
|
|
||
| package dev.icerock.moko.paging.core | ||
|
|
||
| interface IdEntity { | ||
| val id: Long | ||
| } | ||
|
|
||
| class IdComparator<T : IdEntity> : Comparator<T> { | ||
| override fun compare(a: T, b: T): Int { | ||
| return if (a.id == b.id) 0 else 1 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| /* | ||
| * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. | ||
| */ | ||
|
|
||
| package dev.icerock.moko.paging.core | ||
|
|
||
| import kotlinx.coroutines.CoroutineScope | ||
| import kotlinx.coroutines.Deferred | ||
| import kotlinx.coroutines.launch | ||
| import kotlinx.coroutines.sync.Mutex | ||
| import kotlin.coroutines.CoroutineContext | ||
|
|
||
| abstract class Pagination<Item>( | ||
| parentScope: CoroutineScope, | ||
| protected val dataSource: PagedListDataSource<Item>, | ||
| protected val comparator: Comparator<Item>, | ||
| protected val nextPageListener: (Result<List<Item>>) -> Unit, | ||
| protected val refreshListener: (Result<List<Item>>) -> Unit, | ||
| ) : CoroutineScope { | ||
|
|
||
| override val coroutineContext: CoroutineContext = parentScope.coroutineContext | ||
|
|
||
| protected var loadNextPageDeferred: Deferred<List<Item>>? = null | ||
| protected val listMutex = Mutex() | ||
|
|
||
| fun loadFirstPage() { | ||
| launch { loadFirstPageSuspend() } | ||
| } | ||
|
|
||
| fun loadNextPage() { | ||
| launch { loadNextPageSuspend() } | ||
| } | ||
|
|
||
| fun refresh() { | ||
| launch { refreshSuspend() } | ||
| } | ||
|
|
||
| fun setData(items: List<Item>?) { | ||
| launch { setDataSuspend(items) } | ||
| } | ||
|
|
||
| abstract suspend fun loadFirstPageSuspend() | ||
| abstract suspend fun loadNextPageSuspend() | ||
| abstract suspend fun refreshSuspend() | ||
| abstract suspend fun setDataSuspend(items: List<Item>?) | ||
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| /* | ||
| * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. | ||
| */ | ||
|
|
||
| plugins { | ||
| id("dev.icerock.moko.gradle.multiplatform.mobile") | ||
| id("dev.icerock.moko.gradle.publication") | ||
| id("dev.icerock.moko.gradle.stub.javadoc") | ||
| id("dev.icerock.moko.gradle.detekt") | ||
| } | ||
|
|
||
| kotlin { | ||
| jvm() | ||
|
||
| } | ||
|
|
||
| dependencies { | ||
| commonMainImplementation(libs.coroutines) | ||
|
|
||
| commonMainApi(projects.pagingCore) | ||
| commonMainApi(libs.mokoMvvmFlow) | ||
| commonMainApi(libs.mokoMvvmState) | ||
|
|
||
| commonTestImplementation(projects.pagingTest) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <manifest package="dev.icerock.moko.paging.flow" /> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| /* | ||
| * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. | ||
| */ | ||
|
|
||
| package dev.icerock.moko.paging.flow | ||
|
|
||
| import dev.icerock.moko.paging.core.ReachEndNotifierList | ||
| import dev.icerock.moko.paging.core.withReachEndNotifier | ||
| import kotlinx.coroutines.CoroutineScope | ||
| import kotlinx.coroutines.flow.Flow | ||
| import kotlinx.coroutines.flow.SharingStarted | ||
| import kotlinx.coroutines.flow.StateFlow | ||
| import kotlinx.coroutines.flow.combine | ||
| import kotlinx.coroutines.flow.map | ||
| import kotlinx.coroutines.flow.stateIn | ||
|
|
||
| fun <T> Flow<List<T>>.withLoadingItem( | ||
| loading: Flow<Boolean>, | ||
| itemFactory: () -> T | ||
| ): Flow<List<T>> = combine(this, loading) { items, nextPageLoading -> | ||
| if (nextPageLoading) { | ||
| items + itemFactory() | ||
| } else { | ||
| items | ||
| } | ||
| } | ||
|
|
||
| fun <T> Flow<List<T>>.withReachEndNotifier( | ||
| action: (Int) -> Unit | ||
| ): Flow<ReachEndNotifierList<T>> = map { list -> | ||
| list.withReachEndNotifier(action) | ||
| } | ||
|
|
||
| fun <T> Flow<List<T>>.stateWithLoadingItem( | ||
| parentScope: CoroutineScope, | ||
| loading: Flow<Boolean>, | ||
| itemFactory: () -> T | ||
| ): StateFlow<List<T>> = combine(this, loading) { items, nextPageLoading -> | ||
| if (nextPageLoading) { | ||
| items + itemFactory() | ||
| } else { | ||
| items | ||
| } | ||
| }.stateIn(parentScope, SharingStarted.Lazily, emptyList()) | ||
|
||
|
|
||
| fun <T> Flow<List<T>>.stateWithReachEndNotifier( | ||
| parentScope: CoroutineScope, | ||
| action: (Int) -> Unit | ||
| ): StateFlow<ReachEndNotifierList<T>> = map { list -> | ||
| list.withReachEndNotifier(action) | ||
| }.stateIn(parentScope, SharingStarted.Lazily, ReachEndNotifierList(emptyList(), action)) | ||
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,160 @@ | ||
| /* | ||
| * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. | ||
| */ | ||
|
|
||
| package dev.icerock.moko.paging.flow | ||
|
|
||
| import dev.icerock.moko.mvvm.ResourceState | ||
| import dev.icerock.moko.mvvm.asState | ||
| import dev.icerock.moko.paging.core.PagedListDataSource | ||
| import dev.icerock.moko.paging.core.Pagination | ||
| import kotlinx.coroutines.CoroutineScope | ||
| import kotlinx.coroutines.async | ||
| import kotlinx.coroutines.flow.MutableStateFlow | ||
| import kotlinx.coroutines.flow.StateFlow | ||
|
|
||
| class Pagination<Item>( | ||
| parentScope: CoroutineScope, | ||
| dataSource: PagedListDataSource<Item>, | ||
| comparator: Comparator<Item>, | ||
| nextPageListener: (Result<List<Item>>) -> Unit, | ||
| refreshListener: (Result<List<Item>>) -> Unit, | ||
| initValue: List<Item>? = null | ||
| ) : Pagination<Item>( | ||
| parentScope = parentScope, | ||
| dataSource = dataSource, | ||
| comparator = comparator, | ||
| nextPageListener = nextPageListener, | ||
| refreshListener = refreshListener | ||
| ) { | ||
|
|
||
| private val mStateStorage: MutableStateFlow<ResourceState<List<Item>, Throwable>> = | ||
| MutableStateFlow(initValue.asStateNullIsLoading()) | ||
|
|
||
| val state: StateFlow<ResourceState<List<Item>, Throwable>> = mStateStorage | ||
|
|
||
| private val mNextPageLoading: MutableStateFlow<Boolean> = MutableStateFlow(false) | ||
|
||
| val nextPageLoading: StateFlow<Boolean> get() = mNextPageLoading | ||
|
|
||
| private val mEndOfList: MutableStateFlow<Boolean> = MutableStateFlow(false) | ||
|
|
||
| private val mRefreshLoading: MutableStateFlow<Boolean> = MutableStateFlow(false) | ||
| val refreshLoading: StateFlow<Boolean> get() = mRefreshLoading | ||
|
|
||
| override suspend fun loadFirstPageSuspend() { | ||
| loadNextPageDeferred?.cancel() | ||
|
|
||
| listMutex.lock() | ||
|
|
||
| mEndOfList.value = false | ||
| mNextPageLoading.value = false | ||
| mStateStorage.value = ResourceState.Loading() | ||
|
|
||
| @Suppress("TooGenericExceptionCaught") | ||
| try { | ||
| val items: List<Item> = dataSource.loadPage(null) | ||
| mStateStorage.value = items.asState() | ||
| } catch (error: Exception) { | ||
| mStateStorage.value = ResourceState.Failed(error) | ||
| } | ||
| listMutex.unlock() | ||
| } | ||
|
|
||
| @Suppress("ReturnCount") | ||
| override suspend fun loadNextPageSuspend() { | ||
| if (mNextPageLoading.value) return | ||
| if (mRefreshLoading.value) return | ||
| if (mEndOfList.value) return | ||
|
|
||
| listMutex.lock() | ||
|
|
||
| mNextPageLoading.value = true | ||
|
|
||
| @Suppress("TooGenericExceptionCaught") | ||
| try { | ||
| loadNextPageDeferred = this.async { | ||
| val currentList = mStateStorage.value.dataValue() | ||
| ?: throw IllegalStateException("Try to load next page when list is empty") | ||
| // load next page items | ||
| val items = dataSource.loadPage(currentList) | ||
| // remove already exist items | ||
| val newItems = items.filter { item -> | ||
| val existsItem = currentList.firstOrNull { comparator.compare(item, it) == 0 } | ||
| existsItem == null | ||
| } | ||
| // append new items to current list | ||
| val newList = currentList.plus(newItems) | ||
| // mark end of list if no new items | ||
| if (newItems.isEmpty()) { | ||
| mEndOfList.value = true | ||
| } else { | ||
| // save | ||
| mStateStorage.value = newList.asState() | ||
| } | ||
| newList | ||
| } | ||
| val newList = loadNextPageDeferred!!.await() | ||
|
|
||
| // flag | ||
| mNextPageLoading.value = false | ||
| // notify | ||
| nextPageListener(Result.success(newList)) | ||
| } catch (error: Exception) { | ||
| // flag | ||
| mNextPageLoading.value = false | ||
| // notify | ||
| nextPageListener(Result.failure(error)) | ||
| } | ||
| listMutex.unlock() | ||
| } | ||
|
|
||
| override suspend fun refreshSuspend() { | ||
| loadNextPageDeferred?.cancel() | ||
| listMutex.lock() | ||
|
|
||
| if (mRefreshLoading.value) { | ||
| listMutex.unlock() | ||
| return | ||
| } | ||
| if (mNextPageLoading.value) { | ||
| listMutex.unlock() | ||
| return | ||
| } | ||
|
|
||
| mRefreshLoading.value = true | ||
|
|
||
| @Suppress("TooGenericExceptionCaught") | ||
| try { | ||
| // load first page items | ||
| val items = dataSource.loadPage(null) | ||
| // save | ||
| mStateStorage.value = items.asState() | ||
| // flag | ||
| mEndOfList.value = false | ||
| mRefreshLoading.value = false | ||
| // notify | ||
| refreshListener(Result.success(items)) | ||
| } catch (error: Exception) { | ||
| // flag | ||
| mRefreshLoading.value = false | ||
| // notify | ||
| refreshListener(Result.failure(error)) | ||
| } | ||
| listMutex.unlock() | ||
| } | ||
|
|
||
| override suspend fun setDataSuspend(items: List<Item>?) { | ||
| listMutex.lock() | ||
| mStateStorage.value = items.asStateNullIsEmpty() | ||
| mEndOfList.value = false | ||
| listMutex.unlock() | ||
| } | ||
| } | ||
|
|
||
| fun <T, E> List<T>?.asStateNullIsEmpty() = asState { | ||
| ResourceState.Empty<List<T>, E>() | ||
| } | ||
|
|
||
| fun <T, E> List<T>?.asStateNullIsLoading() = asState { | ||
| ResourceState.Loading<List<T>, E>() | ||
| } | ||
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
api dependency. we have
CoroutineScopein constructor