Jetpack Compose - DataStore interaction with Switch (preference) - android

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.

Related

Jetpack Compose: Provide initial value for TextField

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?

Compose not refreshing view on change of mutableStateOf value

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.

Jetpack Compose: MutableState<Boolean> not working as intended

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.

Why Android Datastore always returns same value with runblocking

I've been using Datastore for a long time. Today i had to read the values in the main thread. After reviewing the documentation, I decided to use runblocking. I created a long value which name is lastInsertedId.
I reading lastInsertedId in Fragment A then navigated to Fragment B and I'm changing the value of lastInsertedId. When i pop back to Fragment A i read lastInsertedId again. But lastInsertedId's value was still same. Actually it's value is changing but i can't read its last value.
I think it was because Fragment A was not destroyed. Only onDestroyView called and created from onCreateView. What i want is i need to access lastInsertedID's current value whenever i want in main thread.
When i create it as a variable, it always returns the same value. But when i convert it to function it works well. But i don't think this is the best practices. What's the best way to access this value?
Thanks.
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "main")
#Singleton
class DataStoreManager #Inject constructor(#ApplicationContext appContext: Context) {
private val mainDataStore = appContext.dataStore
suspend fun setLastInsertedId(lastId: Long) {
mainDataStore.edit { main ->
main[LAST_INSERTED_ID] = lastId
}
}
// Returns always the same value
val lastInsertedId: Long = runBlocking {
mainDataStore.data.map { preferences ->
preferences[LAST_INSERTED_ID] ?: 0
}.first()
}
// Returns as expected
fun lastInsertedId(): Long = runBlocking {
mainDataStore.data.map { preferences ->
preferences[LAST_INSERTED_ID] ?: 0
}.first()
}
// This is also work perfectly but i need to access in main thread.
val lastInsertedId : Flow<Long> = mainDataStore.data.map { preferences ->
preferences[LAST_INSERTED_ID] ?: Constants.DEFAULT_FOOD_ID
}
companion object {
private val LAST_INSERTED_ID = longPreferencesKey("last_inserted_id")
}
}
You must add get() to your val definition like this.
val lastInsertedId: Long get() = runBlocking {
mainDataStore.data.map { preferences ->
preferences[LAST_INSERTED_ID] ?: 0
}.first()
}
You seems don't understand the differece between variable and function, take a look at this:
fun main() {
val randomNum1 = (1..10).random()
repeat(5) { println(randomNum1) }
repeat(5) { println(getRandomNum()) }
}
fun getRandomNum() = (1..10).random()
Output:
2
2
2
2
2
8
7
8
10
1
Variable holds a value, and it doesn't change until you assign it a new value.

Get previous value of state in Composable - Jetpack Compose

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
}
}

Categories

Resources