I get some data from api in kotlin and save it to room. I do the saving to Room in the viewmodel of the splash screen. However, each application saves the data to the room when it is opened, I want it to save only once. In this case I tried to do something with shared preferences but I couldn't implement it. Any ideas on this or anyone who has done something similar to this before?
hear is my code
my splash screen ui
#Composable
fun SplashScreen(
navController: NavController,
viewModel : SplashScreenViewModel = hiltViewModel()
) {
val context: Context = LocalContext.current
checkDate(context,viewModel)
val scale = remember {
Animatable(0f)
}
LaunchedEffect(key1 = true) {
scale.animateTo(
targetValue = 0.3f,
animationSpec = tween(
durationMillis = 500,
easing = {
OvershootInterpolator(2f).getInterpolation(it)
}
)
)
delay(2000L)
navController.navigate(Screen.ExchangeMainScreen.route)
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = R.drawable.ic_currency_exchange_logo_512),
contentDescription = "Logo"
)
}
}
fun checkDate(context:Context,viewModel: SplashScreenViewModel){
val sharedPreferences = context.getSharedPreferences("date",Context.MODE_PRIVATE)
val editor = sharedPreferences.edit()
val date = Date(System.currentTimeMillis())
val millis = date.time
editor.apply{
putLong("currentDate", millis)
}.apply()
val sharedDate = sharedPreferences.getLong("currentDate",1)
println(sharedDate)
val isDayPassed = (System.currentTimeMillis() - sharedDate) >= TimeUnit.DAYS.toMillis(1)
if(isDayPassed){
viewModel.update()
}
}
}
my splash screen view model
#HiltViewModel
class SplashScreenViewModel #Inject constructor(
private val insertExchangeUseCase: InsertExchangeUseCase,
private val updateExchangeUseCase: UpdateExchangeUseCase,
private val getAllExchangeUseCase: GetAllExchangeUseCase
) : ViewModel() {
init {
insertExchanges()
}
private fun insertExchanges() {
viewModelScope.launch {
insertExchangeUseCase.insert(DEFAULT_CURRENCY)
}
}
fun update(){
viewModelScope.launch {
updateExchangeUseCase.updateExchange(getAllExchangeUseCase.get(DEFAULT_CURRENCY))
}
}
}
In view model, insert is started automatically in init.
You can’t do something like this properly inside a Composable. Composables are only for creating UI. Remember the rule about no side effects. A function like your checkDate() should be called directly in onCreate() so it isn't called over and over as your Composition recomposes.
The problem with your current checkDate function: You save the "currentDate" every single this time this function is called, and you do it before checking what value was saved there before, so it will perpetually overwrite the old value before you can use it. You're also overcomplicating things by transforming the Long time into a Date and back to a Long for no reason.
Think about the logic of what you're doing:
Update the data if it has been a day since the last time you updated it.
So to implement this, we first check if the previously saved date is more than a day old. Only if it is old do we write the new date and update the data.
Here's a basic implementation. This is assuming you want to update when it has been at least 24 hours since the last update, since that seems like what you were trying to do.
fun checkDate(context: Context, viewModel: SplashScreenViewModel) {
val sharedPreferences = context.getSharedPreferences("date", Context.MODE_PRIVATE)
val lastTimeUpdatedKey = "lastTimeUpdated"
val now = System.currentTimeMillis()
val lastTimeUpdated = sharedPreferences.getLong(lastTimeUpdatedKey, 0)
val isDayPassed = now - lastTimeUpdated > TimeUnit.DAYS.toMillis(1)
if (isDayPassed) {
sharedPreferences.edit { // this is the shortcut way to edit and apply all in one
putLong(lastTimeUpdatedKey, today)
}
viewModel.update()
}
}
I would also change "date" to something longer and more unique so you don't accidentally use it somewhere else for different shared preferences. And I would rename your update() function to something more descriptive.
Don't forget to move the function call out of your Composable!
Basically I have a launched effect reading the item offset of a lazy column. and based on the offset I change the height/alpha etc..
AS is warning me that it should not be read inside a compose function because it will change a lot, so my question is where should I be reading and what is the best practice here?
Should I make a lambda updating the offset outside of the function and read in the highest one? Or the parent one?
Open to suggestions
You can read it inside a derivedStateOf.
val someData = remember {
derivedStateOf {
val offset = lazyListState.firstVisibleItemScrollOffset
val firstVisibleItem = lazyListState.firstVisibleItemIndex
// Convert to some data here and read this data
}
}
I did similar thing in this library for animating color, scale and alpha of items with
val animationData by remember {
derivedStateOf {
val animationData = getAnimationProgress(
lazyListState = lazyListState,
initialFirstVisibleIndex = initialFirstVisibleIndex,
indexOfSelector = indexOfSelector,
itemScaleRange = itemScaleRange,
showPartialItem = showPartialItem,
globalIndex = globalIndex,
selectedIndex = selectedIndex,
availableSpace = availableSpace,
itemSize = itemSizePx,
spaceBetweenItems = spaceBetweenItems,
visibleItemCount = visibleItemCount,
totalItemCount = totalItemCount,
inactiveScale = inactiveItemScale,
inactiveColor = inactiveColor,
activeColor = activeColor
)
selectedIndex = animationData.globalItemIndex
animationData
}
}
If you read a change you can read LaunchedEffect with snapshotFlow either but as i checked same lazyListState is returned so it's not possible to read any change but you can read lazyListState.firstVisibleItemScrollOffset or any changing value as alternative to derivedStateOf.
For instance
LaunchedEffect(Unit){
snapshotFlow { lazyListState.firstVisibleItemScrollOffset }
.onEach {
// Here we get the change in offset of first visible item
// You might read another value here and do calculation
lazyListState.firstVisibleItemIndex
}.launchIn(this)
}
I think reading lazyListState wouldn't pose any problems either. I use this when i only read one value like firstVisibleItemScrollOffset, or layoutInfo inside snapshotFlow
For a LazyRow, or Column, how to I know whether the user has scrolled left or right ( or up or... you know). We do not need callbacks in compose for stuff like that, since mutableStateOf objects always anyway trigger recompositions so I just wish to know a way to store it in a variable. Okay so there's lazyRowState.firstVisibleItemScrollOffset, which can be used to mesaure it in a way, but I can't find a way to store its value first, and then subtract the current value to retrieve the direction (based on positive or negative change). Any ideas on how to do that, thanks
Currently there is no built-in function to get this info from LazyListState.
You can use something like:
#Composable
private fun LazyListState.isScrollingUp(): Boolean {
var previousIndex by remember(this) { mutableStateOf(firstVisibleItemIndex) }
var previousScrollOffset by remember(this) { mutableStateOf(firstVisibleItemScrollOffset) }
return remember(this) {
derivedStateOf {
if (previousIndex != firstVisibleItemIndex) {
previousIndex > firstVisibleItemIndex
} else {
previousScrollOffset >= firstVisibleItemScrollOffset
}.also {
previousIndex = firstVisibleItemIndex
previousScrollOffset = firstVisibleItemScrollOffset
}
}
}.value
}
Then just use listState.isScrollingUp() to get the info about the scroll.
This snippet is used in a google codelab.
Got it
{ //Composable Scope
val lazyRowState = rememberLazyListState()
val pOffset = remember { lazyRowState.firstVisibleItemScrollOffset }
val direc = lazyRowState.firstVisibleItemScrollOffset - pOffset
val scrollingRight /*or Down*/ = direc > 0 // Tad'aa
}
As in https://developer.android.com/jetpack/compose/side-effects, we have derivedStateOf which helps prevent unnecessary Recomposition.
With that, it makes me think, should we always use derivedStateOf instead of remember(value) as below?
// Instead of using
remember(value1) { ... }
// Use
remember { derivedStateOf {value1} }
Doing so will also ensure we don't recompose the function that contains the mutableState, but just all the composable functions using that mutableState.
Is there any downside of using derivedStateOf as opposed to remember(value)?
It is not recommended that you replace them all. If the object is re assigned, use remember. If the object content changes, use derivedstateof
If you replace them all with derivedStateOf, object changes will not be detected
remember is more flexible in some situations:
You can have mutableState inside, which can be updated between reconfigurations and reset to the original value when value1 (or other keys) have changed.
There may be a situation where you only need to update the value when some of the states used in the computation change, not any of them, in which case you can specify them in remember.
On the other hand, derivedStateOf also has its advantages:
It doesn't cause recomposition unless the result of the computation has actually changed.
It can be used outside the composite function, for example in a view model.
In general, if using derivedStateOf gives the expected behavior, you should prefer it over remember.
derivedStateOf is for observing a change or or a property change in State.
if value1 in both cases is a class that is not State derivedStateOf won't update
#Composable
private fun DerivedStateOfExample() {
var numberOfItems by remember {
mutableStateOf(0)
}
val count = numberOfItems
val rememberedCount = remember(count) {
count
}
val derivedCountWithoutState by remember {
derivedStateOf {
count
}
}
val derivedCountWithState by remember {
derivedStateOf {
numberOfItems
}
}
// Use derivedStateOf when a certain state is calculated or derived from other state objects.
// Using this function guarantees that the calculation will only occur whenever one
// of the states used in the calculation changes.
val derivedStateMax by remember {
derivedStateOf {
numberOfItems > 5
}
}
val newMax = remember(count > 5) {
count
}
val derivedStateMin by remember {
derivedStateOf {
numberOfItems > 0
}
}
Column(modifier = Modifier.padding(horizontal = 8.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = "Items: $numberOfItems\n" +
"derivedCountWithoutState: $derivedCountWithoutState\n" +
"derivedCountWithState: $derivedCountWithState\n" +
"newMax:$newMax\n" +
"rememberedCount:$rememberedCount, count: $count",
modifier = Modifier.weight(1f)
)
IconButton(onClick = { numberOfItems++ }) {
Icon(imageVector = Icons.Default.Add, contentDescription = "add")
}
Spacer(modifier = Modifier.width(4.dp))
IconButton(onClick = { if (derivedStateMin) numberOfItems-- }) {
Icon(imageVector = Icons.Default.Remove, contentDescription = "remove")
}
}
if (derivedStateMax) {
Text("Can't be more than 5 items", color = Color(0xffE53935))
}
}
}
derivedCountWithState won't update because it doesn't observe a state. Same goes for derivedStateMax when the value was not derived from numberOfItems but count > 5
Also newMax is updated when when remember(count > 5) turns from false to true. This is where derivedStateOf comes handy, you can check change in each update of State with a condition.
I want to show a slider that can be either updated by the user using drag/drop, or updated from the server in real-time.
When the user finishes their dragging gesture, I want to send the final value to the server.
My initial attempt is:
#Composable
fun LightView(
channel: UiChannel,
onDimmerChanged: (Float) -> Unit
) {
val sliderValue = channel....
Slider(value = sliderValue, onValueChange = onDimmerChanged)
}
And the onDimmerChanged method comes from my ViewModel, which updates the server value.
It works well, however onValueChange is called for each move, which bombards the server with unneeded requests.
I tried to create a custom slider:
#Composable
fun LightView(
channel: UiChannel,
onDimmerChanged: (Float) -> Unit
) {
val sliderValue = channel....
Slider(initialValue = sliderValue, valueSetter = onDimmerChanged)
}
#Composable
fun Slider(initialValue: Float, valueSetter: (Float) -> Unit) {
var value by remember { mutableStateOf(initialValue) }
Slider(
value = value,
onValueChange = { value = it },
onValueChangeFinished = { valueSetter(value) }
)
}
It works well on the app side, and the value is only sent once, when the user stops dragging.
However, it fails updating the view when there is an update from the server. I'm guessing this has something to do with remember. So I tried without remember:
#Composable
fun Slider(initialValue: Float, valueSetter: (Float) -> Unit) {
var value by mutableStateOf(initialValue)
Slider(
value = value,
onValueChange = { value = it },
onValueChangeFinished = { valueSetter(value) }
)
}
This time the view updates correctly when the value is updated by the server, but does not move anymore when the user drags the slider.
I'm sure I'm missing something with state hoisting and all, but I can't figure out what.
So my final question: how to create a Slider that can be either updated by the ViewModel, or by the user, and notifies the ViewModel of a new value only when the user finishes dragging?
EDIT:
I also tried what #CommonsWare suggested:
#Composable
fun LightView(
channel: UiChannel,
onDimmerChanged: (Float) -> Unit
) {
val sliderValue = channel....
val sliderState = mutableStateOf(sliderValue)
Slider(state = sliderState, valueSet = { onDimmerChanged(sliderState.value) })
}
#Composable
fun Slider(state: MutableState<Float>, valueSet: () -> Unit) {
Slider(
value = state.value,
onValueChange = { state.value = it },
onValueChangeFinished = valueSet
)
}
And it does not work either. When using drag and drop, sliderState is correctly updated, and onDimmerChanged() is called with the correct value. But for some reason, when tapping of the sliding (rather than sliding), valueSet is called and sliderState.value does not contain the correct value. I don't understand where this value comes from.
Regarding the original problem with local & server (or viewmodel) states conflicting with eachother:
I solved it for me by detecting wether or not we are interacting with the slider:
if yes, then show and update the local state value
or if not - then show the viewmodels value.
As you have said, we should never update the viewmodel from onValueChange - as this is only for updating the sliders value locally (documentation). Instead onValueChangeFinished is used for sending the current local state to the viewmodel, as soon as we are done interacting.
Regarding detection of current interaction, we can make use of InteractionSource.
Working example:
#Composable
fun SliderDemo() {
// In this demo, ViewModel updates its progress periodically from 0f..1f
val viewModel by remember { mutableStateOf(SliderDemoViewModel()) }
Column(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// local slider value state
var sliderValueRaw by remember { mutableStateOf(viewModel.progress) }
// getting current interaction with slider - are we pressing or dragging?
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
val isDragged by interactionSource.collectIsDraggedAsState()
val isInteracting = isPressed || isDragged
// calculating actual slider value to display
// depending on wether we are interacting or not
// using either the local value, or the ViewModels / server one
val sliderValue by derivedStateOf {
if (isInteracting) {
sliderValueRaw
} else {
viewModel.progress
}
}
Slider(
value = sliderValue, // using calculated sliderValue here from above
onValueChange = {
sliderValueRaw = it
},
onValueChangeFinished = {
viewModel.updateProgress(sliderValue)
},
interactionSource = interactionSource
)
// Debug interaction info
Text("isPressed: ${isPressed} | isDragged: ${isDragged}")
}
}
Hope that helps.
There are a couple of things going on here, so let's try to break it down.
The initial attempt where onDimmerChanged is called for every value change looks great!
Looking at the second attempt, creating a custom Slider component works, but there are a few issues.
#Composable
fun LightView(
channel: UiChannel,
onDimmerChanged: (Float) -> Unit
) {
val sliderValue = channel....
Slider(initialValue = sliderValue, valueSetter = onDimmerChanged)
}
#Composable
fun Slider(initialValue: Float, valueSetter: (Float) -> Unit) {
var value by remember { mutableStateOf(initialValue) }
Slider(
value = value,
onValueChange = { value = it },
onValueChangeFinished = { valueSetter(value) }
)
}
Let's talk about what happens in the Slider composable here:
We remember the state with the initialValue
The channel value is updated, LightView gets recomposed, so does Slider
Since we remembered the state, it is still set to the previous initialValue
You're right with your thought about remember being the culprit here. When memorizing a value, it won't be updated when recomposing unless we tell Compose to. But without memorization (as seen in your third attempt), a state model will be created with every recomposition (var value by mutableStateOf(initialValue)) using the initialValue. Since a recomposition is triggered every time value changes, we will always pass the initialValue instead of the updated value to the Slider, causing it to never update by changes from within this Composable.
Instead, we want to pass initialValue as a key to remember, telling Compose to recalculate the value whenever the key changes.
#Composable
fun Slider(initialValue: Float, valueSetter: (Float) -> Unit) {
var value by remember { mutableStateOf(initialValue) }
Slider(
value = value,
onValueChange = { value = it },
onValueChangeFinished = { valueSetter(value) }
)
}
You can probably also just pull the Slider into your LightView:
#Composable
fun LightView(
channel: UiChannel,
onDimmerChanged: (Float) -> Unit
) {
var value by remember(channel.sliderValue) { mutableStateOf(channel.sliderValue) }
Slider(
value = value,
onValueChange = { value = it },
onValueChangeFinished = { onDimmerChanged(value) }
)
}
Lastly, about the attempt that #commonsware suggested.
#Composable
fun LightView(
channel: UiChannel,
onDimmerChanged: (Float) -> Unit
) {
val sliderValue = channel....
val sliderState = mutableStateOf(sliderValue)
Slider(state = sliderState, valueSet = { onDimmerChanged(sliderState.value) })
}
#Composable
fun Slider(state: MutableState<Float>, valueSet: () -> Unit) {
Slider(
value = state.value,
onValueChange = { state.value = it },
onValueChangeFinished = valueSet
)
}
This is another way of doing the same thing, but passing around MutableStates is an anti-pattern and should be avoided if possible. This is where state hoisting helps (what you were trying to do in the earlier attempts :))
Finally, about the Slider's onValueChangeFinished using the wrong value.
And it does not work either. When using drag and drop, sliderState is correctly updated, and onDimmerChanged() is called with the correct value. But for some reason, when tapping of the sliding (rather than sliding), valueSet is called and sliderState.value does not contain the correct value. I don't understand where this value comes from.
This is a bug in the Slider component. You can check this by looking at the output of this code:
#Composable
fun Slider(initialValue: Float, valueSetter: (Float) -> Unit) {
var value by remember { mutableStateOf(initialValue) }
val key = Random.nextInt()
Slider(
value = value,
onValueChange = { value = it },
onValueChangeFinished = {
valueSetter(value)
println("Callback invoked. Current key: $key")
}
)
}
Since key should change with every recomposition, you can see that the onValueChangeFinished callback holds a reference to an older composition (hope I put that right). So you weren't going crazy, it's not your fault :)
Hope that helped clear things up a bit!
Yeah, I can reproduce your issue as well. I answered in the bug topic, but I'll copy-paste it here so other people can fix it if they're in a rush. This solution sadly involves copying the whole Slider class.
The problem is that the drag and click modifiers use different Position instances, although they should always be the same (as Position is being created inside of remember).
The issue is with the pointer input modifier.
val press = if (enabled) {
Modifier.pointerInput(Unit) {...}
Change the Modifier.pointerInput() to accept any other subject different than Unit so your Position instance in that Modifier is always up to date and updated when Position gets recreated. For example, you can change it to Modifier.pointerInput(valueRange)
I encountered this problem. As jossiwolf pointed out, using the progress as a key for the remember{} is necessary to ensure that the Slider progress is updated after recomposition.
I had an additional issue though, where, if I updated the progress mid-seek, the Slider would recompose, and the thumb would revert back to its old position.
To work around this, I'm using a temporary slider position, which is only used while a drag is in progress:
#Composable
fun MySlider(
progress: Float,
onSeek: (progress: Float) -> Unit,
) {
val sliderPosition = remember(progress) { mutableStateOf(progress) }
val tempSliderPosition = remember { mutableStateOf(progress) }
val interactionSource = remember { MutableInteractionSource() }
val isDragged = interactionSource.collectIsDraggedAsState()
Slider(
value = if (isDragged.value) tempSliderPosition.value else sliderPosition.value,
onValueChange = { progress ->
sliderPosition.value = progress
tempSliderPosition.value = progress
},
onValueChangeFinished = {
sliderPosition.value = tempSliderPosition.value
onSeek(tempSliderPosition.value)
},
interactionSource = interactionSource
)
}
I've elaborated on Steffen's answer a bit so it is possible to update the "external" "viewModel" value even while the user is currently dragging the slider (as there are many use cases when that is required).
The usage is the same as the compose "native" Slider, with the exception that the onValueChangeFinished parameter now accepts the finishing float value.
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsDraggedAsState
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.material.Slider
import androidx.compose.material.SliderColors
import androidx.compose.material.SliderDefaults
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
/**
* "Wrapper" around the Slider solving the issue of overwriting values during drag. See https://stackoverflow.com/questions/66386039/jetpack-compose-react-to-slider-changed-value.
* Use as normal Slider and feel free to update the underlying value in the onValueChange method.
*/
#Composable
fun ComposeSlider(
value: Float,
onValueChange: (Float) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
steps: Int = 0,
onValueChangeFinished: ((Float) -> Unit)? = null,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
colors: SliderColors = SliderDefaults.colors()
) {
// local slider value state with default value (from the "external" source)
var sliderValueRaw by remember { mutableStateOf(value) }
// getting current interaction with slider - are we pressing or dragging?
val isPressed by interactionSource.collectIsPressedAsState()
val isDragged by interactionSource.collectIsDraggedAsState()
val isInteracting = isPressed || isDragged
// calculating actual slider value to display depending on whether we are interacting or not
// using either the local value, or the provided one
val determinedValueToShow by remember(isInteracting, sliderValueRaw, value) {
derivedStateOf {
if (isInteracting) {
sliderValueRaw
} else {
value
}
}
}
Slider(
value = determinedValueToShow,
onValueChange = {
sliderValueRaw = it
onValueChange.invoke(it)
},
modifier = modifier,
enabled = enabled,
valueRange = valueRange,
steps = steps,
onValueChangeFinished = {
onValueChangeFinished?.invoke(sliderValueRaw)
},
interactionSource = interactionSource,
colors = colors
)
}