Last active
April 21, 2025 18:50
-
-
Save inidamleader/7bcc273afe6b885738556d190582a815 to your computer and use it in GitHub Desktop.
ListPicker: Generic compose picker (Number picker, date or any other type) function to select an item from a list by scrolling through the list with: possibility of editing and wrapSelectorWheel parameter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.inidamleader.ovtracker.util.compose | |
import androidx.compose.animation.AnimatedContent | |
import androidx.compose.foundation.clickable | |
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior | |
import androidx.compose.foundation.layout.Box | |
import androidx.compose.foundation.layout.Column | |
import androidx.compose.foundation.layout.Spacer | |
import androidx.compose.foundation.layout.fillMaxWidth | |
import androidx.compose.foundation.layout.height | |
import androidx.compose.foundation.layout.offset | |
import androidx.compose.foundation.layout.padding | |
import androidx.compose.foundation.lazy.LazyColumn | |
import androidx.compose.foundation.lazy.rememberLazyListState | |
import androidx.compose.foundation.text.KeyboardActions | |
import androidx.compose.foundation.text.KeyboardOptions | |
import androidx.compose.material3.HorizontalDivider | |
import androidx.compose.material3.LocalTextStyle | |
import androidx.compose.material3.MaterialTheme | |
import androidx.compose.material3.Surface | |
import androidx.compose.material3.Text | |
import androidx.compose.material3.TextField | |
import androidx.compose.material3.TextFieldDefaults | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.LaunchedEffect | |
import androidx.compose.runtime.derivedStateOf | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.mutableIntStateOf | |
import androidx.compose.runtime.mutableStateOf | |
import androidx.compose.runtime.remember | |
import androidx.compose.runtime.rememberCoroutineScope | |
import androidx.compose.runtime.saveable.rememberSaveable | |
import androidx.compose.runtime.setValue | |
import androidx.compose.runtime.snapshotFlow | |
import androidx.compose.ui.Alignment | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.focus.FocusRequester | |
import androidx.compose.ui.focus.focusRequester | |
import androidx.compose.ui.graphics.Brush | |
import androidx.compose.ui.graphics.Color | |
import androidx.compose.ui.text.TextStyle | |
import androidx.compose.ui.text.input.ImeAction | |
import androidx.compose.ui.text.input.KeyboardType | |
import androidx.compose.ui.text.style.TextAlign | |
import androidx.compose.ui.text.style.TextOverflow | |
import androidx.compose.ui.tooling.preview.Preview | |
import androidx.compose.ui.unit.Dp | |
import androidx.compose.ui.unit.dp | |
import com.inidamleader.ovtracker.global.integerOnlyRegex | |
import com.inidamleader.ovtracker.util.compose.geometry.toDp | |
import kotlinx.coroutines.flow.collectLatest | |
import kotlinx.coroutines.launch | |
import java.time.LocalDate | |
import java.time.format.DateTimeFormatter | |
import java.time.format.FormatStyle | |
import java.util.Locale | |
/** | |
* A composable function that allows users to select an item from a list using a scrollable list with a text field for editing. | |
* | |
* @param initialValue The initial value to be selected in the list. | |
* @param values The list of items. | |
* @param modifier Modifier for customizing the appearance of the `ListPicker`. | |
* @param wrapSelectorWheel Boolean flag indicating whether the list should wrap around like a selector wheel. | |
* @param format A lambda function that formats an item into a string for display. | |
* @param onValueChange A callback function that is invoked when the selected item changes. | |
* @param onIsErrorChange A callback function that is invoked when the isError changes. | |
* @param parse A lambda function that parses a string into an item. | |
* @param enableEdition Boolean flag indicating whether the user can edit the selected item using a text field. | |
* @param beyondViewportPageCount The number of pages to display on either side of the selected item. | |
* @param textStyle The text style for the displayed items. | |
* @param verticalPadding The vertical padding between items. | |
* @param dividerColor The color of the horizontal dividers. | |
* @param dividerThickness The thickness of the horizontal dividers. | |
* | |
* @author Reda El Madini - For support, contact [email protected] | |
*/ | |
@Composable | |
fun <E> ListPicker( | |
initialValue: E, | |
values: List<E>, | |
onValueChange: (E) -> Unit, | |
modifier: Modifier = Modifier, | |
wrapSelectorWheel: Boolean = false, | |
format: E.() -> String = { toString() }, | |
parse: (String.() -> E?)? = null, | |
onIsErrorChange: (Boolean) -> Unit = {}, | |
enableEdition: Boolean = parse != null, | |
beyondViewportPageCount: Int = 1, | |
textStyle: TextStyle = LocalTextStyle.current, | |
verticalPadding: Dp = 16.dp, | |
dividerColor: Color = MaterialTheme.colorScheme.outline, | |
dividerThickness: Dp = 1.dp, | |
keyboardType: KeyboardType = KeyboardType.Text, | |
) { | |
val listSize = values.size | |
val coercedOutOfBoundsPageCount = beyondViewportPageCount.coerceIn(0..listSize / 2) | |
val visibleItemsCount = 1 + coercedOutOfBoundsPageCount * 2 | |
val iteration = run { | |
if (wrapSelectorWheel) remember(key1 = coercedOutOfBoundsPageCount, key2 = listSize) { | |
(Int.MAX_VALUE - 2 * coercedOutOfBoundsPageCount) / listSize | |
} | |
else 1 | |
} | |
val intervals = remember(key1 = coercedOutOfBoundsPageCount, key2 = iteration, key3 = listSize) { | |
listOf( | |
0, | |
coercedOutOfBoundsPageCount, | |
coercedOutOfBoundsPageCount + iteration * listSize, | |
coercedOutOfBoundsPageCount + iteration * listSize + coercedOutOfBoundsPageCount, | |
) | |
} | |
val scrollOfItemIndex = { it: Int -> | |
it + (listSize * (iteration / 2)) | |
} | |
val scrollOfItem = { item: E -> | |
values.indexOf(item).takeIf { it != -1 }?.let { index -> scrollOfItemIndex(index) } | |
} | |
val lazyListState = rememberLazyListState( | |
initialFirstVisibleItemIndex = remember( | |
key1 = initialValue, | |
key2 = listSize, | |
key3 = iteration, | |
) { | |
scrollOfItem(initialValue) ?: 0 | |
}, | |
) | |
LaunchedEffect(key1 = values) { | |
snapshotFlow { lazyListState.firstVisibleItemIndex }.collectLatest { | |
onValueChange(values[it % listSize]) | |
} | |
} | |
Box( | |
modifier = modifier, | |
contentAlignment = Alignment.Center, | |
) { | |
var edit by rememberSaveable { mutableStateOf(false) } | |
ComposeScope { | |
AnimatedContent( | |
targetState = edit, | |
label = "AnimatedContent", | |
) { showTextField -> | |
if (showTextField) { | |
var isError by rememberSaveable { mutableStateOf(false) } | |
val initialSelectedItem = remember { | |
values[lazyListState.firstVisibleItemIndex % listSize] | |
} | |
var value by rememberSaveable { | |
mutableStateOf(initialSelectedItem.format()) | |
} | |
val focusRequester = remember { FocusRequester() } | |
LaunchedEffect(key1 = Unit) { | |
focusRequester.requestFocus() | |
} | |
val coroutineScope = rememberCoroutineScope() | |
ComposeScope { | |
TextField( | |
modifier = Modifier | |
.fillMaxWidth() | |
.focusRequester(focusRequester), | |
value = value, | |
onValueChange = { string -> | |
value = string | |
parse?.invoke(string).let { item -> | |
isError = kotlin.run { | |
if (item != null) !values.contains(item) // true: item not found | |
else true // string cannot be parsed | |
} | |
if (isError) onValueChange(initialSelectedItem) | |
else onValueChange(item ?: initialSelectedItem) | |
onIsErrorChange(isError) | |
} | |
}, | |
textStyle = textStyle.copy(textAlign = TextAlign.Center), | |
keyboardOptions = KeyboardOptions.Default.copy( | |
keyboardType = keyboardType, | |
imeAction = if (!isError) ImeAction.Done else ImeAction.Default, | |
), | |
keyboardActions = KeyboardActions( | |
onDone = { | |
if (!isError) { | |
parse?.invoke(value)?.let { item -> | |
scrollOfItem(item)?.let { scroll -> | |
coroutineScope.launch { | |
lazyListState.scrollToItem(scroll) | |
} | |
} | |
} | |
edit = false | |
} | |
}), | |
isError = isError, | |
colors = TextFieldDefaults.colors().copy( | |
focusedContainerColor = Color.Transparent, | |
unfocusedContainerColor = Color.Transparent, | |
errorContainerColor = Color.Transparent, | |
focusedIndicatorColor = Color.Transparent, | |
unfocusedIndicatorColor = Color.Transparent, | |
errorIndicatorColor = Color.Transparent, | |
errorTextColor = MaterialTheme.colorScheme.error, | |
), | |
) | |
} | |
} else { | |
val itemHeight = textStyle.lineHeight.toDp() + verticalPadding * 2F | |
LazyColumn( | |
state = lazyListState, | |
flingBehavior = rememberSnapFlingBehavior(lazyListState = lazyListState), | |
horizontalAlignment = Alignment.CenterHorizontally, | |
modifier = Modifier | |
.fillMaxWidth() | |
.height(itemHeight * visibleItemsCount) | |
.fadingEdge( | |
brush = remember { | |
Brush.verticalGradient( | |
0F to Color.Transparent, 0.5F to Color.Black, 1F to Color.Transparent | |
) | |
}, | |
), | |
) { | |
items( | |
count = intervals.last(), | |
key = { it }, | |
) { index -> | |
val enabled by remember(key1 = index, key2 = enableEdition) { | |
derivedStateOf { | |
enableEdition && (index == (lazyListState.firstVisibleItemIndex + coercedOutOfBoundsPageCount)) | |
} | |
} | |
val textModifier = Modifier.padding(vertical = verticalPadding) | |
when (index) { | |
in intervals[0]..<intervals[1] -> Text( | |
text = if (wrapSelectorWheel) values[(index - coercedOutOfBoundsPageCount + listSize) % listSize].format() else "", | |
maxLines = 1, | |
overflow = TextOverflow.Ellipsis, | |
style = textStyle, | |
modifier = textModifier, | |
) | |
in intervals[1]..<intervals[2] -> { | |
Text( | |
text = values[(index - coercedOutOfBoundsPageCount) % listSize].format(), | |
maxLines = 1, | |
overflow = TextOverflow.Ellipsis, | |
style = textStyle, | |
modifier = textModifier.then( | |
Modifier.clickable( | |
onClick = { edit = true }, | |
enabled = enabled, | |
) | |
), | |
) | |
} | |
in intervals[2]..<intervals[3] -> Text( | |
text = if (wrapSelectorWheel) values[(index - coercedOutOfBoundsPageCount) % listSize].format() else "", | |
maxLines = 1, | |
overflow = TextOverflow.Ellipsis, | |
style = textStyle, | |
modifier = textModifier, | |
) | |
} | |
} | |
} | |
HorizontalDivider( | |
modifier = Modifier.offset(y = itemHeight * coercedOutOfBoundsPageCount - dividerThickness / 2), | |
thickness = dividerThickness, | |
color = dividerColor, | |
) | |
HorizontalDivider( | |
modifier = Modifier.offset(y = itemHeight * (coercedOutOfBoundsPageCount + 1) - dividerThickness / 2), | |
thickness = dividerThickness, | |
color = dividerColor, | |
) | |
} | |
} | |
} | |
} | |
} | |
@Preview(widthDp = 300) | |
@Composable | |
fun PreviewListPicker1() { | |
MaterialTheme { | |
Surface(color = MaterialTheme.colorScheme.primary) { | |
var value by remember { mutableStateOf(LocalDate.now()) } | |
val list = remember { | |
buildList { | |
repeat(10) { | |
add(LocalDate.now().minusDays((it - 5).toLong())) | |
} | |
} | |
} | |
ListPicker( | |
initialValue = value, | |
values = list, | |
onValueChange = { value = it }, | |
wrapSelectorWheel = true, | |
format = { | |
format( | |
DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(Locale.getDefault()), | |
) | |
}, | |
onIsErrorChange = {}, | |
textStyle = MaterialTheme.typography.labelLarge, | |
verticalPadding = 8.dp, | |
keyboardType = KeyboardType.Number, | |
) | |
} | |
} | |
} | |
@Preview(widthDp = 100) | |
@Composable | |
fun PreviewListPicker2() { | |
MaterialTheme { | |
Surface(color = MaterialTheme.colorScheme.tertiary) { | |
var value by remember { mutableStateOf("5") } | |
val list = remember { (1..10).map { it.toString() } } | |
ListPicker( | |
initialValue = value, | |
values = list, | |
onValueChange = { value = it }, | |
modifier = Modifier, | |
onIsErrorChange = {}, | |
beyondViewportPageCount = 2, | |
textStyle = MaterialTheme.typography.labelLarge, | |
verticalPadding = 8.dp, | |
keyboardType = KeyboardType.Number, | |
) | |
} | |
} | |
} | |
@Preview | |
@Composable | |
fun PreviewListPicker3() { | |
MaterialTheme { | |
Column(horizontalAlignment = Alignment.CenterHorizontally) { | |
var value by remember { mutableIntStateOf(5) } | |
val list = remember { (1..10).map { it } } | |
Surface(color = MaterialTheme.colorScheme.primary) { | |
Text( | |
text = "Selected value: $value", | |
textAlign = TextAlign.Center, | |
modifier = Modifier | |
.fillMaxWidth() | |
.padding(8.dp), | |
style = MaterialTheme.typography.bodyMedium, | |
) | |
} | |
Spacer(modifier = Modifier.height(16.dp)) | |
Surface { | |
ListPicker( | |
initialValue = value, | |
values = list, | |
onValueChange = { value = it }, | |
format = { this.toString() }, | |
parse = { | |
try { | |
takeIf { it.matches(integerOnlyRegex) }?.toInt() | |
} catch (_: NumberFormatException) { | |
null | |
} | |
}, | |
onIsErrorChange = {}, | |
beyondViewportPageCount = 2, | |
textStyle = MaterialTheme.typography.labelLarge, | |
verticalPadding = 8.dp, | |
keyboardType = KeyboardType.Number, | |
) | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.inidamleader.ovtracker.util.compose | |
import androidx.compose.runtime.Composable | |
@Composable | |
fun ComposeScope(content: @Composable () -> Unit) { | |
content() | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.inidamleader.ovtracker.util.compose.geometry | |
import androidx.compose.runtime.Composable | |
import androidx.compose.ui.geometry.Offset | |
import androidx.compose.ui.geometry.Size | |
import androidx.compose.ui.geometry.isSpecified | |
import androidx.compose.ui.platform.LocalDensity | |
import androidx.compose.ui.unit.Density | |
import androidx.compose.ui.unit.Dp | |
import androidx.compose.ui.unit.DpOffset | |
import androidx.compose.ui.unit.DpSize | |
import androidx.compose.ui.unit.IntOffset | |
import androidx.compose.ui.unit.IntSize | |
import androidx.compose.ui.unit.TextUnit | |
import androidx.compose.ui.unit.dp | |
import androidx.compose.ui.unit.isSpecified | |
import kotlin.math.roundToInt | |
// Those functions are designed to be used in lambdas | |
// DP | |
fun Density.dpToSp(dp: Dp) = if (dp.isSpecified) dp.toSp() else TextUnit.Unspecified | |
fun Density.dpToFloatPx(dp: Dp) = if (dp.isSpecified) dp.toPx() else Float.NaN | |
fun Density.dpToIntPx(dp: Dp) = if (dp.isSpecified) dp.toPx().toInt() else 0 | |
fun Density.dpRoundToPx(dp: Dp) = if (dp.isSpecified) dp.roundToPx() else 0 | |
@Composable | |
fun Dp.toSp() = LocalDensity.current.dpToSp(this) | |
@Composable | |
fun Dp.toFloatPx() = LocalDensity.current.dpToFloatPx(this) | |
@Composable | |
fun Dp.toIntPx() = LocalDensity.current.dpToIntPx(this) | |
@Composable | |
fun Dp.roundToPx() = LocalDensity.current.dpRoundToPx(this) | |
fun Dp.toRecDpSize() = if (isSpecified) DpSize(this, this) else DpSize.Unspecified | |
fun Dp.toRecDpOffset() = if (isSpecified) DpOffset(this, this) else DpOffset.Unspecified | |
// TEXT UNIT | |
fun Density.spToDp(sp: TextUnit) = if (sp.isSpecified) sp.toDp() else Dp.Unspecified | |
fun Density.spToFloatPx(sp: TextUnit) = if (sp.isSpecified) sp.toPx() else Float.NaN | |
fun Density.spToIntPx(sp: TextUnit) = if (sp.isSpecified) sp.toPx().toInt() else 0 | |
fun Density.spRoundToPx(sp: TextUnit) = if (sp.isSpecified) sp.roundToPx() else 0 | |
@Composable | |
fun TextUnit.toDp() = LocalDensity.current.spToDp(this) | |
@Composable | |
fun TextUnit.toFloatPx() = LocalDensity.current.spToFloatPx(this) | |
@Composable | |
fun TextUnit.toIntPx() = LocalDensity.current.spToIntPx(this) | |
@Composable | |
fun TextUnit.roundToPx() = LocalDensity.current.spRoundToPx(this) | |
// FLOAT | |
fun Density.floatPxToDp(px: Float) = if (px.isFinite()) px.toDp() else Dp.Unspecified | |
fun Density.floatPxToSp(px: Float) = if (px.isFinite()) px.toSp() else TextUnit.Unspecified | |
@Composable | |
fun Float.toDp() = LocalDensity.current.floatPxToDp(this) | |
@Composable | |
fun Float.toSp() = LocalDensity.current.floatPxToSp(this) | |
fun Float.toIntPx() = if (isFinite()) toInt() else 0 | |
fun Float.roundToPx() = if (isFinite()) roundToInt() else 0 | |
fun Float.toRecSize() = if (isFinite()) Size(this, this) else Size.Unspecified | |
fun Float.toRecOffset() = if (isFinite()) Offset(this, this) else Offset.Unspecified | |
// INT | |
fun Density.intPxToDp(px: Int) = px.toDp() | |
fun Density.intPxToSp(px: Int) = px.toSp() | |
@Composable | |
fun Int.toDp() = LocalDensity.current.intPxToDp(this) | |
@Composable | |
fun Int.toSp() = LocalDensity.current.intPxToSp(this) | |
fun Int.toFloatPx() = toFloat() | |
fun Int.toRecIntSize() = IntSize(this, this) | |
fun Int.toRecIntOffset() = IntOffset(this, this) | |
// DP SIZE | |
fun Density.dpSizeToIntSize(dpSize: DpSize) = | |
if (dpSize.isSpecified) IntSize(dpSize.width.toPx().toInt(), dpSize.height.toPx().toInt()) | |
else IntSize.Zero | |
fun Density.dpSizeRoundToIntSize(dpSize: DpSize) = | |
if (dpSize.isSpecified) IntSize(dpSize.width.roundToPx(), dpSize.height.roundToPx()) | |
else IntSize.Zero | |
fun Density.dpSizeToSize(dpSize: DpSize) = | |
if (dpSize.isSpecified) Size(dpSize.width.toPx(), dpSize.height.toPx()) | |
else Size.Unspecified | |
@Composable | |
fun DpSize.toIntSize() = LocalDensity.current.dpSizeToIntSize(this) | |
@Composable | |
fun DpSize.roundToIntSize() = LocalDensity.current.dpSizeRoundToIntSize(this) | |
@Composable | |
fun DpSize.toSize() = LocalDensity.current.dpSizeToSize(this) | |
fun DpSize.isSpaced() = isSpecified && width > 0.dp && height > 0.dp | |
// SIZE | |
fun Density.sizeToDpSize(size: Size) = | |
if (size.isSpecified) DpSize(size.width.toDp(), size.height.toDp()) | |
else DpSize.Unspecified | |
@Composable | |
fun Size.toDpSize() = | |
if (isSpecified) LocalDensity.current.sizeToDpSize(this) | |
else DpSize.Unspecified | |
fun Size.toIntSize() = | |
if (isSpecified) IntSize(width.toInt(), height.toInt()) | |
else IntSize.Zero | |
fun Size.isSpaced() = isSpecified && width > 0F && height > 0F | |
// INT SIZE | |
fun Density.intSizeToDpSize(intSize: IntSize) = DpSize(intSize.width.toDp(), intSize.height.toDp()) | |
@Composable | |
fun IntSize.toDpSize() = LocalDensity.current.intSizeToDpSize(this) | |
@Composable | |
fun IntSize.toSize() = Size(width.toFloat(), height.toFloat()) | |
fun IntSize.isSpaced() = width > 0 && height > 0 | |
// DP OFFSET | |
fun Density.dpOffsetToIntOffset(dpOffset: DpOffset) = | |
if (dpOffset.isSpecified) IntOffset(dpOffset.x.toPx().toInt(), dpOffset.y.toPx().toInt()) | |
else IntOffset.Zero | |
fun Density.dpOffsetRoundToIntOffset(dpOffset: DpOffset) = | |
if (dpOffset.isSpecified) IntOffset(dpOffset.x.roundToPx(), dpOffset.y.roundToPx()) | |
else IntOffset.Zero | |
fun Density.dpOffsetToOffset(dpOffset: DpOffset) = | |
if (dpOffset.isSpecified) Offset(dpOffset.x.toPx(), dpOffset.y.toPx()) | |
else Offset.Unspecified | |
@Composable | |
fun DpOffset.toIntOffset() = LocalDensity.current.dpOffsetToIntOffset(this) | |
@Composable | |
fun DpOffset.roundToIntOffset() = LocalDensity.current.dpOffsetRoundToIntOffset(this) | |
@Composable | |
fun DpOffset.toOffset() = LocalDensity.current.dpOffsetToOffset(this) | |
// OFFSET | |
fun Density.offsetToDpOffset(offset: Offset) = | |
if (offset.isSpecified) DpOffset(offset.x.toDp(), offset.y.toDp()) | |
else DpOffset.Unspecified | |
@Composable | |
fun Offset.toDpOffset() = LocalDensity.current.offsetToDpOffset(this) | |
fun Offset.toIntOffset() = | |
if (isSpecified) IntOffset(x.toInt(), y.toInt()) | |
else IntOffset.Zero | |
// INT OFFSET | |
fun Density.intOffsetToDpOffset(intOffset: IntOffset) = DpOffset(intOffset.x.toDp(), intOffset.y.toDp()) | |
@Composable | |
fun IntOffset.toDpOffset() = LocalDensity.current.intOffsetToDpOffset(this) | |
fun IntOffset.toOffset() = Offset(x.toFloat(), y.toFloat()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Stable | |
fun Modifier.fadingEdge(brush: Brush) = this | |
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) | |
.drawWithContent { | |
drawContent() | |
drawRect(brush = brush, blendMode = BlendMode.DstIn) | |
} |
Hey! I have issues with updating the ListPicker state on data loads. This is my UI component and the associated state.
@Composable
private fun WeightPicker(
modifier: Modifier = Modifier,
state: WeightPickerState
) {
Log.d("WP", "recompose: ${state.kilos}.${state.grams} kg")
Row(
modifier = modifier,
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Spacer(modifier = Modifier.weight(1f))
ListPicker(
modifier = Modifier.weight(1f),
initialValue = state.kilos,
values = (1..300).toList(),
onValueChange = { state.kilos = it },
)
Text(text = ".")
ListPicker(
modifier = Modifier.weight(1f),
initialValue = state.grams,
values = (0..9).toList(),
onValueChange = { state.grams = it },
)
Spacer(modifier = Modifier.weight(1f))
}
}
@Stable
private class WeightPickerState(
defaultKilos: Int = 75,
defaultGrams: Int = 0,
) {
var kilos by mutableIntStateOf(defaultKilos)
var grams by mutableIntStateOf(defaultGrams)
val concatenatedDouble: Double
get() {
return kilos + (grams / 10.0)
}
fun updateWeight(weight: Double) {
Log.i("WEIGHT", "new weight: $weight")
val newKilos = weight.toInt()
val newGrams = ((weight - weight.toInt()) * 10).toInt()
if (kilos != newKilos) {
Log.i("WEIGHT", "updated kilos: $kilos -> $newKilos")
kilos = newKilos
}
if (grams != newGrams) {
Log.i("WEIGHT", "updated grams: $grams -> $newGrams")
grams = newGrams
}
}
}
@Composable
private fun rememberWeightPickerState(initial: Double): WeightPickerState {
val state by remember { mutableStateOf(WeightPickerState()) }
LaunchedEffect(initial) {
Log.d("WeightPicker", "Updating weight picker with: $initial")
state.updateWeight(initial)
}
return state
}
// Usage.
val latestWeight by viewModel.latestWeight.collectAsState(initial = 75.0)
val weightPickerState = rememberWeightPickerState(initial = latestWeight)
WeightPicker(
modifier = Modifier.fillMaxWidth(),
state = weightPickerState,
)
I see log output for the initial value of 75.0 and the latest weight fetched with 161.0. But the UI never updates. It looks like ListPicker does not update on initialValue changes to me. Is that the case?
recompose: 75.0 kg
...
recompose: 161.0 kg
I think the recomposition chain works, just that the ListPicker doesn't update its state.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@inidamleader a great piece of work, thank you! Didn't you think about releasing it as a library? Maintenance and issue tracking would be easier, plus more people could get involved.