From 8b007bb3afb29f10211a66ee5132f0996ef44b43 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Wed, 26 Feb 2025 20:56:56 +0600 Subject: [PATCH 01/14] start 0.24.8 --- CHANGELOG.md | 2 ++ gradle.properties | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dfd53f9ce7..9eaf613bce4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## 0.24.8 + ## 0.24.7 * `Versions`: diff --git a/gradle.properties b/gradle.properties index e97e92ca5c0..838bb00644d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,5 +15,5 @@ crypto_js_version=4.1.1 # Project data group=dev.inmo -version=0.24.7 -android_code_version=287 +version=0.24.8 +android_code_version=288 From 98c7b48625989bbbbc0345a0901a35b04a898765 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Wed, 26 Feb 2025 20:57:17 +0600 Subject: [PATCH 02/14] add SortedBinaryTreeNode --- CHANGELOG.md | 3 + .../dev/inmo/micro_utils/common/Comparator.kt | 6 + .../collections/SortedBinaryTreeNode.kt | 319 ++++++++++++++++++ .../kotlin/SortedBinaryTreeNodeTests.kt | 138 ++++++++ 4 files changed, 466 insertions(+) create mode 100644 common/src/commonMain/kotlin/dev/inmo/micro_utils/common/Comparator.kt create mode 100644 coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/collections/SortedBinaryTreeNode.kt create mode 100644 coroutines/src/commonTest/kotlin/SortedBinaryTreeNodeTests.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eaf613bce4..c9c3a0ff0e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## 0.24.8 +* `Coroutines`: + * Add `SortedBinaryTreeNode` + ## 0.24.7 * `Versions`: diff --git a/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/Comparator.kt b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/Comparator.kt new file mode 100644 index 00000000000..61ce00847fe --- /dev/null +++ b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/Comparator.kt @@ -0,0 +1,6 @@ +package dev.inmo.micro_utils.common + +/** + * Creates simple [Comparator] which will use [compareTo] of [T] for both objects + */ +fun , C : T> T.createComparator() = Comparator { o1, o2 -> o1.compareTo(o2) } diff --git a/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/collections/SortedBinaryTreeNode.kt b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/collections/SortedBinaryTreeNode.kt new file mode 100644 index 00000000000..fade45c3e61 --- /dev/null +++ b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/collections/SortedBinaryTreeNode.kt @@ -0,0 +1,319 @@ +package dev.inmo.micro_utils.coroutines.collections + +import dev.inmo.micro_utils.coroutines.SmartRWLocker +import dev.inmo.micro_utils.coroutines.waitReadRelease +import dev.inmo.micro_utils.coroutines.withReadAcquire +import dev.inmo.micro_utils.coroutines.withWriteLock +import kotlinx.coroutines.job +import kotlinx.serialization.Serializable +import kotlin.coroutines.coroutineContext + +/** + * Creates simple [Comparator] which will use [compareTo] of [T] for both objects + */ +private fun , C : T> T.createComparator() = Comparator { o1, o2 -> o1.compareTo(o2) } + +@Serializable +class SortedBinaryTreeNode( + val data: T, + internal val comparator: Comparator, +) : Iterable> { + internal var leftNode: SortedBinaryTreeNode? = null + internal var rightNode: SortedBinaryTreeNode? = null + internal val locker: SmartRWLocker by lazy { + SmartRWLocker() + } + + suspend fun getLeftNode() = locker.withReadAcquire { + leftNode + } + + suspend fun getRightNode() = locker.withReadAcquire { + rightNode + } + + suspend fun getLeft() = getLeftNode() ?.data + + suspend fun getRight() = getRightNode() ?.data + + override fun equals(other: Any?): Boolean { + return other === this || (other is SortedBinaryTreeNode<*> && other.data == data && other.rightNode == rightNode && other.leftNode == leftNode) + } + + override fun hashCode(): Int { + return data.hashCode() * 31 + rightNode.hashCode() + leftNode.hashCode() + } + + suspend fun size(): Int { + return locker.withReadAcquire { + 1 + (leftNode ?.size() ?: 0) + (rightNode ?.size() ?: 0) + } + } + + /** + * This [Iterator] will run from less to greater values of nodes starting the + * [dev.inmo.micro_utils.coroutines.collections.SortedBinaryTreeNode]-receiver. Due to non-suspending + * nature of [iterator] builder, this [Iterator] **DO NOT** guarantee consistent content due to iterations. It + * means, that tree can be changed during to iteration process + */ + override fun iterator(): Iterator> = iterator { + leftNode ?.let { + it.iterator().forEach { yield(it) } + } + yield(this@SortedBinaryTreeNode) + rightNode ?.let { + it.iterator().forEach { yield(it) } + } + } + + override fun toString(): String { + return "$data($leftNode;$rightNode)" + } + + companion object { + operator fun > invoke( + data: T, + ) = SortedBinaryTreeNode( + data, + data.createComparator() + ) + } +} + +/** + * Will add subnode in tree if there are no any node with [newData] + * + * * If [newData] is greater than [SortedBinaryTreeNode.data] of currently checking node, + * will be used [SortedBinaryTreeNode.rightNode] + * * If [newData] is equal to [SortedBinaryTreeNode.data] of currently + * checking node - will be returned currently checking node + * * If [newData] is less than [SortedBinaryTreeNode.data] of currently + * checking node - will be used [SortedBinaryTreeNode.leftNode] + * + * This process will continue until function will not find place to put [SortedBinaryTreeNode] with data or + * [SortedBinaryTreeNode] with [SortedBinaryTreeNode.data] same as [newData] will be found + */ +private suspend fun SortedBinaryTreeNode.addSubNode( + subNode: SortedBinaryTreeNode, + skipLockers: Set = emptySet() +): SortedBinaryTreeNode { + var currentlyChecking = this + val lockedLockers = mutableSetOf() + try { + while (coroutineContext.job.isActive) { + if (currentlyChecking.locker !in lockedLockers && currentlyChecking.locker !in skipLockers) { + currentlyChecking.locker.lockWrite() + lockedLockers.add(currentlyChecking.locker) + } + val left = currentlyChecking.leftNode + val right = currentlyChecking.rightNode + val comparingResult = currentlyChecking.comparator.compare(subNode.data, currentlyChecking.data) + val isGreater = comparingResult > 0 + when { + comparingResult == 0 -> return currentlyChecking + isGreater && right == null -> { + currentlyChecking.rightNode = subNode + return subNode + } + isGreater && right != null -> { + currentlyChecking = right + } + left == null -> { + currentlyChecking.leftNode = subNode + return subNode + } + else -> { + currentlyChecking = left + } + } + } + } finally { + lockedLockers.forEach { + runCatching { it.unlockWrite() } + } + } + error("Unable to add node") +} + +/** + * Will add subnode in tree if there are no any node with [newData] + * + * * If [newData] is greater than [SortedBinaryTreeNode.data] of currently checking node, + * will be used [SortedBinaryTreeNode.rightNode] + * * If [newData] is equal to [SortedBinaryTreeNode.data] of currently + * checking node - will be returned currently checking node + * * If [newData] is less than [SortedBinaryTreeNode.data] of currently + * checking node - will be used [SortedBinaryTreeNode.leftNode] + * + * This process will continue until function will not find place to put [SortedBinaryTreeNode] with data or + * [SortedBinaryTreeNode] with [SortedBinaryTreeNode.data] same as [newData] will be found + */ +suspend fun SortedBinaryTreeNode.addSubNode(newData: T): SortedBinaryTreeNode { + return addSubNode( + SortedBinaryTreeNode(newData, comparator) + ) +} + +suspend fun SortedBinaryTreeNode.findParentNode(data: T): SortedBinaryTreeNode? { + var currentParent: SortedBinaryTreeNode? = null + var currentlyChecking: SortedBinaryTreeNode? = this + val lockedLockers = mutableSetOf() + try { + while (coroutineContext.job.isActive) { + if (currentlyChecking == null) { + return null + } + if (currentlyChecking.locker !in lockedLockers) { + currentlyChecking.locker.acquireRead() + lockedLockers.add(currentlyChecking.locker) + } + val comparingResult = currentlyChecking.comparator.compare(data, currentlyChecking.data) + when { + comparingResult > 0 -> { + currentParent = currentlyChecking + currentlyChecking = currentlyChecking.rightNode + continue + } + comparingResult < 0 -> { + currentParent = currentlyChecking + currentlyChecking = currentlyChecking.leftNode + continue + } + else -> return currentParent + } + } + } finally { + lockedLockers.forEach { + runCatching { it.releaseRead() } + } + } + error("Unable to find node") +} + +/** + * Will remove (detach) node from tree starting with [this] [SortedBinaryTreeNode] + * + * @return If data were found, [Pair] where [Pair.first] is the parent node where from [Pair.second] has been detached; + * null otherwise + */ +suspend fun SortedBinaryTreeNode.removeSubNode(data: T): Pair, SortedBinaryTreeNode>? { + val onFoundToRemoveCallback: suspend SortedBinaryTreeNode.(left: SortedBinaryTreeNode?, right: SortedBinaryTreeNode?) -> Unit = { left, right -> + left ?.also { leftNode -> addSubNode(leftNode, setOf(locker)) } + right ?.also { rightNode -> addSubNode(rightNode, setOf(locker)) } + } + while (coroutineContext.job.isActive) { + val foundParentNode = findParentNode(data) ?: return null + foundParentNode.locker.withWriteLock { + val left = foundParentNode.leftNode + val right = foundParentNode.rightNode + when { + left != null && left.comparator.compare(data, left.data) == 0 -> { + foundParentNode.leftNode = null + foundParentNode.onFoundToRemoveCallback(left.leftNode, left.rightNode) + return foundParentNode to left + } + right != null && right.comparator.compare(data, right.data) == 0 -> { + foundParentNode.rightNode = null + foundParentNode.onFoundToRemoveCallback(right.leftNode, right.rightNode) + return foundParentNode to right + } + else -> { + return@withWriteLock // data has been changed, new search required + } + } + } + } + error("Unable to remove node") +} +suspend fun SortedBinaryTreeNode.findNode(data: T): SortedBinaryTreeNode? { + var currentlyChecking: SortedBinaryTreeNode? = this + val lockedLockers = mutableSetOf() + try { + while (coroutineContext.job.isActive) { + if (currentlyChecking == null) { + return null + } + if (currentlyChecking.locker !in lockedLockers) { + currentlyChecking.locker.acquireRead() + lockedLockers.add(currentlyChecking.locker) + } + val comparingResult = currentlyChecking.comparator.compare(data, currentlyChecking.data) + when { + comparingResult > 0 -> { + currentlyChecking = currentlyChecking.rightNode + continue + } + comparingResult < 0 -> { + currentlyChecking = currentlyChecking.leftNode + continue + } + else -> return currentlyChecking + } + } + } finally { + lockedLockers.forEach { + runCatching { it.releaseRead() } + } + } + error("Unable to find node") +} +suspend fun SortedBinaryTreeNode.contains(data: T): Boolean = findNode(data) != null + +suspend fun SortedBinaryTreeNode.findNodesInRange(from: T, to: T, fromInclusiveMode: Boolean, toInclusiveMode: Boolean): Set> { + val results = mutableSetOf>() + val leftToCheck = mutableSetOf(this) + val lockedLockers = mutableSetOf() + val fromComparingFun: (SortedBinaryTreeNode) -> Boolean = if (fromInclusiveMode) { + { it.comparator.compare(from, it.data) <= 0 } + } else { + { it.comparator.compare(from, it.data) < 0 } + } + val toComparingFun: (SortedBinaryTreeNode) -> Boolean = if (toInclusiveMode) { + { it.comparator.compare(to, it.data) >= 0 } + } else { + { it.comparator.compare(to, it.data) > 0 } + } + try { + while (coroutineContext.job.isActive && leftToCheck.isNotEmpty()) { + val currentlyChecking = leftToCheck.first() + leftToCheck.remove(currentlyChecking) + if (currentlyChecking in results) { + continue + } + currentlyChecking.locker.acquireRead() + lockedLockers.add(currentlyChecking.locker) + if (fromComparingFun(currentlyChecking) && toComparingFun(currentlyChecking)) { + results.add(currentlyChecking) + currentlyChecking.leftNode ?.let { leftToCheck.add(it) } + currentlyChecking.rightNode ?.let { leftToCheck.add(it) } + continue + } + when { + currentlyChecking.comparator.compare(to, currentlyChecking.data) < 0 -> currentlyChecking.leftNode ?.let { leftToCheck.add(it) } + currentlyChecking.comparator.compare(from, currentlyChecking.data) > 0 -> currentlyChecking.rightNode ?.let { leftToCheck.add(it) } + } + } + return results.toSet() + } finally { + lockedLockers.forEach { + runCatching { it.releaseRead() } + } + } + error("Unable to find nodes range") +} +suspend fun SortedBinaryTreeNode.findNodesInRange(from: T, to: T): Set> = findNodesInRange( + from = from, + to = to, + fromInclusiveMode = true, + toInclusiveMode = true +) +suspend fun SortedBinaryTreeNode.findNodesInRangeExcluding(from: T, to: T): Set> = findNodesInRange( + from = from, + to = to, + fromInclusiveMode = false, + toInclusiveMode = false +) +suspend fun > SortedBinaryTreeNode.findNodesInRange(range: ClosedRange): Set> = findNodesInRange( + from = range.start, + to = range.endInclusive, +) \ No newline at end of file diff --git a/coroutines/src/commonTest/kotlin/SortedBinaryTreeNodeTests.kt b/coroutines/src/commonTest/kotlin/SortedBinaryTreeNodeTests.kt new file mode 100644 index 00000000000..884c85ea061 --- /dev/null +++ b/coroutines/src/commonTest/kotlin/SortedBinaryTreeNodeTests.kt @@ -0,0 +1,138 @@ +import dev.inmo.micro_utils.coroutines.collections.SortedBinaryTreeNode +import dev.inmo.micro_utils.coroutines.collections.addSubNode +import dev.inmo.micro_utils.coroutines.collections.findNode +import dev.inmo.micro_utils.coroutines.collections.findNodesInRange +import dev.inmo.micro_utils.coroutines.collections.findParentNode +import dev.inmo.micro_utils.coroutines.collections.removeSubNode +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class SortedBinaryTreeNodeTests { + @Test + fun insertOnZeroLevelWorks() = runTest { + val zeroNode = SortedBinaryTreeNode(0) + zeroNode.addSubNode(1) + zeroNode.addSubNode(-1) + + assertEquals(0, zeroNode.data) + assertEquals(1, zeroNode.getRightNode() ?.data) + assertEquals(-1, zeroNode.getLeftNode() ?.data) + } + @Test + fun searchOnZeroLevelWorks() = runTest { + val zeroNode = SortedBinaryTreeNode(0) + val oneNode = zeroNode.addSubNode(1) + val minusOneNode = zeroNode.addSubNode(-1) + + val assertingNodesToSearchQuery = mapOf( + setOf(oneNode) to (1 .. 1), + setOf(zeroNode, oneNode) to (0 .. 1), + setOf(minusOneNode, zeroNode, oneNode) to (-1 .. 1), + setOf(minusOneNode, zeroNode) to (-1 .. 0), + setOf(minusOneNode) to (-1 .. -1), + setOf(zeroNode) to (0 .. 0), + ) + + assertingNodesToSearchQuery.forEach { + val foundData = zeroNode.findNodesInRange(it.value) + assertTrue(foundData.containsAll(it.key)) + assertTrue(it.key.containsAll(foundData)) + } + } + @Test + fun deepInsertOnWorks() = runTest { + val zeroNode = SortedBinaryTreeNode(0) + val rangeRadius = 500 + val nodes = mutableMapOf>() + for (i in -rangeRadius .. rangeRadius) { + nodes[i] = zeroNode.addSubNode(i) + } + + for (i in -rangeRadius .. rangeRadius) { + val expectedNode = nodes.getValue(i) + val foundNode = zeroNode.findNode(i) + + assertTrue(expectedNode === foundNode) + + if (expectedNode === zeroNode) continue + + val parentNode = zeroNode.findParentNode(i) + assertTrue( + parentNode ?.getLeftNode() === expectedNode || parentNode ?.getRightNode() === expectedNode, + "It is expected, that parent node with data ${parentNode ?.data} will be parent of ${expectedNode.data}, but its left subnode is ${parentNode ?.getLeftNode() ?.data} and right one is ${parentNode ?.getRightNode() ?.data}" + ) + } + + val sourceTreeSize = zeroNode.size() + assertTrue(sourceTreeSize == nodes.size) + assertTrue(sourceTreeSize == (rangeRadius * 2 + 1)) + + for (i in -rangeRadius .. rangeRadius) { + val expectedNode = nodes.getValue(i) + val parentNode = zeroNode.findParentNode(i) + + if (parentNode == null && i == zeroNode.data && expectedNode === zeroNode) continue + + assertTrue(parentNode != null, "It is expected, that parent node of ${expectedNode.data} will not be null") + + assertTrue( + parentNode.getLeftNode() === expectedNode || parentNode.getRightNode() === expectedNode, + "It is expected, that parent node with data ${parentNode ?.data} will be parent of ${expectedNode.data}, but its left subnode is ${parentNode ?.getLeftNode() ?.data} and right one is ${parentNode ?.getRightNode() ?.data}" + ) + + val removeResult = zeroNode.removeSubNode(i) + assertTrue(removeResult ?.first === parentNode) + assertTrue(removeResult.second === expectedNode) + + nodes[i] = zeroNode.addSubNode(i) + assertTrue(nodes[i] != null) + assertTrue(nodes[i] != expectedNode) + assertTrue(nodes[i] ?.data == i) + } + + assertTrue(sourceTreeSize == zeroNode.size()) + + for (i in -rangeRadius .. rangeRadius) { + val expectedNode = nodes.getValue(i) + val foundNode = zeroNode.findNode(i) + + assertTrue(expectedNode === foundNode) + + if (expectedNode === zeroNode) continue + + val parentNode = zeroNode.findParentNode(i) + assertTrue( + parentNode ?.getLeftNode() === expectedNode || parentNode ?.getRightNode() === expectedNode, + "It is expected, that parent node with data ${parentNode ?.data} will be parent of ${expectedNode.data}, but its left subnode is ${parentNode ?.getLeftNode() ?.data} and right one is ${parentNode ?.getRightNode() ?.data}" + ) + } + + var previousData = -rangeRadius - 1 + for (node in zeroNode) { + assertTrue(nodes[node.data] === node) + assertTrue(previousData == node.data - 1) + previousData = node.data + } + + assertTrue(sourceTreeSize == zeroNode.size()) + } + @Test + fun deepInsertIteratorWorking() = runTest { + val zeroNode = SortedBinaryTreeNode(0) + val rangeRadius = 500 + val nodes = mutableMapOf>() + for (i in -rangeRadius .. rangeRadius) { + nodes[i] = zeroNode.addSubNode(i) + } + + var previousData = -rangeRadius - 1 + for (node in zeroNode) { + assertTrue(nodes[node.data] === node) + assertTrue(previousData == node.data - 1) + previousData = node.data + } + assertTrue(previousData == rangeRadius) + } +} \ No newline at end of file From 85f11439e8343f2349fe9b10e855f89714e2cc73 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Wed, 26 Feb 2025 22:01:39 +0600 Subject: [PATCH 03/14] adapt gradle files for new tests --- coroutines/src/commonTest/kotlin/SortedBinaryTreeNodeTests.kt | 4 +++- .../mppComposeJvmJsAndroidLinuxMingwLinuxArm64Project.gradle | 4 ++-- .../mppJvmJsAndroidLinuxMingwLinuxArm64Project.gradle | 4 ++-- gradle/templates/mppJvmJsLinuxMingwProject.gradle | 4 ++-- gradle/templates/mppProjectWithSerializationAndCompose.gradle | 4 ++-- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/coroutines/src/commonTest/kotlin/SortedBinaryTreeNodeTests.kt b/coroutines/src/commonTest/kotlin/SortedBinaryTreeNodeTests.kt index 884c85ea061..b601f75309b 100644 --- a/coroutines/src/commonTest/kotlin/SortedBinaryTreeNodeTests.kt +++ b/coroutines/src/commonTest/kotlin/SortedBinaryTreeNodeTests.kt @@ -8,6 +8,8 @@ import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds +import kotlin.time.measureTime class SortedBinaryTreeNodeTests { @Test @@ -42,7 +44,7 @@ class SortedBinaryTreeNodeTests { } } @Test - fun deepInsertOnWorks() = runTest { + fun deepInsertOnWorks() = runTest(timeout = 320.seconds) { // 320 due to js targets -.- val zeroNode = SortedBinaryTreeNode(0) val rangeRadius = 500 val nodes = mutableMapOf>() diff --git a/gradle/templates/mppComposeJvmJsAndroidLinuxMingwLinuxArm64Project.gradle b/gradle/templates/mppComposeJvmJsAndroidLinuxMingwLinuxArm64Project.gradle index 093baec0738..78457422610 100644 --- a/gradle/templates/mppComposeJvmJsAndroidLinuxMingwLinuxArm64Project.gradle +++ b/gradle/templates/mppComposeJvmJsAndroidLinuxMingwLinuxArm64Project.gradle @@ -15,14 +15,14 @@ kotlin { browser { testTask { useMocha { - timeout = "60000" + timeout = "600000" } } } nodejs { testTask { useMocha { - timeout = "60000" + timeout = "600000" } } } diff --git a/gradle/templates/mppJvmJsAndroidLinuxMingwLinuxArm64Project.gradle b/gradle/templates/mppJvmJsAndroidLinuxMingwLinuxArm64Project.gradle index 0da72807753..01d73caacf5 100644 --- a/gradle/templates/mppJvmJsAndroidLinuxMingwLinuxArm64Project.gradle +++ b/gradle/templates/mppJvmJsAndroidLinuxMingwLinuxArm64Project.gradle @@ -15,14 +15,14 @@ kotlin { browser { testTask { useMocha { - timeout = "60000" + timeout = "600000" } } } nodejs { testTask { useMocha { - timeout = "60000" + timeout = "600000" } } } diff --git a/gradle/templates/mppJvmJsLinuxMingwProject.gradle b/gradle/templates/mppJvmJsLinuxMingwProject.gradle index 82e5a4025b7..86bc347ae69 100644 --- a/gradle/templates/mppJvmJsLinuxMingwProject.gradle +++ b/gradle/templates/mppJvmJsLinuxMingwProject.gradle @@ -15,14 +15,14 @@ kotlin { browser { testTask { useMocha { - timeout = "60000" + timeout = "600000" } } } nodejs { testTask { useMocha { - timeout = "60000" + timeout = "600000" } } } diff --git a/gradle/templates/mppProjectWithSerializationAndCompose.gradle b/gradle/templates/mppProjectWithSerializationAndCompose.gradle index f0cea653e66..7b74e53eb34 100644 --- a/gradle/templates/mppProjectWithSerializationAndCompose.gradle +++ b/gradle/templates/mppProjectWithSerializationAndCompose.gradle @@ -15,14 +15,14 @@ kotlin { browser { testTask { useMocha { - timeout = "60000" + timeout = "600000" } } } nodejs { testTask { useMocha { - timeout = "60000" + timeout = "600000" } } } From ea527b5e910d6d697087fca917bcbac239ec9aaf Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Sat, 1 Mar 2025 16:49:20 +0600 Subject: [PATCH 04/14] start add paged/loading component --- .../common/compose/LoadableComponent.kt | 74 ++++++++++++ .../inmo/micro_utils/pagination/Pagination.kt | 2 + pagination/compose/build.gradle | 20 ++++ .../kotlin/InfinityPagedComponent.kt | 99 ++++++++++++++++ .../src/commonMain/kotlin/PagedComponent.kt | 108 ++++++++++++++++++ settings.gradle | 1 + 6 files changed, 304 insertions(+) create mode 100644 common/compose/src/commonMain/kotlin/dev/inmo/micro_utils/common/compose/LoadableComponent.kt create mode 100644 pagination/compose/build.gradle create mode 100644 pagination/compose/src/commonMain/kotlin/InfinityPagedComponent.kt create mode 100644 pagination/compose/src/commonMain/kotlin/PagedComponent.kt diff --git a/common/compose/src/commonMain/kotlin/dev/inmo/micro_utils/common/compose/LoadableComponent.kt b/common/compose/src/commonMain/kotlin/dev/inmo/micro_utils/common/compose/LoadableComponent.kt new file mode 100644 index 00000000000..8e93701ad1c --- /dev/null +++ b/common/compose/src/commonMain/kotlin/dev/inmo/micro_utils/common/compose/LoadableComponent.kt @@ -0,0 +1,74 @@ +package dev.inmo.micro_utils.common.compose + +import androidx.compose.runtime.* +import dev.inmo.micro_utils.common.Optional +import dev.inmo.micro_utils.common.dataOrThrow +import dev.inmo.micro_utils.common.optional + +class LoadableComponentContext internal constructor( + presetOptional: Optional, +) { + internal val iterationState: MutableState = mutableStateOf(0) + + internal var dataOptional: Optional = if (presetOptional.dataPresented) presetOptional else Optional.absent() + private set + internal val dataState: MutableState> = mutableStateOf(dataOptional) + + fun reload() { + iterationState.value++ + } +} + +/** + * Showing data with ability to reload data + * + * [block] will be shown when [loader] will complete loading. If you want to reload data, just call + * [LoadableComponentContext.reload] + */ +@Composable +fun LoadableComponent( + preload: Optional, + loader: suspend LoadableComponentContext.() -> T, + block: @Composable LoadableComponentContext.(T) -> Unit +) { + val context = remember { LoadableComponentContext(preload) } + + LaunchedEffect(context.iterationState.value) { + context.dataState.value = loader(context).optional + } + + context.dataState.let { + if (it.value.dataPresented) { + context.block(it.value.dataOrThrow(IllegalStateException("Data must be presented, but optional has been changed by some way"))) + } + } +} + +/** + * Showing data with ability to reload data + * + * [block] will be shown when [loader] will complete loading. If you want to reload data, just call + * [LoadableComponentContext.reload] + */ +@Composable +fun LoadableComponent( + preload: T, + loader: suspend LoadableComponentContext.() -> T, + block: @Composable LoadableComponentContext.(T) -> Unit +) { + LoadableComponent(preload.optional, loader, block) +} + +/** + * Showing data with ability to reload data + * + * [block] will be shown when [loader] will complete loading. If you want to reload data, just call + * [LoadableComponentContext.reload] + */ +@Composable +fun LoadableComponent( + loader: suspend LoadableComponentContext.() -> T, + block: @Composable LoadableComponentContext.(T) -> Unit +) { + LoadableComponent(Optional.absent(), loader, block) +} diff --git a/pagination/common/src/commonMain/kotlin/dev/inmo/micro_utils/pagination/Pagination.kt b/pagination/common/src/commonMain/kotlin/dev/inmo/micro_utils/pagination/Pagination.kt index d6d70d24748..519c9c43bfb 100644 --- a/pagination/common/src/commonMain/kotlin/dev/inmo/micro_utils/pagination/Pagination.kt +++ b/pagination/common/src/commonMain/kotlin/dev/inmo/micro_utils/pagination/Pagination.kt @@ -40,6 +40,8 @@ fun Pagination.intersect( inline val Pagination.isFirstPage get() = page == 0 +fun Pagination.firstPage() = if (isFirstPage) this else SimplePagination(0, size) + /** * First number in index of objects. It can be used as offset for databases or other data sources */ diff --git a/pagination/compose/build.gradle b/pagination/compose/build.gradle new file mode 100644 index 00000000000..c3885146e2b --- /dev/null +++ b/pagination/compose/build.gradle @@ -0,0 +1,20 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" + alias(libs.plugins.jb.compose) + alias(libs.plugins.kt.jb.compose) +} + +apply from: "$mppComposeJvmJsAndroidLinuxMingwLinuxArm64Project" + +kotlin { + sourceSets { + commonMain { + dependencies { + api project(":micro_utils.pagination.common") + api project(":micro_utils.common.compose") + } + } + } +} diff --git a/pagination/compose/src/commonMain/kotlin/InfinityPagedComponent.kt b/pagination/compose/src/commonMain/kotlin/InfinityPagedComponent.kt new file mode 100644 index 00000000000..23a4137ae98 --- /dev/null +++ b/pagination/compose/src/commonMain/kotlin/InfinityPagedComponent.kt @@ -0,0 +1,99 @@ +package dev.inmo.micro_utils.pagination.compose + +import androidx.compose.runtime.* +import dev.inmo.micro_utils.common.Optional +import dev.inmo.micro_utils.common.dataOrThrow +import dev.inmo.micro_utils.common.optional +import dev.inmo.micro_utils.pagination.* + +class InfinityPagedComponentContext internal constructor( + preset: List? = null, + initialPage: Int, + size: Int +) { + internal val iterationState: MutableState> = mutableStateOf(0 to SimplePagination(preset ?.page ?: initialPage, preset ?.size ?: size)) + + internal var dataOptional: List? = preset + private set + internal val dataState: MutableState?> = mutableStateOf(dataOptional) + + fun loadNext() { + iterationState.value = iterationState.value.let { + if ((dataState.value as? PaginationResult<*>) ?.isLastPage == true) return + (it.first + 1) to it.second.nextPage() + } + } + fun reload() { + iterationState.value = iterationState.value.let { + (it.first + 1) to (it.second.firstPage()) + } + } +} + +@Composable +internal fun InfinityPagedComponent( + preload: List?, + initialPage: Int, + size: Int, + loader: suspend PagedComponentContext.(Pagination) -> PaginationResult, + block: @Composable PagedComponentContext.(List) -> Unit +) { + val context = remember { InfinityPagedComponentContext(preload, initialPage, size) } + + LaunchedEffect(context.iterationState.value) { + context.dataState.value = loader(context, context.iterationState.value.second) + } + + context.dataState.value ?.let { + context.block() + } +} + +@Composable +fun InfinityPagedComponent( + preload: PaginationResult, + loader: suspend PagedComponentContext.(Pagination) -> PaginationResult, + block: @Composable PagedComponentContext.(PaginationResult) -> Unit +) { + PagedComponent( + preload, + preload.page, + preload.size, + loader, + block + ) +} + +@Composable +fun InfinityPagedComponent( + pageInfo: Pagination, + loader: suspend PagedComponentContext.(Pagination) -> PaginationResult, + block: @Composable PagedComponentContext.(PaginationResult) -> Unit +) { + PagedComponent( + null, + pageInfo.page, + pageInfo.size, + loader, + block + ) +} + +@Composable +fun InfinityPagedComponent( + initialPage: Int, + size: Int, + loader: suspend PagedComponentContext.(Pagination) -> PaginationResult, + block: @Composable PagedComponentContext.(PaginationResult) -> Unit +) { + PagedComponent(null, initialPage, size, loader, block) +} + +@Composable +fun InfinityPagedComponent( + size: Int, + loader: suspend PagedComponentContext.(Pagination) -> PaginationResult, + block: @Composable PagedComponentContext.(PaginationResult) -> Unit +) { + PagedComponent(0, size, loader, block) +} diff --git a/pagination/compose/src/commonMain/kotlin/PagedComponent.kt b/pagination/compose/src/commonMain/kotlin/PagedComponent.kt new file mode 100644 index 00000000000..2f890c0eaf2 --- /dev/null +++ b/pagination/compose/src/commonMain/kotlin/PagedComponent.kt @@ -0,0 +1,108 @@ +package dev.inmo.micro_utils.pagination.compose + +import androidx.compose.runtime.* +import dev.inmo.micro_utils.common.Optional +import dev.inmo.micro_utils.common.dataOrThrow +import dev.inmo.micro_utils.common.optional +import dev.inmo.micro_utils.pagination.* + +class PagedComponentContext internal constructor( + preset: PaginationResult? = null, + initialPage: Int, + size: Int +) { + internal val iterationState: MutableState> = mutableStateOf(0 to SimplePagination(preset ?.page ?: initialPage, preset ?.size ?: size)) + + internal var dataOptional: PaginationResult? = preset + private set + internal val dataState: MutableState?> = mutableStateOf(dataOptional) + + fun loadNext() { + iterationState.value = iterationState.value.let { + if (dataState.value ?.isLastPage == true) return + (it.first + 1) to it.second.nextPage() + } + } + fun loadPrevious() { + iterationState.value = iterationState.value.let { + if (it.second.isFirstPage) return + (it.first - 1) to SimplePagination( + it.second.page - 1, + it.second.size + ) + } + } + fun reload() { + iterationState.value = iterationState.value.let { + it.copy(it.first + 1) + } + } +} + +@Composable +internal fun PagedComponent( + preload: PaginationResult?, + initialPage: Int, + size: Int, + loader: suspend PagedComponentContext.(Pagination) -> PaginationResult, + block: @Composable PagedComponentContext.(PaginationResult) -> Unit +) { + val context = remember { PagedComponentContext(preload, initialPage, size) } + + LaunchedEffect(context.iterationState.value) { + context.dataState.value = loader(context, context.iterationState.value.second) + } + + context.dataState.value ?.let { + context.block(it) + } +} + +@Composable +fun PagedComponent( + preload: PaginationResult, + loader: suspend PagedComponentContext.(Pagination) -> PaginationResult, + block: @Composable PagedComponentContext.(PaginationResult) -> Unit +) { + PagedComponent( + preload, + preload.page, + preload.size, + loader, + block + ) +} + +@Composable +fun PagedComponent( + pageInfo: Pagination, + loader: suspend PagedComponentContext.(Pagination) -> PaginationResult, + block: @Composable PagedComponentContext.(PaginationResult) -> Unit +) { + PagedComponent( + null, + pageInfo.page, + pageInfo.size, + loader, + block + ) +} + +@Composable +fun PagedComponent( + initialPage: Int, + size: Int, + loader: suspend PagedComponentContext.(Pagination) -> PaginationResult, + block: @Composable PagedComponentContext.(PaginationResult) -> Unit +) { + PagedComponent(null, initialPage, size, loader, block) +} + +@Composable +fun PagedComponent( + size: Int, + loader: suspend PagedComponentContext.(Pagination) -> PaginationResult, + block: @Composable PagedComponentContext.(PaginationResult) -> Unit +) { + PagedComponent(0, size, loader, block) +} diff --git a/settings.gradle b/settings.gradle index 6379141d8cb..a8e6a646598 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,6 +11,7 @@ String[] includes = [ ":koin:generator:test", ":selector:common", ":pagination:common", + ":pagination:compose", ":pagination:exposed", ":pagination:ktor:common", ":pagination:ktor:server", From 0a5cfaba181de8558a87085b99bc3eeab4912a7e Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Sat, 1 Mar 2025 22:04:41 +0600 Subject: [PATCH 05/14] complete InfinityPagedComponentContext --- .../kotlin/InfinityPagedComponent.kt | 41 +++++-------------- 1 file changed, 10 insertions(+), 31 deletions(-) diff --git a/pagination/compose/src/commonMain/kotlin/InfinityPagedComponent.kt b/pagination/compose/src/commonMain/kotlin/InfinityPagedComponent.kt index 23a4137ae98..c0e1766b1ff 100644 --- a/pagination/compose/src/commonMain/kotlin/InfinityPagedComponent.kt +++ b/pagination/compose/src/commonMain/kotlin/InfinityPagedComponent.kt @@ -1,21 +1,15 @@ package dev.inmo.micro_utils.pagination.compose import androidx.compose.runtime.* -import dev.inmo.micro_utils.common.Optional -import dev.inmo.micro_utils.common.dataOrThrow -import dev.inmo.micro_utils.common.optional import dev.inmo.micro_utils.pagination.* class InfinityPagedComponentContext internal constructor( - preset: List? = null, - initialPage: Int, + page: Int, size: Int ) { - internal val iterationState: MutableState> = mutableStateOf(0 to SimplePagination(preset ?.page ?: initialPage, preset ?.size ?: size)) + internal val iterationState: MutableState> = mutableStateOf(0 to SimplePagination(page, size)) - internal var dataOptional: List? = preset - private set - internal val dataState: MutableState?> = mutableStateOf(dataOptional) + internal val dataState: MutableState?> = mutableStateOf(null) fun loadNext() { iterationState.value = iterationState.value.let { @@ -24,6 +18,7 @@ class InfinityPagedComponentContext internal constructor( } } fun reload() { + dataState.value = null iterationState.value = iterationState.value.let { (it.first + 1) to (it.second.firstPage()) } @@ -32,38 +27,22 @@ class InfinityPagedComponentContext internal constructor( @Composable internal fun InfinityPagedComponent( - preload: List?, - initialPage: Int, + page: Int, size: Int, - loader: suspend PagedComponentContext.(Pagination) -> PaginationResult, - block: @Composable PagedComponentContext.(List) -> Unit + loader: suspend InfinityPagedComponentContext.(Pagination) -> PaginationResult, + block: @Composable InfinityPagedComponentContext.(List?) -> Unit ) { - val context = remember { InfinityPagedComponentContext(preload, initialPage, size) } + val context = remember { InfinityPagedComponentContext(page, size) } LaunchedEffect(context.iterationState.value) { - context.dataState.value = loader(context, context.iterationState.value.second) + context.dataState.value = (context.dataState.value ?: emptyList()) + loader(context, context.iterationState.value.second).results } context.dataState.value ?.let { - context.block() + context.block(context.dataState.value) } } -@Composable -fun InfinityPagedComponent( - preload: PaginationResult, - loader: suspend PagedComponentContext.(Pagination) -> PaginationResult, - block: @Composable PagedComponentContext.(PaginationResult) -> Unit -) { - PagedComponent( - preload, - preload.page, - preload.size, - loader, - block - ) -} - @Composable fun InfinityPagedComponent( pageInfo: Pagination, From 3b7dde3cb1e94a00bef83978dd622e6c5f047e45 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Sat, 1 Mar 2025 22:16:48 +0600 Subject: [PATCH 06/14] add KDocs for InfinityPagedComponentContext and PagedComponent --- .../kotlin/InfinityPagedComponent.kt | 52 +++++++++++++- .../src/commonMain/kotlin/PagedComponent.kt | 70 ++++++++++++++++++- 2 files changed, 119 insertions(+), 3 deletions(-) diff --git a/pagination/compose/src/commonMain/kotlin/InfinityPagedComponent.kt b/pagination/compose/src/commonMain/kotlin/InfinityPagedComponent.kt index c0e1766b1ff..9a44bb7a52c 100644 --- a/pagination/compose/src/commonMain/kotlin/InfinityPagedComponent.kt +++ b/pagination/compose/src/commonMain/kotlin/InfinityPagedComponent.kt @@ -3,20 +3,36 @@ package dev.inmo.micro_utils.pagination.compose import androidx.compose.runtime.* import dev.inmo.micro_utils.pagination.* +/** + * Context for managing infinite pagination in a Compose UI. + * + * @param T The type of the data being paginated. + * @property iterationState Holds the current pagination state and iteration count. + * @property dataState Stores the loaded data, initially null. + * @constructor Internal constructor to initialize pagination. + * @param page Initial page number. + * @param size Number of items per page. + */ class InfinityPagedComponentContext internal constructor( page: Int, size: Int ) { internal val iterationState: MutableState> = mutableStateOf(0 to SimplePagination(page, size)) - internal val dataState: MutableState?> = mutableStateOf(null) + /** + * Loads the next page of data. If the current page is the last one, the function returns early. + */ fun loadNext() { iterationState.value = iterationState.value.let { if ((dataState.value as? PaginationResult<*>) ?.isLastPage == true) return (it.first + 1) to it.second.nextPage() } } + + /** + * Reloads the pagination from the first page, clearing previously loaded data. + */ fun reload() { dataState.value = null iterationState.value = iterationState.value.let { @@ -25,6 +41,15 @@ class InfinityPagedComponentContext internal constructor( } } +/** + * Composable function for managing an infinitely paged component. + * + * @param T The type of the paginated data. + * @param page Initial page number. + * @param size Number of items per page. + * @param loader Suspended function that loads paginated data. + * @param block Composable function that renders the UI with the loaded data. + */ @Composable internal fun InfinityPagedComponent( page: Int, @@ -43,6 +68,14 @@ internal fun InfinityPagedComponent( } } +/** + * Overloaded composable function for an infinitely paged component. + * + * @param T The type of the paginated data. + * @param pageInfo Initial pagination information. + * @param loader Suspended function that loads paginated data. + * @param block Composable function that renders the UI with the loaded data. + */ @Composable fun InfinityPagedComponent( pageInfo: Pagination, @@ -58,6 +91,15 @@ fun InfinityPagedComponent( ) } +/** + * Overloaded composable function for an infinitely paged component. + * + * @param T The type of the paginated data. + * @param initialPage Initial page number. + * @param size Number of items per page. + * @param loader Suspended function that loads paginated data. + * @param block Composable function that renders the UI with the loaded data. + */ @Composable fun InfinityPagedComponent( initialPage: Int, @@ -68,6 +110,14 @@ fun InfinityPagedComponent( PagedComponent(null, initialPage, size, loader, block) } +/** + * Overloaded composable function for an infinitely paged component. + * + * @param T The type of the paginated data. + * @param size Number of items per page. + * @param loader Suspended function that loads paginated data. + * @param block Composable function that renders the UI with the loaded data. + */ @Composable fun InfinityPagedComponent( size: Int, diff --git a/pagination/compose/src/commonMain/kotlin/PagedComponent.kt b/pagination/compose/src/commonMain/kotlin/PagedComponent.kt index 2f890c0eaf2..88127bf0563 100644 --- a/pagination/compose/src/commonMain/kotlin/PagedComponent.kt +++ b/pagination/compose/src/commonMain/kotlin/PagedComponent.kt @@ -6,23 +6,42 @@ import dev.inmo.micro_utils.common.dataOrThrow import dev.inmo.micro_utils.common.optional import dev.inmo.micro_utils.pagination.* +/** + * Context for managing paginated data in a Compose UI. + * + * @param T The type of data being paginated. + * @property iterationState Holds the current pagination state and iteration count. + * @property dataOptional Stores the optional preloaded pagination result. + * @property dataState Stores the current pagination result. + * @constructor Internal constructor for setting up pagination. + * @param preset Optional preset pagination result. + * @param initialPage Initial page number. + * @param size Number of items per page. + */ class PagedComponentContext internal constructor( preset: PaginationResult? = null, initialPage: Int, size: Int ) { - internal val iterationState: MutableState> = mutableStateOf(0 to SimplePagination(preset ?.page ?: initialPage, preset ?.size ?: size)) - + internal val iterationState: MutableState> = mutableStateOf(0 to SimplePagination(preset?.page ?: initialPage, preset?.size ?: size)) + internal var dataOptional: PaginationResult? = preset private set internal val dataState: MutableState?> = mutableStateOf(dataOptional) + /** + * Loads the next page of data. If the last page is reached, this function returns early. + */ fun loadNext() { iterationState.value = iterationState.value.let { if (dataState.value ?.isLastPage == true) return (it.first + 1) to it.second.nextPage() } } + + /** + * Loads the previous page of data if available. + */ fun loadPrevious() { iterationState.value = iterationState.value.let { if (it.second.isFirstPage) return @@ -32,6 +51,10 @@ class PagedComponentContext internal constructor( ) } } + + /** + * Reloads the current page, refreshing the data. + */ fun reload() { iterationState.value = iterationState.value.let { it.copy(it.first + 1) @@ -39,6 +62,16 @@ class PagedComponentContext internal constructor( } } +/** + * Composable function for paginated data displaying in a Compose UI. + * + * @param T The type of paginated data. + * @param preload Optional preloaded pagination result. + * @param initialPage Initial page number. + * @param size Number of items per page. + * @param loader Suspended function that loads paginated data. + * @param block Composable function that renders the UI with the loaded data. + */ @Composable internal fun PagedComponent( preload: PaginationResult?, @@ -58,6 +91,14 @@ internal fun PagedComponent( } } +/** + * Overloaded composable function for paginated components with preloaded data. + * + * @param T The type of paginated data. + * @param preload Preloaded pagination result. + * @param loader Suspended function that loads paginated data. + * @param block Composable function that renders the UI with the loaded data. + */ @Composable fun PagedComponent( preload: PaginationResult, @@ -73,6 +114,14 @@ fun PagedComponent( ) } +/** + * Overloaded composable function for paginated components with pagination info. + * + * @param T The type of paginated data. + * @param pageInfo Initial pagination information. + * @param loader Suspended function that loads paginated data. + * @param block Composable function that renders the UI with the loaded data. + */ @Composable fun PagedComponent( pageInfo: Pagination, @@ -88,6 +137,15 @@ fun PagedComponent( ) } +/** + * Overloaded composable function for paginated components with an initial page. + * + * @param T The type of paginated data. + * @param initialPage Initial page number. + * @param size Number of items per page. + * @param loader Suspended function that loads paginated data. + * @param block Composable function that renders the UI with the loaded data. + */ @Composable fun PagedComponent( initialPage: Int, @@ -98,6 +156,14 @@ fun PagedComponent( PagedComponent(null, initialPage, size, loader, block) } +/** + * Overloaded composable function for paginated components with only a size parameter. + * + * @param T The type of paginated data. + * @param size Number of items per page. + * @param loader Suspended function that loads paginated data. + * @param block Composable function that renders the UI with the loaded data. + */ @Composable fun PagedComponent( size: Int, From e70d34d91a3968824e6fdfaa6ac4831979eac0b0 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Sun, 2 Mar 2025 23:06:42 +0600 Subject: [PATCH 07/14] add tests for paged loading components --- .../jvmTest/kotlin/LoadableComponentTests.kt | 42 ++++++++++++ .../kotlin/InfinityPagedComponent.kt | 44 +++++-------- .../kotlin/InfinityPagedComponentTests.kt | 47 ++++++++++++++ .../src/jvmTest/kotlin/PagedComponentTests.kt | 64 +++++++++++++++++++ 4 files changed, 169 insertions(+), 28 deletions(-) create mode 100644 common/compose/src/jvmTest/kotlin/LoadableComponentTests.kt create mode 100644 pagination/compose/src/jvmTest/kotlin/InfinityPagedComponentTests.kt create mode 100644 pagination/compose/src/jvmTest/kotlin/PagedComponentTests.kt diff --git a/common/compose/src/jvmTest/kotlin/LoadableComponentTests.kt b/common/compose/src/jvmTest/kotlin/LoadableComponentTests.kt new file mode 100644 index 00000000000..dc33811f442 --- /dev/null +++ b/common/compose/src/jvmTest/kotlin/LoadableComponentTests.kt @@ -0,0 +1,42 @@ +import androidx.compose.runtime.remember +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.runComposeUiTest +import dev.inmo.micro_utils.common.compose.LoadableComponent +import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull +import org.jetbrains.annotations.TestOnly +import kotlin.test.Test +import kotlin.test.assertTrue + +class LoadableComponentTests { + @OptIn(ExperimentalTestApi::class) + @Test + @TestOnly + fun testSimpleLoad() = runComposeUiTest { + val loadingFlow = SpecialMutableStateFlow(0) + val loadedFlow = SpecialMutableStateFlow(0) + setContent { + LoadableComponent({ + loadingFlow.filter { it == 1 }.first() + }) { + assert(dataState.value.data == 1) + remember { + loadedFlow.value = 2 + } + } + } + + waitForIdle() + + assertTrue(loadedFlow.value == 0) + + loadingFlow.value = 1 + + waitForIdle() + + assertTrue(loadedFlow.value == 2) + } +} \ No newline at end of file diff --git a/pagination/compose/src/commonMain/kotlin/InfinityPagedComponent.kt b/pagination/compose/src/commonMain/kotlin/InfinityPagedComponent.kt index 9a44bb7a52c..1af1fe28ab6 100644 --- a/pagination/compose/src/commonMain/kotlin/InfinityPagedComponent.kt +++ b/pagination/compose/src/commonMain/kotlin/InfinityPagedComponent.kt @@ -19,11 +19,14 @@ class InfinityPagedComponentContext internal constructor( ) { internal val iterationState: MutableState> = mutableStateOf(0 to SimplePagination(page, size)) internal val dataState: MutableState?> = mutableStateOf(null) + internal var lastPageLoaded = false /** * Loads the next page of data. If the current page is the last one, the function returns early. */ fun loadNext() { + if (lastPageLoaded) return + iterationState.value = iterationState.value.let { if ((dataState.value as? PaginationResult<*>) ?.isLastPage == true) return (it.first + 1) to it.second.nextPage() @@ -35,6 +38,7 @@ class InfinityPagedComponentContext internal constructor( */ fun reload() { dataState.value = null + lastPageLoaded = false iterationState.value = iterationState.value.let { (it.first + 1) to (it.second.firstPage()) } @@ -59,8 +63,12 @@ internal fun InfinityPagedComponent( ) { val context = remember { InfinityPagedComponentContext(page, size) } - LaunchedEffect(context.iterationState.value) { - context.dataState.value = (context.dataState.value ?: emptyList()) + loader(context, context.iterationState.value.second).results + LaunchedEffect(context.iterationState.value.first) { + val paginationResult = loader(context, context.iterationState.value.second) + if (paginationResult.isLastPage) { + context.lastPageLoaded = true + } + context.dataState.value = (context.dataState.value ?: emptyList()) + paginationResult.results } context.dataState.value ?.let { @@ -79,11 +87,10 @@ internal fun InfinityPagedComponent( @Composable fun InfinityPagedComponent( pageInfo: Pagination, - loader: suspend PagedComponentContext.(Pagination) -> PaginationResult, - block: @Composable PagedComponentContext.(PaginationResult) -> Unit + loader: suspend InfinityPagedComponentContext.(Pagination) -> PaginationResult, + block: @Composable InfinityPagedComponentContext.(List?) -> Unit ) { - PagedComponent( - null, + InfinityPagedComponent( pageInfo.page, pageInfo.size, loader, @@ -91,25 +98,6 @@ fun InfinityPagedComponent( ) } -/** - * Overloaded composable function for an infinitely paged component. - * - * @param T The type of the paginated data. - * @param initialPage Initial page number. - * @param size Number of items per page. - * @param loader Suspended function that loads paginated data. - * @param block Composable function that renders the UI with the loaded data. - */ -@Composable -fun InfinityPagedComponent( - initialPage: Int, - size: Int, - loader: suspend PagedComponentContext.(Pagination) -> PaginationResult, - block: @Composable PagedComponentContext.(PaginationResult) -> Unit -) { - PagedComponent(null, initialPage, size, loader, block) -} - /** * Overloaded composable function for an infinitely paged component. * @@ -121,8 +109,8 @@ fun InfinityPagedComponent( @Composable fun InfinityPagedComponent( size: Int, - loader: suspend PagedComponentContext.(Pagination) -> PaginationResult, - block: @Composable PagedComponentContext.(PaginationResult) -> Unit + loader: suspend InfinityPagedComponentContext.(Pagination) -> PaginationResult, + block: @Composable InfinityPagedComponentContext.(List?) -> Unit ) { - PagedComponent(0, size, loader, block) + InfinityPagedComponent(0, size, loader, block) } diff --git a/pagination/compose/src/jvmTest/kotlin/InfinityPagedComponentTests.kt b/pagination/compose/src/jvmTest/kotlin/InfinityPagedComponentTests.kt new file mode 100644 index 00000000000..9844e0d400b --- /dev/null +++ b/pagination/compose/src/jvmTest/kotlin/InfinityPagedComponentTests.kt @@ -0,0 +1,47 @@ +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.runComposeUiTest +import dev.inmo.micro_utils.pagination.* +import dev.inmo.micro_utils.pagination.compose.InfinityPagedComponent +import dev.inmo.micro_utils.pagination.compose.PagedComponent +import org.jetbrains.annotations.TestOnly +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +class InfinityPagedComponentTests { + @OptIn(ExperimentalTestApi::class) + @Test + @TestOnly + fun testSimpleLoad() = runComposeUiTest { + var expectedList = listOf() + setContent { + InfinityPagedComponent( + size = 1, + loader = { + PaginationResult( + page = it.page, + size = it.size, + results = (it.firstIndex .. it.lastIndex).toList(), + objectsNumber = 3 + ).also { + expectedList += it.results + } + } + ) { + assertEquals(expectedList, it) + + LaunchedEffect(it ?.size) { + loadNext() + } + } + } + + waitForIdle() + + assertContentEquals( + listOf(0, 1, 2), + expectedList + ) + } +} \ No newline at end of file diff --git a/pagination/compose/src/jvmTest/kotlin/PagedComponentTests.kt b/pagination/compose/src/jvmTest/kotlin/PagedComponentTests.kt new file mode 100644 index 00000000000..375db11cdef --- /dev/null +++ b/pagination/compose/src/jvmTest/kotlin/PagedComponentTests.kt @@ -0,0 +1,64 @@ +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.runComposeUiTest +import dev.inmo.micro_utils.pagination.* +import dev.inmo.micro_utils.pagination.compose.PagedComponent +import org.jetbrains.annotations.TestOnly +import kotlin.test.Test +import kotlin.test.assertEquals + +class PagedComponentTests { + @OptIn(ExperimentalTestApi::class) + @Test + @TestOnly + fun testSimpleLoad() = runComposeUiTest { + var expectedPage = PaginationResult( + page = 0, + size = 1, + results = listOf(0), + objectsNumber = 3 + ) + var previousPage = expectedPage + setContent { + PagedComponent( + initialPage = 0, + size = 1, + loader = { + previousPage = expectedPage + expectedPage = PaginationResult( + page = it.page, + size = it.size, + results = (it.firstIndex .. it.lastIndex).toList(), + objectsNumber = 3 + ) + expectedPage + } + ) { + assertEquals(expectedPage, it) + assertEquals(expectedPage.results, it.results) + + if (it.isLastPage || it.page < previousPage.page) { + if (it.isFirstPage) { + // do nothing - end of test + } else { + loadPrevious() + } + } else { + loadNext() + } + } + } + + waitForIdle() + + assertEquals( + PaginationResult( + page = 0, + size = 1, + results = listOf(0), + objectsNumber = 3 + ), + expectedPage + ) + } +} \ No newline at end of file From 3bf2ed5168195cee53645cd4c6be3dd8c2dd4844 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Mon, 3 Mar 2025 08:57:40 +0600 Subject: [PATCH 08/14] fix of InfinityPagedComponent --- pagination/compose/build.gradle | 1 + .../src/commonMain/kotlin/InfinityPagedComponent.kt | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/pagination/compose/build.gradle b/pagination/compose/build.gradle index c3885146e2b..7087b3e456d 100644 --- a/pagination/compose/build.gradle +++ b/pagination/compose/build.gradle @@ -14,6 +14,7 @@ kotlin { dependencies { api project(":micro_utils.pagination.common") api project(":micro_utils.common.compose") + api libs.kt.coroutines } } } diff --git a/pagination/compose/src/commonMain/kotlin/InfinityPagedComponent.kt b/pagination/compose/src/commonMain/kotlin/InfinityPagedComponent.kt index 1af1fe28ab6..44a0259252f 100644 --- a/pagination/compose/src/commonMain/kotlin/InfinityPagedComponent.kt +++ b/pagination/compose/src/commonMain/kotlin/InfinityPagedComponent.kt @@ -17,7 +17,8 @@ class InfinityPagedComponentContext internal constructor( page: Int, size: Int ) { - internal val iterationState: MutableState> = mutableStateOf(0 to SimplePagination(page, size)) + internal val startPage = SimplePagination(page, size) + internal val iterationState: MutableState> = mutableStateOf(0 to null) internal val dataState: MutableState?> = mutableStateOf(null) internal var lastPageLoaded = false @@ -26,10 +27,11 @@ class InfinityPagedComponentContext internal constructor( */ fun loadNext() { if (lastPageLoaded) return + if (iterationState.value.second is SimplePagination) return // Data loading has been inited but not loaded yet iterationState.value = iterationState.value.let { - if ((dataState.value as? PaginationResult<*>) ?.isLastPage == true) return - (it.first + 1) to it.second.nextPage() + if ((it.second as? PaginationResult<*>) ?.isLastPage == true) return + (it.first + 1) to (it.second ?: startPage).nextPage() } } @@ -40,7 +42,7 @@ class InfinityPagedComponentContext internal constructor( dataState.value = null lastPageLoaded = false iterationState.value = iterationState.value.let { - (it.first + 1) to (it.second.firstPage()) + (it.first + 1) to null } } } @@ -64,10 +66,11 @@ internal fun InfinityPagedComponent( val context = remember { InfinityPagedComponentContext(page, size) } LaunchedEffect(context.iterationState.value.first) { - val paginationResult = loader(context, context.iterationState.value.second) + val paginationResult = loader(context, context.iterationState.value.second ?: context.startPage) if (paginationResult.isLastPage) { context.lastPageLoaded = true } + context.iterationState.value = context.iterationState.value.copy(second = paginationResult) context.dataState.value = (context.dataState.value ?: emptyList()) + paginationResult.results } From 8a059cc26d19d6b68696eec5bbd093afc9d190f9 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Mon, 3 Mar 2025 12:21:04 +0600 Subject: [PATCH 09/14] fix of build --- common/compose/build.gradle | 1 + pagination/compose/build.gradle | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/common/compose/build.gradle b/common/compose/build.gradle index df504ddfb76..eb6e9137d55 100644 --- a/common/compose/build.gradle +++ b/common/compose/build.gradle @@ -13,6 +13,7 @@ kotlin { commonMain { dependencies { api project(":micro_utils.common") + api libs.kt.coroutines } } } diff --git a/pagination/compose/build.gradle b/pagination/compose/build.gradle index 7087b3e456d..df1a5bb3725 100644 --- a/pagination/compose/build.gradle +++ b/pagination/compose/build.gradle @@ -14,7 +14,7 @@ kotlin { dependencies { api project(":micro_utils.pagination.common") api project(":micro_utils.common.compose") - api libs.kt.coroutines + api project(":micro_utils.coroutines") } } } From 51ec46bbd76b5bf4c14f6c034350b0379080c6b0 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Mon, 3 Mar 2025 13:40:33 +0600 Subject: [PATCH 10/14] update timeout for deepInsertOnWorks --- coroutines/src/commonTest/kotlin/SortedBinaryTreeNodeTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coroutines/src/commonTest/kotlin/SortedBinaryTreeNodeTests.kt b/coroutines/src/commonTest/kotlin/SortedBinaryTreeNodeTests.kt index b601f75309b..148445e886b 100644 --- a/coroutines/src/commonTest/kotlin/SortedBinaryTreeNodeTests.kt +++ b/coroutines/src/commonTest/kotlin/SortedBinaryTreeNodeTests.kt @@ -44,7 +44,7 @@ class SortedBinaryTreeNodeTests { } } @Test - fun deepInsertOnWorks() = runTest(timeout = 320.seconds) { // 320 due to js targets -.- + fun deepInsertOnWorks() = runTest(timeout = 440.seconds) { // 440 due to js targets -.- val zeroNode = SortedBinaryTreeNode(0) val rangeRadius = 500 val nodes = mutableMapOf>() From 339483c8a3f75f21ff43fa09a7b53400989d5806 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Mon, 3 Mar 2025 14:42:05 +0600 Subject: [PATCH 11/14] disable hard test of binary trees for js --- .../SortedBinaryTreeNodeTests.android.kt | 2 + .../kotlin/SortedBinaryTreeNodeTests.kt | 40 ++++++++++++++++++- .../kotlin/SortedBinaryTreeNodeTests.js.kt | 2 + .../kotlin/SortedBinaryTreeNodeTests.jvm.kt | 2 + .../SortedBinaryTreeNodeTests.linuxArm64.kt | 2 + .../SortedBinaryTreeNodeTests.linuxX64.kt | 2 + .../SortedBinaryTreeNodeTests.mingwX64.kt | 2 + ...sAndroidLinuxMingwLinuxArm64Project.gradle | 4 +- ...sAndroidLinuxMingwLinuxArm64Project.gradle | 4 +- .../mppJvmJsLinuxMingwProject.gradle | 4 +- ...pProjectWithSerializationAndCompose.gradle | 4 +- 11 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 coroutines/src/androidUnitTest/kotlin/SortedBinaryTreeNodeTests.android.kt create mode 100644 coroutines/src/jsTest/kotlin/SortedBinaryTreeNodeTests.js.kt create mode 100644 coroutines/src/jvmTest/kotlin/SortedBinaryTreeNodeTests.jvm.kt create mode 100644 coroutines/src/linuxArm64Test/kotlin/SortedBinaryTreeNodeTests.linuxArm64.kt create mode 100644 coroutines/src/linuxX64Test/kotlin/SortedBinaryTreeNodeTests.linuxX64.kt create mode 100644 coroutines/src/mingwX64Test/kotlin/SortedBinaryTreeNodeTests.mingwX64.kt diff --git a/coroutines/src/androidUnitTest/kotlin/SortedBinaryTreeNodeTests.android.kt b/coroutines/src/androidUnitTest/kotlin/SortedBinaryTreeNodeTests.android.kt new file mode 100644 index 00000000000..4ab57abfee1 --- /dev/null +++ b/coroutines/src/androidUnitTest/kotlin/SortedBinaryTreeNodeTests.android.kt @@ -0,0 +1,2 @@ +actual val AllowDeepReInsertOnWorksTest: Boolean + get() = true \ No newline at end of file diff --git a/coroutines/src/commonTest/kotlin/SortedBinaryTreeNodeTests.kt b/coroutines/src/commonTest/kotlin/SortedBinaryTreeNodeTests.kt index 148445e886b..6dfb0156d15 100644 --- a/coroutines/src/commonTest/kotlin/SortedBinaryTreeNodeTests.kt +++ b/coroutines/src/commonTest/kotlin/SortedBinaryTreeNodeTests.kt @@ -11,6 +11,8 @@ import kotlin.test.assertTrue import kotlin.time.Duration.Companion.seconds import kotlin.time.measureTime +expect val AllowDeepReInsertOnWorksTest: Boolean + class SortedBinaryTreeNodeTests { @Test fun insertOnZeroLevelWorks() = runTest { @@ -44,7 +46,8 @@ class SortedBinaryTreeNodeTests { } } @Test - fun deepInsertOnWorks() = runTest(timeout = 440.seconds) { // 440 due to js targets -.- + fun deepReInsertOnWorks() = runTest(timeout = 240.seconds) { + if (AllowDeepReInsertOnWorksTest == false) return@runTest val zeroNode = SortedBinaryTreeNode(0) val rangeRadius = 500 val nodes = mutableMapOf>() @@ -121,6 +124,41 @@ class SortedBinaryTreeNodeTests { assertTrue(sourceTreeSize == zeroNode.size()) } @Test + fun deepInsertOnWorks() = runTest(timeout = 440.seconds) { // 440 due to js targets -.- + val zeroNode = SortedBinaryTreeNode(0) + val rangeRadius = 500 + val nodes = mutableMapOf>() + for (i in -rangeRadius .. rangeRadius) { + nodes[i] = zeroNode.addSubNode(i) + } + + for (i in -rangeRadius .. rangeRadius) { + val expectedNode = nodes.getValue(i) + val foundNode = zeroNode.findNode(i) + + assertTrue(expectedNode === foundNode) + + if (expectedNode === zeroNode) continue + + val parentNode = zeroNode.findParentNode(i) + assertTrue( + parentNode ?.getLeftNode() === expectedNode || parentNode ?.getRightNode() === expectedNode, + "It is expected, that parent node with data ${parentNode ?.data} will be parent of ${expectedNode.data}, but its left subnode is ${parentNode ?.getLeftNode() ?.data} and right one is ${parentNode ?.getRightNode() ?.data}" + ) + } + + val sourceTreeSize = zeroNode.size() + + var previousData = -rangeRadius - 1 + for (node in zeroNode) { + assertTrue(nodes[node.data] === node) + assertTrue(previousData == node.data - 1) + previousData = node.data + } + + assertTrue(sourceTreeSize == zeroNode.size()) + } + @Test fun deepInsertIteratorWorking() = runTest { val zeroNode = SortedBinaryTreeNode(0) val rangeRadius = 500 diff --git a/coroutines/src/jsTest/kotlin/SortedBinaryTreeNodeTests.js.kt b/coroutines/src/jsTest/kotlin/SortedBinaryTreeNodeTests.js.kt new file mode 100644 index 00000000000..3b385510216 --- /dev/null +++ b/coroutines/src/jsTest/kotlin/SortedBinaryTreeNodeTests.js.kt @@ -0,0 +1,2 @@ +actual val AllowDeepReInsertOnWorksTest: Boolean + get() = false \ No newline at end of file diff --git a/coroutines/src/jvmTest/kotlin/SortedBinaryTreeNodeTests.jvm.kt b/coroutines/src/jvmTest/kotlin/SortedBinaryTreeNodeTests.jvm.kt new file mode 100644 index 00000000000..4ab57abfee1 --- /dev/null +++ b/coroutines/src/jvmTest/kotlin/SortedBinaryTreeNodeTests.jvm.kt @@ -0,0 +1,2 @@ +actual val AllowDeepReInsertOnWorksTest: Boolean + get() = true \ No newline at end of file diff --git a/coroutines/src/linuxArm64Test/kotlin/SortedBinaryTreeNodeTests.linuxArm64.kt b/coroutines/src/linuxArm64Test/kotlin/SortedBinaryTreeNodeTests.linuxArm64.kt new file mode 100644 index 00000000000..4ab57abfee1 --- /dev/null +++ b/coroutines/src/linuxArm64Test/kotlin/SortedBinaryTreeNodeTests.linuxArm64.kt @@ -0,0 +1,2 @@ +actual val AllowDeepReInsertOnWorksTest: Boolean + get() = true \ No newline at end of file diff --git a/coroutines/src/linuxX64Test/kotlin/SortedBinaryTreeNodeTests.linuxX64.kt b/coroutines/src/linuxX64Test/kotlin/SortedBinaryTreeNodeTests.linuxX64.kt new file mode 100644 index 00000000000..4ab57abfee1 --- /dev/null +++ b/coroutines/src/linuxX64Test/kotlin/SortedBinaryTreeNodeTests.linuxX64.kt @@ -0,0 +1,2 @@ +actual val AllowDeepReInsertOnWorksTest: Boolean + get() = true \ No newline at end of file diff --git a/coroutines/src/mingwX64Test/kotlin/SortedBinaryTreeNodeTests.mingwX64.kt b/coroutines/src/mingwX64Test/kotlin/SortedBinaryTreeNodeTests.mingwX64.kt new file mode 100644 index 00000000000..4ab57abfee1 --- /dev/null +++ b/coroutines/src/mingwX64Test/kotlin/SortedBinaryTreeNodeTests.mingwX64.kt @@ -0,0 +1,2 @@ +actual val AllowDeepReInsertOnWorksTest: Boolean + get() = true \ No newline at end of file diff --git a/gradle/templates/mppComposeJvmJsAndroidLinuxMingwLinuxArm64Project.gradle b/gradle/templates/mppComposeJvmJsAndroidLinuxMingwLinuxArm64Project.gradle index 78457422610..5e2606e4b50 100644 --- a/gradle/templates/mppComposeJvmJsAndroidLinuxMingwLinuxArm64Project.gradle +++ b/gradle/templates/mppComposeJvmJsAndroidLinuxMingwLinuxArm64Project.gradle @@ -15,14 +15,14 @@ kotlin { browser { testTask { useMocha { - timeout = "600000" + timeout = "240000" } } } nodejs { testTask { useMocha { - timeout = "600000" + timeout = "240000" } } } diff --git a/gradle/templates/mppJvmJsAndroidLinuxMingwLinuxArm64Project.gradle b/gradle/templates/mppJvmJsAndroidLinuxMingwLinuxArm64Project.gradle index 01d73caacf5..574b69a0107 100644 --- a/gradle/templates/mppJvmJsAndroidLinuxMingwLinuxArm64Project.gradle +++ b/gradle/templates/mppJvmJsAndroidLinuxMingwLinuxArm64Project.gradle @@ -15,14 +15,14 @@ kotlin { browser { testTask { useMocha { - timeout = "600000" + timeout = "240000" } } } nodejs { testTask { useMocha { - timeout = "600000" + timeout = "240000" } } } diff --git a/gradle/templates/mppJvmJsLinuxMingwProject.gradle b/gradle/templates/mppJvmJsLinuxMingwProject.gradle index 86bc347ae69..b62c12dc548 100644 --- a/gradle/templates/mppJvmJsLinuxMingwProject.gradle +++ b/gradle/templates/mppJvmJsLinuxMingwProject.gradle @@ -15,14 +15,14 @@ kotlin { browser { testTask { useMocha { - timeout = "600000" + timeout = "240000" } } } nodejs { testTask { useMocha { - timeout = "600000" + timeout = "240000" } } } diff --git a/gradle/templates/mppProjectWithSerializationAndCompose.gradle b/gradle/templates/mppProjectWithSerializationAndCompose.gradle index 7b74e53eb34..0a5c853bf56 100644 --- a/gradle/templates/mppProjectWithSerializationAndCompose.gradle +++ b/gradle/templates/mppProjectWithSerializationAndCompose.gradle @@ -15,14 +15,14 @@ kotlin { browser { testTask { useMocha { - timeout = "600000" + timeout = "240000" } } } nodejs { testTask { useMocha { - timeout = "600000" + timeout = "240000" } } } From 659d3b6fa561c79e0637a048a5194ca49642dc03 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Mon, 3 Mar 2025 14:53:49 +0600 Subject: [PATCH 12/14] update dependencies and fill changelog --- CHANGELOG.md | 9 +++++++++ gradle/libs.versions.toml | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9c3a0ff0e5..5def8df5279 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,17 @@ ## 0.24.8 +* `Versions`: + * `Ktor`: `3.1.0` -> `3.1.1` + * `KSP`: `2.1.10-1.0.30` -> `2.1.10-1.0.31` +* `Common`: + * `Compose`: + * Add component `LoadableComponent` * `Coroutines`: * Add `SortedBinaryTreeNode` +* `Pagination`: + * `Compose`: + * Add components `PagedComponent` and `InfinityPagedComponent` ## 0.24.7 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 744238c128f..2135de11fce 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ sqlite = "3.49.1.0" korlibs = "5.4.0" uuid = "0.8.4" -ktor = "3.1.0" +ktor = "3.1.1" gh-release = "2.5.2" @@ -23,7 +23,7 @@ koin = "4.0.2" okio = "3.10.2" -ksp = "2.1.10-1.0.30" +ksp = "2.1.10-1.0.31" kotlin-poet = "1.18.1" versions = "0.51.0" From 24657b43beb400f997bb83a3940d2a63289820c0 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Mon, 3 Mar 2025 15:17:54 +0600 Subject: [PATCH 13/14] update InfinityPagedComponents --- .../src/commonMain/kotlin/InfinityPagedComponent.kt | 13 +++++++------ .../jvmTest/kotlin/InfinityPagedComponentTests.kt | 8 +++++++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/pagination/compose/src/commonMain/kotlin/InfinityPagedComponent.kt b/pagination/compose/src/commonMain/kotlin/InfinityPagedComponent.kt index 44a0259252f..b94590f80e8 100644 --- a/pagination/compose/src/commonMain/kotlin/InfinityPagedComponent.kt +++ b/pagination/compose/src/commonMain/kotlin/InfinityPagedComponent.kt @@ -54,7 +54,8 @@ class InfinityPagedComponentContext internal constructor( * @param page Initial page number. * @param size Number of items per page. * @param loader Suspended function that loads paginated data. - * @param block Composable function that renders the UI with the loaded data. + * @param block Composable function that renders the UI with the loaded data. When data is in loading state, block will + * receive null as `it` parameter */ @Composable internal fun InfinityPagedComponent( @@ -74,9 +75,7 @@ internal fun InfinityPagedComponent( context.dataState.value = (context.dataState.value ?: emptyList()) + paginationResult.results } - context.dataState.value ?.let { - context.block(context.dataState.value) - } + context.block(context.dataState.value) } /** @@ -85,7 +84,8 @@ internal fun InfinityPagedComponent( * @param T The type of the paginated data. * @param pageInfo Initial pagination information. * @param loader Suspended function that loads paginated data. - * @param block Composable function that renders the UI with the loaded data. + * @param block Composable function that renders the UI with the loaded data. When data is in loading state, block will + * receive null as `it` parameter */ @Composable fun InfinityPagedComponent( @@ -107,7 +107,8 @@ fun InfinityPagedComponent( * @param T The type of the paginated data. * @param size Number of items per page. * @param loader Suspended function that loads paginated data. - * @param block Composable function that renders the UI with the loaded data. + * @param block Composable function that renders the UI with the loaded data. When data is in loading state, block will + * receive null as `it` parameter */ @Composable fun InfinityPagedComponent( diff --git a/pagination/compose/src/jvmTest/kotlin/InfinityPagedComponentTests.kt b/pagination/compose/src/jvmTest/kotlin/InfinityPagedComponentTests.kt index 9844e0d400b..0dfb82a7fe8 100644 --- a/pagination/compose/src/jvmTest/kotlin/InfinityPagedComponentTests.kt +++ b/pagination/compose/src/jvmTest/kotlin/InfinityPagedComponentTests.kt @@ -29,7 +29,13 @@ class InfinityPagedComponentTests { } } ) { - assertEquals(expectedList, it) + if (it == null) { + if (this.iterationState.value.second != null) { + assertEquals(0, (this.iterationState.value.second as? SimplePagination) ?.page) + } + } else { + assertEquals(expectedList, it) + } LaunchedEffect(it ?.size) { loadNext() From 2fbd14956d63b2c6483b3d7e7b6ca2d089bf291a Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Mon, 3 Mar 2025 15:49:46 +0600 Subject: [PATCH 14/14] small update in SortedBinaryTreeNodeTests --- .../kotlin/SortedBinaryTreeNodeTests.android.kt | 2 +- .../src/commonTest/kotlin/SortedBinaryTreeNodeTests.kt | 10 +++++----- .../src/jsTest/kotlin/SortedBinaryTreeNodeTests.js.kt | 2 +- .../jvmTest/kotlin/SortedBinaryTreeNodeTests.jvm.kt | 2 +- .../kotlin/SortedBinaryTreeNodeTests.linuxArm64.kt | 2 +- .../kotlin/SortedBinaryTreeNodeTests.linuxX64.kt | 2 +- .../kotlin/SortedBinaryTreeNodeTests.mingwX64.kt | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/coroutines/src/androidUnitTest/kotlin/SortedBinaryTreeNodeTests.android.kt b/coroutines/src/androidUnitTest/kotlin/SortedBinaryTreeNodeTests.android.kt index 4ab57abfee1..75410922c07 100644 --- a/coroutines/src/androidUnitTest/kotlin/SortedBinaryTreeNodeTests.android.kt +++ b/coroutines/src/androidUnitTest/kotlin/SortedBinaryTreeNodeTests.android.kt @@ -1,2 +1,2 @@ -actual val AllowDeepReInsertOnWorksTest: Boolean +actual val AllowDeepInsertOnWorksTest: Boolean get() = true \ No newline at end of file diff --git a/coroutines/src/commonTest/kotlin/SortedBinaryTreeNodeTests.kt b/coroutines/src/commonTest/kotlin/SortedBinaryTreeNodeTests.kt index 6dfb0156d15..03f81d15ace 100644 --- a/coroutines/src/commonTest/kotlin/SortedBinaryTreeNodeTests.kt +++ b/coroutines/src/commonTest/kotlin/SortedBinaryTreeNodeTests.kt @@ -9,9 +9,8 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue import kotlin.time.Duration.Companion.seconds -import kotlin.time.measureTime -expect val AllowDeepReInsertOnWorksTest: Boolean +expect val AllowDeepInsertOnWorksTest: Boolean class SortedBinaryTreeNodeTests { @Test @@ -46,8 +45,8 @@ class SortedBinaryTreeNodeTests { } } @Test - fun deepReInsertOnWorks() = runTest(timeout = 240.seconds) { - if (AllowDeepReInsertOnWorksTest == false) return@runTest + fun deepReInsertOnWorks() = runTest(timeout = 300.seconds) { + if (AllowDeepInsertOnWorksTest == false) return@runTest val zeroNode = SortedBinaryTreeNode(0) val rangeRadius = 500 val nodes = mutableMapOf>() @@ -124,7 +123,8 @@ class SortedBinaryTreeNodeTests { assertTrue(sourceTreeSize == zeroNode.size()) } @Test - fun deepInsertOnWorks() = runTest(timeout = 440.seconds) { // 440 due to js targets -.- + fun deepInsertOnWorks() = runTest(timeout = 240.seconds) { + if (AllowDeepInsertOnWorksTest == false) return@runTest val zeroNode = SortedBinaryTreeNode(0) val rangeRadius = 500 val nodes = mutableMapOf>() diff --git a/coroutines/src/jsTest/kotlin/SortedBinaryTreeNodeTests.js.kt b/coroutines/src/jsTest/kotlin/SortedBinaryTreeNodeTests.js.kt index 3b385510216..4f2e6974a96 100644 --- a/coroutines/src/jsTest/kotlin/SortedBinaryTreeNodeTests.js.kt +++ b/coroutines/src/jsTest/kotlin/SortedBinaryTreeNodeTests.js.kt @@ -1,2 +1,2 @@ -actual val AllowDeepReInsertOnWorksTest: Boolean +actual val AllowDeepInsertOnWorksTest: Boolean get() = false \ No newline at end of file diff --git a/coroutines/src/jvmTest/kotlin/SortedBinaryTreeNodeTests.jvm.kt b/coroutines/src/jvmTest/kotlin/SortedBinaryTreeNodeTests.jvm.kt index 4ab57abfee1..75410922c07 100644 --- a/coroutines/src/jvmTest/kotlin/SortedBinaryTreeNodeTests.jvm.kt +++ b/coroutines/src/jvmTest/kotlin/SortedBinaryTreeNodeTests.jvm.kt @@ -1,2 +1,2 @@ -actual val AllowDeepReInsertOnWorksTest: Boolean +actual val AllowDeepInsertOnWorksTest: Boolean get() = true \ No newline at end of file diff --git a/coroutines/src/linuxArm64Test/kotlin/SortedBinaryTreeNodeTests.linuxArm64.kt b/coroutines/src/linuxArm64Test/kotlin/SortedBinaryTreeNodeTests.linuxArm64.kt index 4ab57abfee1..75410922c07 100644 --- a/coroutines/src/linuxArm64Test/kotlin/SortedBinaryTreeNodeTests.linuxArm64.kt +++ b/coroutines/src/linuxArm64Test/kotlin/SortedBinaryTreeNodeTests.linuxArm64.kt @@ -1,2 +1,2 @@ -actual val AllowDeepReInsertOnWorksTest: Boolean +actual val AllowDeepInsertOnWorksTest: Boolean get() = true \ No newline at end of file diff --git a/coroutines/src/linuxX64Test/kotlin/SortedBinaryTreeNodeTests.linuxX64.kt b/coroutines/src/linuxX64Test/kotlin/SortedBinaryTreeNodeTests.linuxX64.kt index 4ab57abfee1..75410922c07 100644 --- a/coroutines/src/linuxX64Test/kotlin/SortedBinaryTreeNodeTests.linuxX64.kt +++ b/coroutines/src/linuxX64Test/kotlin/SortedBinaryTreeNodeTests.linuxX64.kt @@ -1,2 +1,2 @@ -actual val AllowDeepReInsertOnWorksTest: Boolean +actual val AllowDeepInsertOnWorksTest: Boolean get() = true \ No newline at end of file diff --git a/coroutines/src/mingwX64Test/kotlin/SortedBinaryTreeNodeTests.mingwX64.kt b/coroutines/src/mingwX64Test/kotlin/SortedBinaryTreeNodeTests.mingwX64.kt index 4ab57abfee1..75410922c07 100644 --- a/coroutines/src/mingwX64Test/kotlin/SortedBinaryTreeNodeTests.mingwX64.kt +++ b/coroutines/src/mingwX64Test/kotlin/SortedBinaryTreeNodeTests.mingwX64.kt @@ -1,2 +1,2 @@ -actual val AllowDeepReInsertOnWorksTest: Boolean +actual val AllowDeepInsertOnWorksTest: Boolean get() = true \ No newline at end of file