Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .idea/copyright/IceRock.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions .idea/copyright/profiles_settings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ recyclerViewVersion = "1.1.0"
swipeRefreshLayoutVersion = "1.1.0"
ktorClientVersion = "2.0.0"
coroutinesVersion = "1.6.0-native-mt"
mokoMvvmVersion = "0.12.0"
mokoResourcesVersion = "0.18.0"
mokoMvvmVersion = "0.13.0"
mokoResourcesVersion = "0.20.1"
mokoUnitsVersion = "0.8.0"
mokoPagingVersion = "0.7.1"
mokoPagingVersion = "0.8.0"

[libraries]
appCompat = { module = "androidx.appcompat:appcompat", version.ref = "androidAppCompatVersion" }
Expand All @@ -24,6 +24,8 @@ mokoUnits = { module = "dev.icerock.moko:units", version.ref = "mokoUnitsVersion
mokoUnitsDataBinding = { module = "dev.icerock.moko:units-databinding", version.ref = "mokoUnitsVersion" }
mokoMvvmLiveData = { module = "dev.icerock.moko:mvvm-livedata", version.ref = "mokoMvvmVersion" }
mokoMvvmState = { module = "dev.icerock.moko:mvvm-state", version.ref = "mokoMvvmVersion" }
mokoMvvmFlow = { module = "dev.icerock.moko:mvvm-flow", version.ref = "mokoMvvmVersion" }
mokoMvvmCore = { module = "dev.icerock.moko:mvvm-core", version.ref = "mokoMvvmVersion" }
kotlinTestJUnit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlinVersion" }
androidCoreTesting = { module = "androidx.arch.core:core-testing", version.ref = "androidCoreTestingVersion" }
ktorClientMock = { module = "io.ktor:ktor-client-mock", version.ref = "ktorClientVersion" }
Expand Down
18 changes: 18 additions & 0 deletions paging-core/build.gradle.kts
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

api dependency. we have CoroutineScope in constructor

}
2 changes: 2 additions & 0 deletions paging-core/src/androidMain/AndroidManifest.xml
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
Expand Up @@ -2,7 +2,7 @@
* Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.icerock.moko.paging
package dev.icerock.moko.paging.core

class LambdaPagedListDataSource<T>(
private val loadPageLambda: suspend (List<T>?) -> List<T>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.icerock.moko.paging
package dev.icerock.moko.paging.core

interface PagedListDataSource<T> {
suspend fun loadPage(currentList: List<T>?): List<T>
Expand Down
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>?)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should not be abstract - it should be implemented here, in core.
abstract should be only protected methods for set/get data in state.

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.icerock.moko.paging
package dev.icerock.moko.paging.core

@Suppress("TooManyFunctions")
class ReachEndNotifierList<T>(
Expand Down
24 changes: 24 additions & 0 deletions paging-flow/build.gradle.kts
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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

something wrong with format

}

dependencies {
commonMainImplementation(libs.coroutines)

commonMainApi(projects.pagingCore)
commonMainApi(libs.mokoMvvmFlow)
commonMainApi(libs.mokoMvvmState)

commonTestImplementation(projects.pagingTest)
}
2 changes: 2 additions & 0 deletions paging-flow/src/androidMain/AndroidManifest.xml
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())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why duplication?

this.withLoadingItem(loading, itemFactory).stateIn(..)


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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please remove duplication

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove m prefixes please. update to current kotlin codestyle

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>()
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this not related to Flow

Loading