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.
Related
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.
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?
In the Android developer docs at the following web address: https://developer.android.com/jetpack/compose/mental-model#recomposition
There is a composable function which is given as the following:
#Composable
fun ClickCounter(clicks: Int, onClick: () -> Unit) {
Button(onClick = onClick) {
Text("I've been clicked $clicks times")
}
}
It's said in the text that this produces an element which updates the number of times its been clicked every time it is clicked. However, looking at it, it seems to need a lambda function to do that.
When I try and put it into the SetContent function I get the following:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
WorkTimerTheme {
Conversation(SampleData.conversationSample)
ClickCounter(clicks = 0) {
//Insert My Function Here
}
}
}
}
}
The comment //Insert My Function Here has been added by me. I presume that within this I have to put a Lambda which updates the clicks value of the composable, but I have no idea what to put. Does anyone know an acceptable way of writing this?
You need a MutableState to trigger recomposition and remember{} to keep previous value when recomposition occurred.
I asked a question about it and my question contains answer to your question.
#Composable
fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
val counterState = remember { mutableStateOf(0) }
Column(modifier = Modifier.fillMaxHeight()) {
Counter(
count = counterState.value,
updateCount = { newCount ->
counterState.value = newCount
}
)
}
}
#Composable
fun Counter(count: Int, updateCount: (Int) -> Unit) {
Button(
onClick = { updateCount(count + 1) },
) {
Text("I've been clicked $count times")
}
}
Thanks very much to #Thracian for linking a similar question. As the answer to mine is related yet slightly different I thought I would post my own.
The correct code is as follows:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val counterState = remember { mutableStateOf(0) }
WorkTimerTheme {
Conversation(SampleData.conversationSample)
ClickCounter(clicks = counterState.value) {
counterState.value++
}
}
}
}
}
As suggested I added a mutableState value which appears to remember the value of something at the last "recomposition", unless it is explicitly updated. If a mutablestate is explicitly updated that will trigger a recomposition (as noted in the answer to #Thracian's question).
A recomposition will redraw the Element.
In order to update the value at recomposition the number of times the button has been clicked must be stored in the mutablestate and passed to the Composable Function at each recomposition.
Using the Composable functions Lambda argument to affect the mutable state completes the loop, updating the mutablestate which then recomposes the button with the updated value.
That is the purpose of counterState.value++.
As suggested above for more information on this, try reading this documentation: https://developer.android.com/jetpack/compose/state#viewmodel-state
The video is related to what we're discussing here.
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
}
}