Merge pull request #295 from InsanusMokrassar/0.20.1

0.20.1
This commit is contained in:
InsanusMokrassar 2023-08-12 22:47:03 +06:00 committed by GitHub
commit bd39ab2467
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 834 additions and 2 deletions

View File

@ -1,5 +1,15 @@
# Changelog
## 0.20.1
* `SmallTextField`:
* Module is initialized
* `Pickers`:
* Module is initialized
* `Coroutines`:
* Add `SmartSemaphore`
* Add `SmartRWLocker`
## 0.20.0
* `Versions`:

View File

@ -0,0 +1,18 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
alias(libs.plugins.jb.compose)
}
apply from: "$mppProjectWithSerializationAndComposePresetPath"
kotlin {
sourceSets {
androidMain {
dependencies {
api project(":micro_utils.android.smalltextfield")
}
}
}
}

View File

@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.android.pickers"/>

View File

@ -0,0 +1,27 @@
package dev.inmo.micro_utils.android.pickers
import androidx.compose.animation.core.*
internal suspend fun Animatable<Float, AnimationVector1D>.fling(
initialVelocity: Float,
animationSpec: DecayAnimationSpec<Float>,
adjustTarget: ((Float) -> Float)?,
block: (Animatable<Float, AnimationVector1D>.() -> Unit)? = null,
): AnimationResult<Float, AnimationVector1D> {
val targetValue = animationSpec.calculateTargetValue(value, initialVelocity)
val adjustedTarget = adjustTarget?.invoke(targetValue)
return if (adjustedTarget != null) {
animateTo(
targetValue = adjustedTarget,
initialVelocity = initialVelocity,
block = block
)
} else {
animateDecay(
initialVelocity = initialVelocity,
animationSpec = animationSpec,
block = block,
)
}
}

View File

@ -0,0 +1,222 @@
package dev.inmo.micro_utils.android.pickers
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.exponentialDecay
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.ContentAlpha
import androidx.compose.material.IconButton
import androidx.compose.material.ProvideTextStyle
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.center
import androidx.compose.ui.unit.dp
import dev.inmo.micro_utils.android.smalltextfield.SmallTextField
import kotlinx.coroutines.launch
import kotlin.math.abs
import kotlin.math.absoluteValue
import kotlin.math.roundToInt
private inline fun PointerInputScope.checkContains(offset: Offset): Boolean {
return ((size.center.x - offset.x).absoluteValue < size.width / 2) && ((size.center.y - offset.y).absoluteValue < size.height / 2)
}
// src: https://gist.github.com/vganin/a9a84653a9f48a2d669910fbd48e32d5
@OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class)
@Composable
fun NumberPicker(
number: Int,
modifier: Modifier = Modifier,
range: IntRange? = null,
textStyle: TextStyle = LocalTextStyle.current,
arrowsColor: Color = MaterialTheme.colorScheme.primary,
allowUseManualInput: Boolean = true,
onStateChanged: (Int) -> Unit = {},
) {
val coroutineScope = rememberCoroutineScope()
val numbersColumnHeight = 36.dp
val halvedNumbersColumnHeight = numbersColumnHeight / 2
val halvedNumbersColumnHeightPx = with(LocalDensity.current) { halvedNumbersColumnHeight.toPx() }
fun animatedStateValue(offset: Float): Int = number - (offset / halvedNumbersColumnHeightPx).toInt()
val animatedOffset = remember { Animatable(0f) }.apply {
if (range != null) {
val offsetRange = remember(number, range) {
val value = number
val first = -(range.last - value) * halvedNumbersColumnHeightPx
val last = -(range.first - value) * halvedNumbersColumnHeightPx
first..last
}
updateBounds(offsetRange.start, offsetRange.endInclusive)
}
}
val coercedAnimatedOffset = animatedOffset.value % halvedNumbersColumnHeightPx
val animatedStateValue = animatedStateValue(animatedOffset.value)
val disabledArrowsColor = arrowsColor.copy(alpha = ContentAlpha.disabled)
val inputFieldShown = if (allowUseManualInput) {
remember { mutableStateOf(false) }
} else {
null
}
Column(
modifier = modifier
.wrapContentSize()
.draggable(
orientation = Orientation.Vertical,
state = rememberDraggableState { deltaY ->
if (inputFieldShown ?.value != true) {
coroutineScope.launch {
animatedOffset.snapTo(animatedOffset.value + deltaY)
}
}
},
onDragStopped = { velocity ->
if (inputFieldShown ?.value != true) {
coroutineScope.launch {
val endValue = animatedOffset.fling(
initialVelocity = velocity,
animationSpec = exponentialDecay(frictionMultiplier = 20f),
adjustTarget = { target ->
val coercedTarget = target % halvedNumbersColumnHeightPx
val coercedAnchors =
listOf(-halvedNumbersColumnHeightPx, 0f, halvedNumbersColumnHeightPx)
val coercedPoint = coercedAnchors.minByOrNull { abs(it - coercedTarget) }!!
val base =
halvedNumbersColumnHeightPx * (target / halvedNumbersColumnHeightPx).toInt()
coercedPoint + base
}
).endState.value
onStateChanged(animatedStateValue(endValue))
animatedOffset.snapTo(0f)
}
}
}
),
horizontalAlignment = Alignment.CenterHorizontally
) {
val spacing = 4.dp
val upEnabled = range == null || range.first < number
IconButton(
{
onStateChanged(number - 1)
inputFieldShown ?.value = false
},
enabled = upEnabled
) {
Icon(Icons.Default.KeyboardArrowUp, "", tint = if (upEnabled) arrowsColor else disabledArrowsColor)
}
Spacer(modifier = Modifier.height(spacing))
Box(
modifier = Modifier
.offset { IntOffset(x = 0, y = coercedAnimatedOffset.roundToInt()) },
contentAlignment = Alignment.Center
) {
val baseLabelModifier = Modifier.align(Alignment.Center)
ProvideTextStyle(textStyle) {
Text(
text = (animatedStateValue - 1).toString(),
modifier = baseLabelModifier
.offset(y = -halvedNumbersColumnHeight)
.alpha(coercedAnimatedOffset / halvedNumbersColumnHeightPx)
)
if (inputFieldShown ?.value == true) {
val currentValue = remember { mutableStateOf(number.toString()) }
val focusRequester = remember { FocusRequester() }
SmallTextField(
currentValue.value,
{
val asDigit = it.toIntOrNull()
when {
(asDigit == null && it.isEmpty()) -> currentValue.value = (range ?.first ?: 0).toString()
(asDigit != null && (range == null || asDigit in range)) -> currentValue.value = it
else -> { /* do nothing */ }
}
},
baseLabelModifier.focusRequester(focusRequester).width(IntrinsicSize.Min).pointerInput(number) {
detectTapGestures {
if (!checkContains(it)) {
currentValue.value.toIntOrNull() ?.let(onStateChanged)
inputFieldShown.value = false
}
}
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number
),
keyboardActions = KeyboardActions {
currentValue.value.toIntOrNull() ?.let(onStateChanged)
inputFieldShown.value = false
},
singleLine = true,
textStyle = textStyle
)
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
} else {
Text(
text = animatedStateValue.toString(),
modifier = baseLabelModifier
.alpha(1 - abs(coercedAnimatedOffset) / halvedNumbersColumnHeightPx)
.clickable {
if (inputFieldShown ?.value == false) {
inputFieldShown.value = true
}
}
)
}
Text(
text = (animatedStateValue + 1).toString(),
modifier = baseLabelModifier
.offset(y = halvedNumbersColumnHeight)
.alpha(-coercedAnimatedOffset / halvedNumbersColumnHeightPx)
)
}
}
Spacer(modifier = Modifier.height(spacing))
val downEnabled = range == null || range.last > number
IconButton(
{
onStateChanged(number + 1)
inputFieldShown ?.value = false
},
enabled = downEnabled
) {
Icon(Icons.Default.KeyboardArrowDown, "", tint = if (downEnabled) arrowsColor else disabledArrowsColor)
}
}
}

View File

@ -0,0 +1,156 @@
package dev.inmo.micro_utils.android.pickers
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.exponentialDecay
import androidx.compose.foundation.gestures.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.ContentAlpha
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import kotlin.math.*
@OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class)
@Composable
fun <T> SetPicker(
current: T,
dataList: List<T>,
modifier: Modifier = Modifier,
textStyle: TextStyle = LocalTextStyle.current,
arrowsColor: Color = MaterialTheme.colorScheme.primary,
dataToString: @Composable (T) -> String = { it.toString() },
onStateChanged: (T) -> Unit = {},
) {
val coroutineScope = rememberCoroutineScope()
val numbersColumnHeight = 8.dp + with(LocalDensity.current) {
textStyle.lineHeight.toDp()
}
val numbersColumnHeightPx = with(LocalDensity.current) { numbersColumnHeight.toPx() }
val halvedNumbersColumnHeight = numbersColumnHeight / 2
val halvedNumbersColumnHeightPx = with(LocalDensity.current) { halvedNumbersColumnHeight.toPx() }
val index = dataList.indexOfFirst { it === current }.takeIf { it > -1 } ?: dataList.indexOf(current)
val lastIndex = dataList.size - 1
fun animatedStateValue(offset: Float): Int = index - (offset / halvedNumbersColumnHeightPx).toInt()
val animatedOffset = remember { Animatable(0f) }.apply {
val offsetRange = remember(index, lastIndex) {
val value = index
val first = -(lastIndex - value) * halvedNumbersColumnHeightPx
val last = value * halvedNumbersColumnHeightPx
first..last
}
updateBounds(offsetRange.start, offsetRange.endInclusive)
}
val indexAnimatedOffset = if (animatedOffset.value > 0) {
(index - floor(animatedOffset.value / halvedNumbersColumnHeightPx).toInt())
} else {
(index - ceil(animatedOffset.value / halvedNumbersColumnHeightPx).toInt())
}
val coercedAnimatedOffset = animatedOffset.value % halvedNumbersColumnHeightPx
val boxOffset = (indexAnimatedOffset * halvedNumbersColumnHeightPx) - coercedAnimatedOffset
val disabledArrowsColor = arrowsColor.copy(alpha = ContentAlpha.disabled)
val scrollState = rememberScrollState()
Column(
modifier = modifier
.wrapContentSize()
.draggable(
orientation = Orientation.Vertical,
state = rememberDraggableState { deltaY ->
coroutineScope.launch {
animatedOffset.snapTo(animatedOffset.value + deltaY)
}
},
onDragStopped = { velocity ->
coroutineScope.launch {
val endValue = animatedOffset.fling(
initialVelocity = velocity,
animationSpec = exponentialDecay(frictionMultiplier = 20f),
adjustTarget = { target ->
val coercedTarget = target % halvedNumbersColumnHeightPx
val coercedAnchors =
listOf(-halvedNumbersColumnHeightPx, 0f, halvedNumbersColumnHeightPx)
val coercedPoint = coercedAnchors.minByOrNull { abs(it - coercedTarget) }!!
val base =
halvedNumbersColumnHeightPx * (target / halvedNumbersColumnHeightPx).toInt()
coercedPoint + base
}
).endState.value
onStateChanged(dataList.elementAt(animatedStateValue(endValue)))
animatedOffset.snapTo(0f)
}
}
),
horizontalAlignment = Alignment.CenterHorizontally
) {
val spacing = 4.dp
val upEnabled = index > 0
IconButton(
{
onStateChanged(dataList.elementAt(index - 1))
},
enabled = upEnabled
) {
Icon(Icons.Default.KeyboardArrowUp, "", tint = if (upEnabled) arrowsColor else disabledArrowsColor)
}
Spacer(modifier = Modifier.height(spacing))
Box(
modifier = Modifier,
contentAlignment = Alignment.Center
) {
ProvideTextStyle(textStyle) {
dataList.forEachIndexed { i, t ->
val alpha = when {
i == indexAnimatedOffset - 1 -> coercedAnimatedOffset / halvedNumbersColumnHeightPx
i == indexAnimatedOffset -> 1 - (abs(coercedAnimatedOffset) / halvedNumbersColumnHeightPx)
i == indexAnimatedOffset + 1 -> -coercedAnimatedOffset / halvedNumbersColumnHeightPx
else -> return@forEachIndexed
}
val offset = when {
i == indexAnimatedOffset - 1 && coercedAnimatedOffset > 0 -> coercedAnimatedOffset - halvedNumbersColumnHeightPx
i == indexAnimatedOffset -> coercedAnimatedOffset
i == indexAnimatedOffset + 1 && coercedAnimatedOffset < 0 -> coercedAnimatedOffset + halvedNumbersColumnHeightPx
else -> return@forEachIndexed
}
Text(
text = dataToString(t),
modifier = Modifier
.alpha(alpha)
.offset(y = with(LocalDensity.current) { offset.toDp() })
)
}
}
}
Spacer(modifier = Modifier.height(spacing))
val downEnabled = index < lastIndex
IconButton(
{
onStateChanged(dataList.elementAt(index + 1))
},
enabled = downEnabled
) {
Icon(Icons.Default.KeyboardArrowDown, "", tint = if (downEnabled) arrowsColor else disabledArrowsColor)
}
}
}

View File

@ -0,0 +1,18 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
alias(libs.plugins.jb.compose)
}
apply from: "$mppProjectWithSerializationAndComposePresetPath"
kotlin {
sourceSets {
androidMain {
dependencies {
api libs.android.compose.material3
}
}
}
}

View File

@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.android.smalltextfield"/>

View File

@ -0,0 +1,66 @@
package dev.inmo.micro_utils.android.smalltextfield
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.VisualTransformation
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SmallTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
textStyle: TextStyle = LocalTextStyle.current,
textColor: Color = textStyle.color.takeOrElse {
LocalContentColor.current
},
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
singleLine: Boolean = false,
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
minLines: Int = 1,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
BasicTextField(
value = value,
modifier = modifier,
onValueChange = onValueChange,
enabled = enabled,
readOnly = readOnly,
textStyle = textStyle.copy(
color = textColor
),
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
interactionSource = interactionSource,
singleLine = singleLine,
maxLines = maxLines,
minLines = minLines,
cursorBrush = SolidColor(
textStyle.color.takeOrElse {
LocalContentColor.current
}
)
)
}

View File

@ -0,0 +1,102 @@
package dev.inmo.micro_utils.coroutines
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
/**
* Composite mutex which works with next rules:
*
* * [acquireRead] require to [writeMutex] be free. Then it will take one lock from [readSemaphore]
* * [releaseRead] will just free up one permit in [readSemaphore]
* * [lockWrite] will lock [writeMutex] and then await while all [readSemaphore] will be freed
* * [unlockWrite] will just unlock [writeMutex]
*/
class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked: Boolean = false) {
private val _readSemaphore = SmartSemaphore.Mutable(permits = readPermits, acquiredPermits = 0)
private val _writeMutex = SmartMutex.Mutable(locked = writeIsLocked)
val readSemaphore: SmartSemaphore.Immutable = _readSemaphore.immutable()
val writeMutex: SmartMutex.Immutable = _writeMutex.immutable()
/**
* Do lock in [readSemaphore] inside of [writeMutex] locking
*/
suspend fun acquireRead() {
_writeMutex.withLock {
_readSemaphore.acquire()
}
}
/**
* Release one read permit in [readSemaphore]
*/
suspend fun releaseRead(): Boolean {
return _readSemaphore.release()
}
/**
* Locking [writeMutex] and wait while all [readSemaphore] permits will be freed
*/
suspend fun lockWrite() {
_writeMutex.lock()
readSemaphore.waitRelease(readPermits)
}
/**
* Unlock [writeMutex]
*/
suspend fun unlockWrite(): Boolean {
return _writeMutex.unlock()
}
}
/**
* Will call [SmartSemaphore.Mutable.lock], then execute [action] and return the result after [SmartSemaphore.Mutable.unlock]
*/
@OptIn(ExperimentalContracts::class)
suspend inline fun <T> SmartRWLocker.withReadAcquire(action: () -> T): T {
contract {
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
}
acquireRead()
try {
return action()
} finally {
releaseRead()
}
}
/**
* Will wait until the [SmartSemaphore.permitsStateFlow] of [this] instance will have [permits] count free permits.
*
* Anyway, after the end of this block there are no any guaranties that [SmartSemaphore.freePermits] >= [permits] due to
* the fact that some other parties may lock it again
*/
suspend fun SmartRWLocker.waitReadRelease(permits: Int = 1) = readSemaphore.waitRelease(permits)
/**
* Will call [SmartMutex.Mutable.lock], then execute [action] and return the result after [SmartMutex.Mutable.unlock]
*/
@OptIn(ExperimentalContracts::class)
suspend inline fun <T> SmartRWLocker.withWriteLock(action: () -> T): T {
contract {
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
}
lockWrite()
try {
return action()
} finally {
unlockWrite()
}
}
/**
* Will wait until the [SmartMutex.lockStateFlow] of [this] instance will be false.
*
* Anyway, after the end of this block there are no any guaranties that [SmartMutex.isLocked] == false due to the fact
* that some other parties may lock it again
*/
suspend fun SmartRWLocker.waitWriteUnlock() = writeMutex.waitUnlock()

View File

@ -0,0 +1,142 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.isActive
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withLock
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
/**
* It is interface which will work like classic [Semaphore], but in difference have [permitsStateFlow] for listening of the
* [SmartSemaphore] state.
*
* There is [Mutable] and [Immutable] realizations. In case you are owner and manager current state of lock, you need
* [Mutable] [SmartSemaphore]. Otherwise, [Immutable].
*
* Any [Mutable] [SmartSemaphore] may produce its [Immutable] variant which will contains [permitsStateFlow] equal to its
* [Mutable] creator
*/
sealed interface SmartSemaphore {
val permitsStateFlow: StateFlow<Int>
/**
* * True - locked
* * False - unlocked
*/
val freePermits: Int
get() = permitsStateFlow.value
/**
* Immutable variant of [SmartSemaphore]. In fact will depend on the owner of [permitsStateFlow]
*/
class Immutable(override val permitsStateFlow: StateFlow<Int>) : SmartSemaphore
/**
* Mutable variant of [SmartSemaphore]. With that variant you may [lock] and [unlock]. Besides, you may create
* [Immutable] variant of [this] instance with [immutable] factory
*
* @param locked Preset state of [freePermits] and its internal [_permitsStateFlow]
*/
class Mutable(private val permits: Int, acquiredPermits: Int = 0) : SmartSemaphore {
private val _permitsStateFlow = MutableStateFlow<Int>(permits - acquiredPermits)
override val permitsStateFlow: StateFlow<Int> = _permitsStateFlow.asStateFlow()
private val internalChangesMutex = Mutex(false)
fun immutable() = Immutable(permitsStateFlow)
private fun checkedPermits(permits: Int) = permits.coerceIn(1 .. this.permits)
/**
* Holds call until this [SmartSemaphore] will be re-locked. That means that while [freePermits] == true, [holds] will
* wait for [freePermits] == false and then try to lock
*/
suspend fun acquire(permits: Int = 1) {
do {
val checkedPermits = checkedPermits(permits)
waitRelease(checkedPermits)
val shouldContinue = internalChangesMutex.withLock {
if (_permitsStateFlow.value < checkedPermits) {
true
} else {
_permitsStateFlow.value -= checkedPermits
false
}
}
} while (shouldContinue && currentCoroutineContext().isActive)
}
/**
* Will try to lock this [SmartSemaphore] immediataly
*
* @return True if lock was successful. False otherwise
*/
suspend fun tryAcquire(permits: Int = 1): Boolean {
val checkedPermits = checkedPermits(permits)
return if (_permitsStateFlow.value < checkedPermits) {
internalChangesMutex.withLock {
if (_permitsStateFlow.value < checkedPermits) {
_permitsStateFlow.value -= checkedPermits
true
} else {
false
}
}
} else {
false
}
}
/**
* If [freePermits] == true - will change it to false and return true. If current call will not unlock this
* [SmartSemaphore] - false
*/
suspend fun release(permits: Int = 1): Boolean {
val checkedPermits = checkedPermits(permits)
return if (_permitsStateFlow.value < this.permits) {
internalChangesMutex.withLock {
if (_permitsStateFlow.value < this.permits) {
_permitsStateFlow.value = minOf(_permitsStateFlow.value + checkedPermits, this.permits)
true
} else {
false
}
}
} else {
false
}
}
}
}
/**
* Will call [SmartSemaphore.Mutable.lock], then execute [action] and return the result after [SmartSemaphore.Mutable.unlock]
*/
@OptIn(ExperimentalContracts::class)
suspend inline fun <T> SmartSemaphore.Mutable.withAcquire(permits: Int = 1, action: () -> T): T {
contract {
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
}
acquire(permits)
try {
return action()
} finally {
release(permits)
}
}
/**
* Will wait until the [SmartSemaphore.permitsStateFlow] of [this] instance will have [permits] count free permits.
*
* Anyway, after the end of this block there are no any guaranties that [SmartSemaphore.freePermits] >= [permits] due to
* the fact that some other parties may lock it again
*/
suspend fun SmartSemaphore.waitRelease(permits: Int = 1) = permitsStateFlow.first { it >= permits }

View File

@ -0,0 +1,60 @@
import dev.inmo.micro_utils.coroutines.SmartRWLocker
import dev.inmo.micro_utils.coroutines.withReadAcquire
import dev.inmo.micro_utils.coroutines.withWriteLock
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.delay
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class SmartRWLockerTests {
@Test
fun compositeTest() {
val locker = SmartRWLocker()
val readAndWriteWorkers = 10
runTest {
var started = 0
var done = 0
val doneMutex = Mutex()
val readWorkers = (0 until readAndWriteWorkers).map {
launch(start = CoroutineStart.LAZY) {
locker.withReadAcquire {
doneMutex.withLock {
started++
}
delay(100L)
doneMutex.withLock {
done++
}
}
}
}
var doneWrites = 0
val writeWorkers = (0 until readAndWriteWorkers).map {
launch(start = CoroutineStart.LAZY) {
locker.withWriteLock {
assertTrue(done == readAndWriteWorkers || started == 0)
delay(10L)
doneWrites++
}
}
}
readWorkers.forEach { it.start() }
writeWorkers.forEach { it.start() }
readWorkers.joinAll()
writeWorkers.joinAll()
assertEquals(expected = readAndWriteWorkers, actual = done)
assertEquals(expected = readAndWriteWorkers, actual = doneWrites)
}
}
}

View File

@ -15,5 +15,5 @@ crypto_js_version=4.1.1
# Project data
group=dev.inmo
version=0.20.0
android_code_version=206
version=0.20.1
android_code_version=207

View File

@ -35,6 +35,7 @@ android-appCompat = "1.6.1"
android-fragment = "1.6.1"
android-espresso = "3.5.1"
android-test = "1.1.5"
android-compose-material3 = "1.1.1"
android-props-minSdk = "21"
android-props-compileSdk = "33"
@ -83,6 +84,7 @@ jb-exposed = { module = "org.jetbrains.exposed:exposed-core", version.ref = "jb-
android-coreKtx = { module = "androidx.core:core-ktx", version.ref = "android-coreKtx" }
android-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "android-recyclerView" }
android-appCompat-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "android-appCompat" }
android-compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "android-compose-material3" }
android-fragment = { module = "androidx.fragment:fragment", version.ref = "android-fragment" }
android-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "android-espresso" }
android-test-junit = { module = "androidx.test.ext:junit", version.ref = "android-test" }

View File

@ -18,6 +18,7 @@ kotlin {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
implementation libs.kt.coroutines.test
}
}
}

View File

@ -22,6 +22,7 @@ kotlin {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
implementation libs.kt.coroutines.test
}
}

View File

@ -28,6 +28,7 @@ kotlin {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
implementation libs.kt.coroutines.test
}
}

View File

@ -32,6 +32,7 @@ kotlin {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
implementation libs.kt.coroutines.test
}
}
jvmTest {

View File

@ -31,6 +31,7 @@ kotlin {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
implementation libs.kt.coroutines.test
}
}
jvmMain {

View File

@ -32,6 +32,8 @@ String[] includes = [
":coroutines",
":coroutines:compose",
":android:recyclerview",
":android:pickers",
":android:smalltextfield",
":android:alerts:common",
":android:alerts:recyclerview",
":serialization:base64",