assume my code looks like this
#Composable
fun ExampleList() {
val tickers by exampleViewModel.tickers.observeAsState()
LazyColumn() {
items(items = tickers) { ticker ->
ExampleItem(ticker)
}
}
}
#Composable
fun ExampleItem(ticker: Ticker) {
Text(text= ticker.lastPrice)
}
is there anyway to get previous value of ticker in ExampleItem Compose everytime ticker is updated?
I'm wondering if there's something like componentDidUpdate in React Native
While the answer is technically correct, the first example renders too many times and I did not understand the second example unfortunately.
So I got back to React to see how it is done there and it is explained very good here:
This is what the hook (remember function as you will) looks like (for the curious):
function usePrevious<T>(value: T): T {
// The ref object is a generic container whose current property is mutable ...
// ... and can hold any value, similar to an instance property on a class
const ref: any = useRef<T>();
// Store current value in ref
useEffect(() => {
ref.current = value;
}, [value]); // Only re-run if value changes
// Return previous value (happens before update in useEffect above)
return ref.current;
}
The same idea can be implemented in compose un a reusable way (it is important that the #Composable should not be rerendered when setting the previous value):
/**
* Returns a dummy MutableState that does not cause render when setting it
*/
#Composable
fun <T> rememberRef(): MutableState<T?> {
// for some reason it always recreated the value with vararg keys,
// leaving out the keys as a parameter for remember for now
return remember() {
object: MutableState<T?> {
override var value: T? = null
override fun component1(): T? = value
override fun component2(): (T?) -> Unit = { value = it }
}
}
}
and the actual rememberPrevious:
#Composable
fun <T> rememberPrevious(
current: T,
shouldUpdate: (prev: T?, curr: T) -> Boolean = { a: T?, b: T -> a != b },
): T? {
val ref = rememberRef<T>()
// launched after render, so the current render will have the old value anyway
SideEffect {
if (shouldUpdate(ref.value, current)) {
ref.value = current
}
}
return ref.value
}
key values can be added to the remember function, but I've found that the remember did not work in my case, as it always rerendered even when no keys were passed in.
Usage:
#Composable
fun SomeComponent() {
...
val prevValue = rememberPrevious(currentValue)
}
I figured out that I could get last value of ticker by using remember {mutableStateOf} as below:
var lastTicker by remember { mutableStateOf(ticker)}
SideEffect {
if (lastTicker != ticker) {
// compare lastTicker to current ticker before assign new value
lastTicker = ticker
}
}
by using remember { mutableStateOf(ticker)}, I can persist value of ticker throught recomposition.
then inside SideEffect I can use lastTicker value ( to compare last ticker and current ticker in my case) before assign it to new value to use for next composition
or using derivedStateOf to watch ticker change only, avoid recomposition
val compareValue by remember(ticker) {
derivedStateOf {
// compare lastTicker to current ticker before assign new value
lastTicker = ticker
// return value
}
}
Related
I want to achieve the following use case: A payment flow where you start with a screen to enter the amount (AmountScreen) to pay and some other screens to enter other values for the payment. At the end of the flow, a summary screen (SummaryScreen) is shown where you can modify the values inline. For the sake of simplicity we will assume there is only AmountScreen followed by SummaryScreen.
Now the following requirements should be realized:
on AmountScreen you don't loose your input on configuration change
when changing a value in SummaryScreen and go back to AmountScreen (using system back), the input is set to the changed value
AmountScreen and SummaryScreen must not know about the viewModel of the payment flow (PaymentFlowViewModel, see below)
So the general problem is: we have a screen with an initial value for an input field. The initial value can be changed on another (later) screen and when navigating back to the first screen, the initial value should be set to the changed value.
I tried various approaches to achieve this without reverting to Kotlin flows (or LiveData). Is there an approach without flows to achieve this (I am quite new to compose so I might be overlooking something obvious). If flows is the correct approach, would I keep a MutableStateFlow inside the PaymentFlowViewModel for amount instead of a simple string?
Here is the approach I tried (stripped and simplified from the real world example).
General setup:
internal class PaymentFlowViewModel : ViewModel() {
var amount: String = ""
}
#Composable
internal fun NavigationGraph(viewModel: PaymentFlowViewModel = viewModel()) {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "AMOUNT_INPUT_SCREEN"
) {
composable("AMOUNT_INPUT_SCREEN") {
AmountInputRoute(
// called when the Continue button is clicked
onAmountConfirmed = {
viewModel.amount = it
navController.navigate("SUMMARY_SCREEN")
},
// apply the entered amount as the initial value for the input text
initialAmount = viewModel.amount
)
}
composable("SUMMARY_SCREEN") {
SummaryRoute(
// called when the amount is changed inline
onAmountChanged = {
viewModel.amount = it
},
// apply the entered amount as the initial value for the input text
amount = viewModel.amount
)
}
}
}
The classes of the AmountScreen look like this:
#Composable
internal fun AmountInputRoute(
initialAmount: String,
onAmountConfirmed: (String) -> Unit
) {
// without the "LaunchedEffect" statement below this fulfils all requirements
// except that the changed value from the SummaryScreen is not applied
val amountInputState: MutableState<String> = rememberSaveable { mutableStateOf(initialAmount) }
// inserting this fulfils the req. that the changed value from SummaryScreen is
// applied, but breaks keeping the entered value on configuration change
LaunchedEffect(Unit) {
amountInputState.value = initialAmount
}
Column {
AmountInputView(
amountInput = amountInputState.value,
onAmountChange = { amountInput ->
amountInputState.value = amountInput
}
)
Button(onClick = { onAmountConfirmed(amountInputState.value) }) {
Text(text = "Continue")
}
}
}
```
I achieved the goal with a quite complicated approach - I would think there are better alternatives out there.
What I tried that did not work: using rememberSaveable passing initialAmount as parameter for inputs. Theoretically rememberSaveable would reinitialize its value when inputs changes, but apparently this does not happen when the composable is only on the back stack and also is not executed when it gets restored from the back stack.
What I implemented that did work:
#Composable
internal fun AmountInputRoute(
initialAmount:String,
onAmountConfirmed: (String) -> Unit
) {
var changedAmount by rememberSaveable {
mutableStateOf<String?>(null)
}
val amountInput by derivedStateOf {
if (changedAmount != null)
changedAmount
else
initialAmount
}
AmountInputView(
amountInput = amountInput,
onContinueClicked = {
onAmountConfirmed(amountInput)
changedAmount = null
},
validAmountChanged = {
changedAmount = it
}
)
}
Any better ideas?
I'm learning Jetpack compose and I'm having a hard time.
I have a DataStoreUtil class which sets and gets a Boolean value. Default value is true (if not found).
class DataStoreUtil(private val context: Context) {
// to make sure there's only one instance
companion object {
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore("settings")
val FORCE_DARK_THEME = booleanPreferencesKey("force_dark_theme")
}
//get the saved email
val getForceDarkTheme: Flow<Boolean> = context.dataStore.data
.map { preferences ->
preferences[FORCE_DARK_THEME] ?: true
}
//save email into datastore
suspend fun saveForceDarkTheme(value: Boolean) {
context.dataStore.edit { preferences ->
preferences[FORCE_DARK_THEME] = value
}
}
}
I have tested it and it works.
On the other side I have a Switch which should manage this Boolean value.
val value = dataStoreUtil.getForceDarkTheme.collectAsState(initial = true).value
var checked by remember { mutableStateOf(value) }
Switch(checked = checked, onCheckedChange = {
checked = it
scope.launch {
dataStoreUtil.saveForceDarkTheme(checked)
}
})
Saving this value via Switch works but not initial state, which is ALWAYS set to true on screen open.
I guess it's because collectAsState(initial = true) but... how can achieve this goal? (Have it set to false if DataStoreUtil returns false.
Any help is appreciated.
You will need to read the value synchronously when you start the app, using runBlocking.
fun getForceDarkThemeSync() = runBlocking {
getForceDarkTheme.first()
}
Edit: if you want to hold the splash screen until you have read the data and made a determination on whether to use light or dark mode, check this.
I have a ViewModel with the property
var uiState by mutableStateOf(UiState())
I have a composable that accesses this value like so
fun AppView(viewModel: BridgeViewModel) {
val uiState = viewModel.uiState
when {
uiState.isLoading -> {
LoadingView()
}
uiState.data != null -> {
TextToShareView(to = uiState.data)
}
}
}
After the composable is created I trigger a function in my ViewModel that changes the state like so
uiState = UiState(isLoading = true)
The problem is, the composable is not being redrawn when I change the value of state.
Any idea? I can't see how this is different to the official sample in the docs.
In our Android app we want to introduce Compose to a simple debug screen, where we can enable/disable SharedPreferences. I'm trying to get that running using Compose' interface MutableState - but it does not work how I think it does. My plan is to temporarily use MutableState to set a boolean in SharedPreferences (before migrating to DataStore later).
Here is what I had in mind:
private class MyOwnState(startWith: Boolean) : MutableState<Boolean> {
override var value: Boolean = startWith
override fun component1(): Boolean = value
override fun component2(): (Boolean) -> Unit = { value = it }
}
// then, in composable:
var value by remember { MyOwnState(false) }
Of course in real life I would overwrite the getter+setter of the value - but this example is enough, because it does not work. The state change is not propagated and the UI is not updated.
To illustrate this, I but together the code snippets by remember { mutableStateOf(false) } and by remember { MyOwnState(false) }. The first one works (switch is updated), the second one does not.
Full code:
#Composable
fun SomeStateExamples() {
Column {
SwitchWorks()
SwitchDoesNotWork()
}
}
#Composable
fun SwitchWorks() {
var value by remember { mutableStateOf(false) }
Switch(checked = value, onCheckedChange = { value = it })
}
#Composable
fun SwitchDoesNotWork() {
var value by remember { MyOwnState(false) }
Switch(checked = value, onCheckedChange = { value = it })
}
private class MyOwnState(startWith: Boolean) : MutableState<Boolean> {
override var value: Boolean = startWith
override fun component1(): Boolean = value
override fun component2(): (Boolean) -> Unit = { value = it }
}
The first switch is togglable, the second one is not:
What am I missing? The MutableState interface is pretty simple, and stable - and I didn't find any extra methods (aka invalidate, notifyListeners, ...) that I need to call.
Thank you for your help! 🙏
Adding to Johan's answer, it looks like you also need to implement StateObject to fetch the value and update thd snapshot system. By having a look at SnapshotMutableStateImpl
override var value: T
get() = next.readable(this).value
set(value) = next.withCurrent {
if (!policy.equivalent(it.value, value)) {
next.overwritable(this, it) { this.value = value }
}
}
private var next: StateStateRecord<T> = StateStateRecord(value)
override val firstStateRecord: StateRecord
get() = next
You will see that using StateObject makes you work with StateRecords where you store the updatable value, read it and update it.
In your MyOwnState class you have to implement private mutableState value like this:
private class MyOwnState(startWith: Boolean) : MutableState<Boolean> {
private var _value by mutableStateOf(startWith)
override var value: Boolean = startWith
get() = _value
set(value) {
_value = value
field = value
}
override fun component1(): Boolean = value
override fun component2(): (Boolean) -> Unit = { value = it }
}
When you will try to change value inside composable, composition will recompose because you also changed MutableState _value. Read more about how state works in Jetpack Compose here.
Not an answer directly, but looking at how mutableStateOf works, it's also calling createSnapshotMutableState(value, policy) behind the scenes.
So I don't think just inheriting MutableState and changing that will cause Compose to initiate a recomposition and thus updating the UI.
I would probably instead try to pass in the state of the UI from outside as a model with ViewModel or LiveData and mutate that model data.
Using LiveData to store the value of the slider makes it lag and move jerky (I suppose because of postValue ())
#Composable
fun MyComposable(
viewModel: MyViewModel
) {
val someValue = viewModel.someValue.observeAsState()
Slider(someValue) {
viewModel.setValue(it)
}
}
class MyViewModel() : ViewModel() {
val someValue: LiveData<Float> = dataStore.someValue // MutableLiveData
fun setValue(value: Float) {
dataStore.setValue(value)
}
}
class MyDataStore() {
val someValue = MutableLiveData<Float>()
fun setValue(value: Float) {
// Some heavy logic
someValue.postValue(value)
}
}
As I understand it, postValue() takes a while, and because of this, the slider seems to be trying to resist changing the value.
In order to somehow get around this, I had to create additional State variables so that the slider would directly update its value
#Composable
fun MyComposable(
viewModel: MyViewModel
) {
val someValue = viewModel.someValue.observeAsState()
var someValue2 by remember { mutableStateOf(someValue) }
Slider(someValue2) {
someValue2 = it
viewModel.setValue(it) // I also had to remove postValue ()
}
}
As I understand it, if the data from the DataStore comes with a delay, then the value in someValue will not have time to initialize and it will be null by the time the view appears (this has not happened yet, but is it theoretically possible?), And thus the value of the slider will not be relevant. Are there any solutions to this problem?