Is there a way to override the setter for the underlying value of a MutableState
Basically I want to achieve the exact behaviour of a non MutableState
object MyClass2 {
val myProperty2: Int = 0
set(value) {
if (value>= 0) {
field = value
}
}
}
Using myProperty2 = -3 will execute my custom setter and not update the value since the if condition is not met.
object MyClass1 {
val myProperty1: MutableState<Int> = mutableStateOf(0)
set(value) {
if (value.value >= 0) {
field = value
}
}
}
Using myProperty1.value = -3 will NOT execute my custom setter since im changing the underlying value and not the MutableState itself
This is related to this question but the answers look for state changes only.(updates the value automatically). I want to change the state only if a condition is met.
As long as our code is based on interfaces, we can always use composition and delegation to solve this kind of problems. In this case we can create our own implementation of MutableState and then implement everything according to our needs, possibly delegating to the original mutable state wherever it makes sense:
val myProperty1 = run {
val state = mutableStateOf(0)
object : MutableState<Int> by state {
override var value: Int
get() = state.value
set(value) {
if (value > 0) {
state.value = value
}
}
}
}
Related
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.
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
}
}
I am facing and issue with Android LiveData and Transformation map. I am gonna explain the case:
I have a SingleLiveEvent and LiveData as follows (one for all items and another one for items to display in screen):
val documents: SingleLiveEvent<List<DocumentData>> = SingleLiveEvent()
val itemsToDisplay: LiveData<List<DocumentData>>
get() {
return Transformations.map(documents) { documents ->
return#map documents.filter { showOptionals || it.isMandatory }
}
}
In Fragment, after observing itemsToDisplay, if I am trying to get the value of itemsToDisplay LiveData (itemsToDisplay.value) is always null
itemsToDisplay.observe(this, Observer {
// Inside this method I need to get a calculated property from VM which uses
```itemsToDisplay.value``` and I would like to reuse across the app
loadData(it)
})
// View Model
val hasDocWithSign: Boolean
get() {
return **itemsToDisplay.value**?.any { it.isSignable } ?: false
}
Does anyone know if a LiveData does not hold the value if it is calculated using Transformation.map or it could be a potential bug?
When you call itemsToDisplay you get a new empty LiveData instance, because you declared it as a getter without a backing field.
val itemsToDisplay: LiveData<List<DocumentData>>
= Transformations.map(documents) { documents ->
documents.filter { showOptionals || it.isMandatory }
}
https://kotlinlang.org/docs/reference/properties.html#backing-fields
I made a var extension property to data class like this:
var Element.bitmap: Bitmap?
get() = downloadImg(PLACE_HOLDER_URL) //Default
set(value) = this.bitmap.let {
it = value ?: return#let
}
However, the compiler complaining "Val cannot be reassigned" when I try to reassign "it" to the new value in this line:
it = value ?: return#let
I couldn't really understand why this happens?? I have a "var" bitmap property not "val", So what is the problem? And more importantly what is solution or alternative?
The variable inside the lambdas are immutable, the it there is a shallow copy of the variable bitmap.
You should be using the field to assign into the backing field (actual variable).
set(value) {
value?.let { field = it }
}
The let function creates a shallow copy of the variable, so that if a visible mutable variable (var) is changed by another thread then it can be safely used without risking mutation.
Example:
class Test {
var prop: Int? = 5
}
fun main() {
val test = Test()
thread {
Thread.sleep(100)
test.prop = null
}
if (test.prop != null) {
Thread.sleep(300) // pretend we did something
println(test.prop) // prints null even inside if check
}
}
To tackle these situations, a shallow copy is used such as with let which passes a immutable shallow copy of these.
class Test {
var prop: Int? = 5
}
fun main() {
val test = Test()
thread {
Thread.sleep(100)
test.prop = null
}
test.prop?.let { // `it` is a shallow copy, changes won't be reflected
Thread.sleep(300) // pretend we did something
println(it) // prints 5
}
}
Conclusion: it is not the actual variable itself, so changes won't reflect to the actual variable even if you would have been able to assign something to it.
Edit: Extension properties can't have backing field, extensions are literally getters and setters.
One thing you can do is to make a Map with a unique identifier in which you can store the values, but that might not be able to be garbage collected
Another thing you can do (which I recommend) is to use delegation
Delegation Example:
class ElementBitmapDelegate {
private var value: Bitmap? = null
operator fun getValue(thisRef: Any?, property: KProperty<*>): Bitmap {
return value ?: downloadImg(PLACE_HOLDER_URL).also { setValue(thisRef, property, it) }
// return value or if value is null return from downloadImg() and set it to value
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, v: Bitmap?) {
v?.let { value = it } // if v is not null then set value to v
}
}
var Element.bitmap: Bitmap? by ElementBitmapDelegate()
In order to create a custom setter, you need to access to the backing field of the property through field and not it, like this:
set(value) = {
field = value
}