Compare commits

...

12 Commits

12 changed files with 283 additions and 88 deletions

View File

@@ -1,5 +1,21 @@
# Changelog
## 0.20.52
* `Coroutines`:
* Small rework of weak jobs: add `WeakScope` factory, rename old weal extensions and add kdocs
## 0.20.51
* `Versions`:
* `Android Fragment`: `1.7.0` -> `1.7.1`
* `Pagination`:
* Add `Pagination.nextPageIfTrue` and `Pagination.thisPageIftrue` extensions to get the page according to condition
pass status
* Add `PaginationResult.nextPageIfNotEmptyOrLastPage` and `PaginationResult.thisPageIfNotEmptyOrLastPage`
* Change all `doForAll` and `getAll` extensions fo pagination to work basing on `nextPageIfNotEmptyOrLastPage` and
`thisPageIfNotEmptyOrLastPage`
## 0.20.50
* `Versions`:

View File

@@ -4,28 +4,71 @@ import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
private fun CoroutineScope.createWeakSubScope() = CoroutineScope(coroutineContext.minusKey(Job)).also { newScope ->
coroutineContext.job.invokeOnCompletion { newScope.cancel() }
/**
* Created [CoroutineScope] which will [launch] listening of [context] job completing and drop itself. Current weak
* scope **will not** be attached to [context] directly. So, this [CoroutineScope] will not prevent parent one from
* cancelling if it is launched with [supervisorScope] or [coroutineScope], but still will follow closing status
* of parent [Job]
*/
fun WeakScope(
context: CoroutineContext
) = CoroutineScope(context.minusKey(Job) + Job()).also { newScope ->
newScope.launch {
context.job.join()
newScope.cancel()
}
}
fun CoroutineScope.weakLaunch(
/**
* Created [CoroutineScope] which will [launch] listening of [scope] [CoroutineContext] job completing and drop itself. Current weak
* scope **will not** be attached to [scope] [CoroutineContext] directly. So, this [CoroutineScope] will not prevent parent one from
* cancelling if it is launched with [supervisorScope] or [coroutineScope], but still will follow closing status
* of parent [Job]
*/
fun WeakScope(
scope: CoroutineScope
) = WeakScope(scope.coroutineContext)
/**
* [this] [CoroutineScope] will be used as base for [WeakScope]. Other parameters ([context], [start], [block])
* will be used to [launch] [Job]
*/
fun CoroutineScope.launchWeak(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val scope = createWeakSubScope()
val scope = WeakScope(this)
val job = scope.launch(context, start, block)
job.invokeOnCompletion { scope.cancel() }
return job
}
fun <T> CoroutineScope.weakAsync(
/**
* [this] [CoroutineScope] will be used as base for [WeakScope]. Other parameters ([context], [start], [block])
* will be used to create [async] [Deferred]
*/
fun <T> CoroutineScope.asyncWeak(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> {
val scope = createWeakSubScope()
val scope = WeakScope(this)
val deferred = scope.async(context, start, block)
deferred.invokeOnCompletion { scope.cancel() }
return deferred
}
@Deprecated("Renamed", ReplaceWith("launchWeak(context, start, block)", "dev.inmo.micro_utils.coroutines.launchWeak"))
fun CoroutineScope.weakLaunch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job = launchWeak(context, start, block)
@Deprecated("Renamed", ReplaceWith("asyncWeak(context, start, block)", "dev.inmo.micro_utils.coroutines.asyncWeak"))
fun <T> CoroutineScope.weakAsync(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> = asyncWeak(context, start, block)

View File

@@ -0,0 +1,64 @@
import dev.inmo.micro_utils.coroutines.asyncWeak
import dev.inmo.micro_utils.coroutines.launchWeak
import kotlinx.coroutines.*
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertTrue
class WeakJobTests {
@Test
fun testWeakJob() = runTest {
var commonJobDone = false
var weakJobStarted = false
try {
coroutineScope {
launch {
delay(1000)
commonJobDone = true
}
asyncWeak {
weakJobStarted = true
delay(100500L)
error("This must never happen")
}
}.await()
} catch (error: Throwable) {
assertTrue(error is CancellationException)
assertTrue(commonJobDone)
assertTrue(weakJobStarted)
return@runTest
}
error("Cancellation exception has not been thrown")
}
@Test
fun testThatWeakJobsWorksCorrectly() = runTest {
val scope = CoroutineScope(Dispatchers.Default)
lateinit var weakLaunchJob: Job
lateinit var weakAsyncJob: Job
val completeDeferred = Job()
coroutineScope {
weakLaunchJob = launchWeak {
while (isActive) {
delay(100L)
}
}
weakAsyncJob = asyncWeak {
while (isActive) {
delay(100L)
}
}
coroutineContext.job.invokeOnCompletion {
scope.launch {
delay(1000L)
completeDeferred.complete()
}
}
launch { delay(1000L); cancel() }
}
completeDeferred.join()
assertTrue(!weakLaunchJob.isActive)
assertTrue(!weakAsyncJob.isActive)
}
}

View File

@@ -1,40 +0,0 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.*
import org.junit.Test
class WeakJob {
@Test
fun `test that weak jobs works correctly`() {
val scope = CoroutineScope(Dispatchers.Default)
lateinit var weakLaunchJob: Job
lateinit var weakAsyncJob: Job
scope.launchSynchronously {
val completeDeferred = Job()
coroutineScope {
weakLaunchJob = weakLaunch {
while (isActive) {
delay(100L)
}
}
weakAsyncJob = weakAsync {
while (isActive) {
delay(100L)
}
}
coroutineContext.job.invokeOnCompletion {
scope.launch {
delay(1000L)
completeDeferred.complete()
}
}
launch { delay(1000L); cancel() }
}
completeDeferred.join()
}
assert(!weakLaunchJob.isActive)
assert(!weakAsyncJob.isActive)
}
}

View File

@@ -15,5 +15,5 @@ crypto_js_version=4.1.1
# Project data
group=dev.inmo
version=0.20.50
android_code_version=256
version=0.20.52
android_code_version=258

View File

@@ -26,13 +26,13 @@ kotlin-poet = "1.16.0"
versions = "0.51.0"
android-gradle = "8.3.2"
android-gradle = "8.2.0"
dexcount = "4.0.0"
android-coreKtx = "1.13.1"
android-recyclerView = "1.3.2"
android-appCompat = "1.6.1"
android-fragment = "1.7.0"
android-fragment = "1.7.1"
android-espresso = "3.5.1"
android-test = "1.1.5"
android-compose-material3 = "1.2.1"

View File

@@ -20,7 +20,13 @@ data class PaginationResult<T>(
* Amount of pages for current pagination
*/
@EncodeDefault
val pagesNumber: Int = ceil(objectsNumber / size.toFloat()).toInt()
@SerialName("pagesNumber")
val pagesNumberLong: Long = ceil(objectsNumber / size.toFloat()).toLong()
/**
* Amount of pages for current pagination
*/
@Transient
val pagesNumber: Int = pagesNumberLong.toInt()
constructor(
page: Int,
@@ -35,31 +41,58 @@ data class PaginationResult<T>(
)
}
fun <T> emptyPaginationResult() = PaginationResult<T>(0, 0, emptyList(), 0L)
val PaginationResult<*>.lastPageLong
get() = pagesNumberLong - 1
val PaginationResult<*>.lastPage
get() = lastPageLong.toInt()
val PaginationResult<*>.isLastPage
get() = page.toLong() == lastPageLong
fun <T> emptyPaginationResult(
basePagination: Pagination
basePagination: Pagination,
objectsNumber: Number
) = PaginationResult<T>(
basePagination.page,
basePagination.size,
emptyList(),
0L
objectsNumber.toLong()
)
fun <T> emptyPaginationResult(
basePagination: Pagination,
) = emptyPaginationResult<T>(basePagination, 0)
fun <T> emptyPaginationResult() = emptyPaginationResult<T>(FirstPagePagination(0))
/**
* @return New [PaginationResult] with [data] without checking of data sizes equality
*/
inline fun <I, O> PaginationResult<I>.changeResultsUnchecked(
block: PaginationResult<I>.() -> List<O>
): PaginationResult<O> = PaginationResult(page, size, block(), objectsNumber)
/**
* @return New [PaginationResult] with [data] without checking of data sizes equality
*/
fun <I, O> PaginationResult<I>.changeResultsUnchecked(
data: List<O>
): PaginationResult<O> = PaginationResult(page, size, data, objectsNumber)
): PaginationResult<O> = changeResultsUnchecked { data }
/**
* @return New [PaginationResult] with [data] <b>with</b> checking of data sizes equality
*/
inline fun <I, O> PaginationResult<I>.changeResults(
block: PaginationResult<I>.() -> List<O>
): PaginationResult<O> {
val data = block()
require(data.size == results.size)
return changeResultsUnchecked(data)
}
/**
* @return New [PaginationResult] with [data] <b>with</b> checking of data sizes equality
*/
fun <I, O> PaginationResult<I>.changeResults(
data: List<O>
): PaginationResult<O> {
require(data.size == results.size)
return changeResultsUnchecked(data)
}
): PaginationResult<O> = changeResults { data }
fun <T> List<T>.createPaginationResult(
pagination: Pagination,

View File

@@ -10,8 +10,7 @@ inline fun doWithPagination(
}
}
@Suppress("NOTHING_TO_INLINE")
inline fun PaginationResult<*>.nextPageIfNotEmpty() = if (results.isNotEmpty()) {
inline fun <T, PR: PaginationResult<T>> PR.nextPageIfTrue(condition: PR.() -> Boolean) = if (condition()) {
SimplePagination(
page + 1,
size
@@ -20,12 +19,28 @@ inline fun PaginationResult<*>.nextPageIfNotEmpty() = if (results.isNotEmpty())
null
}
@Suppress("NOTHING_TO_INLINE")
inline fun <T> PaginationResult<T>.thisPageIfNotEmpty(): PaginationResult<T>? = if (results.isNotEmpty()) {
inline fun <T, PR: PaginationResult<T>> PR.thisPageIfTrue(condition: PR.() -> Boolean): PR? = if (condition()) {
this
} else {
null
}
@Suppress("NOTHING_TO_INLINE")
inline fun <T> PaginationResult<T>.currentPageIfNotEmpty() = thisPageIfNotEmpty()
fun PaginationResult<*>.nextPageIfNotEmpty() = nextPageIfTrue { results.isNotEmpty() }
fun <T> PaginationResult<T>.thisPageIfNotEmpty(): PaginationResult<T>? = thisPageIfTrue { results.isNotEmpty() }
fun <T> PaginationResult<T>.currentPageIfNotEmpty() = thisPageIfNotEmpty()
fun PaginationResult<*>.nextPageIfNotEmptyOrLastPage() = nextPageIfTrue { results.isNotEmpty() && !this.isLastPage }
fun <T> PaginationResult<T>.thisPageIfNotEmptyOrLastPage(): PaginationResult<T>? = thisPageIfTrue { results.isNotEmpty() && !this.isLastPage }
fun <T> PaginationResult<T>.currentPageIfNotEmptyOrLastPage() = thisPageIfNotEmptyOrLastPage()
fun PaginationResult<*>.nextPageIfNotLastPage() = nextPageIfTrue { !this.isLastPage }
fun <T> PaginationResult<T>.thisPageIfNotLastPage(): PaginationResult<T>? = thisPageIfTrue { !this.isLastPage }
fun <T> PaginationResult<T>.currentPageIfNotLastPage() = thisPageIfNotLastPage()

View File

@@ -18,7 +18,7 @@ inline fun <T> doForAllWithNextPaging(
) {
doForAll(
initialPagination,
{ it.nextPageIfNotEmpty() },
{ it.nextPageIfNotEmptyOrLastPage() },
block
)
}
@@ -29,7 +29,7 @@ inline fun <T> doAllWithCurrentPaging(
) {
doForAll(
initialPagination,
{ it.currentPageIfNotEmpty() },
{ it.nextPageIfNotEmptyOrLastPage() },
block
)
}

View File

@@ -31,7 +31,7 @@ inline fun <T> getAllWithNextPaging(
block: (Pagination) -> PaginationResult<T>
): List<T> = getAll(
initialPagination,
{ it.nextPageIfNotEmpty() },
{ it.nextPageIfNotEmptyOrLastPage() },
block
)
@@ -48,7 +48,7 @@ inline fun <T> getAllWithCurrentPaging(
block: (Pagination) -> PaginationResult<T>
): List<T> = getAll(
initialPagination,
{ it.currentPageIfNotEmpty() },
{ it.thisPageIfNotEmptyOrLastPage() },
block
)

View File

@@ -21,33 +21,25 @@ fun <T> Iterable<T>.paginate(with: Pagination): PaginationResult<T> {
}
fun <T> List<T>.paginate(with: Pagination): PaginationResult<T> {
val firstIndex = maxOf(with.firstIndex, 0)
val lastIndex = minOf(with.lastIndexExclusive, size)
if (firstIndex > lastIndex) {
return emptyPaginationResult()
if (with.firstIndex >= size || with.lastIndex < 0) {
return emptyPaginationResult(with, size.toLong())
}
return subList(firstIndex, lastIndex).createPaginationResult(
return asSequence().drop(with.firstIndex).take(with.size).toList().createPaginationResult(
with,
size.toLong()
)
}
fun <T> List<T>.paginate(with: Pagination, reversed: Boolean): PaginationResult<T> {
val actualPagination = with.optionallyReverse(
size,
reversed
)
val firstIndex = maxOf(actualPagination.firstIndex, 0)
val lastIndex = minOf(actualPagination.lastIndexExclusive, size)
if (firstIndex > lastIndex) {
return emptyPaginationResult()
return if (reversed) {
val actualPagination = with.optionallyReverse(
size,
reversed
)
paginate(actualPagination).changeResultsUnchecked { results.reversed() }
} else {
paginate(with)
}
return subList(firstIndex, lastIndex).optionallyReverse(reversed).createPaginationResult(
with,
size.toLong()
)
}
fun <T> Set<T>.paginate(with: Pagination): PaginationResult<T> {

View File

@@ -0,0 +1,72 @@
package dev.inmo.micro_utils.pagination.utils
import dev.inmo.micro_utils.pagination.*
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class PaginationPaging {
@Test
fun testPaginateOnList() {
val list = (0 until 7).toList()
val startPagination = FirstPagePagination(2)
var lastPageHappened = false
doForAllWithNextPaging(startPagination) {
val result = list.paginate(it)
if (result.isLastPage) {
lastPageHappened = true
assertTrue(result.results.size == 1)
}
val testSublist = list.subList(it.firstIndex, minOf(it.lastIndexExclusive, list.size))
assertEquals(result.results, testSublist)
result
}
assertTrue(lastPageHappened)
}
@Test
fun testEmptyPaginateOnList() {
val list = listOf<Int>()
val startPagination = FirstPagePagination(2)
var paginationHappend = false
doForAllWithNextPaging(startPagination) {
val resultPagination = list.paginate(it)
assertEquals(resultPagination, emptyPaginationResult(it, list.size))
assertFalse(paginationHappend)
paginationHappend = true
resultPagination
}
assertTrue(paginationHappend)
}
@Test
fun testRightOutPaginateOnList() {
val list = (0 until 7).toList()
val startPagination = SimplePagination(page = 4, size = 2)
var paginationHappend = false
doForAllWithNextPaging(startPagination) {
val resultPagination = list.paginate(it)
assertEquals(resultPagination, emptyPaginationResult(it, list.size))
assertFalse(paginationHappend)
paginationHappend = true
resultPagination
}
assertTrue(paginationHappend)
}
}