kotlin - applying changes from sharedPreferences instantly - android

In my app i have a settings fragment which the user can set data like the amount of decimal places they want. Currently, using shared preferences the app successfully remembers the last entered data when reopening the app. However the change is only applied when the user reopens the app and not when first applied.
the following code is located in the MainActivity.kt:
val sp = PreferenceManager.getDefaultSharedPreferences(this.applicationContext)
val decimalPlaces = sp.getString("decimalPlaces", "")
sharedViewModel.form = DecimalFormat("#,###.##")
when(decimalPlaces!!.toInt()){
0 -> sharedViewModel.form = DecimalFormat("#,###")
1 -> sharedViewModel.form = DecimalFormat("#,###.#")
2 -> sharedViewModel.form = DecimalFormat("#,###.##")
3 -> sharedViewModel.form = DecimalFormat("#,###.###")
4 -> sharedViewModel.form = DecimalFormat("#,###.####")
}
upon researching solutions to have the changes apply while still in the app, i came across the following:
Commit() and apply()
Along with other similar approaches, However i was unable to successfully implement these methods.
Would anyone have a suggestion on how to have the changes apply without exiting the app?

Changes are being applied instantly but it seems that you are only reading shared preferences when app is started. in order to change your based on the changed value of decimal place, you need to inform your Activity that decimal place value is changed and it should update the view accordingly.
One way to accomplish this is to use a LiveData object and post changed decimal place value to it, when you save to shared preferences.
In SharedViewModel declare a LiveData object
val decimalPlaces : MutableLiveData<String> = MutableLiveData()
When you save value to shared preferences, update this LiveData as
fun saveToSharedPreferences(newDecimalPlacesValue: String){
// Your code that saves to shared preferences
decimalPlaces.postValue(newDecimalPlacesValue)
}
Finally observe this LiveData in your Activity and update UI
override fun onCreate(){
...
sharedViewModel.decimalPlaces.observe(this, Observe{
if(!it.isNullOrEmpty()){
when(it.toInt()){
0 -> sharedViewModel.form = DecimalFormat("#,###")
1 -> sharedViewModel.form = DecimalFormat("#,###.#")
2 -> sharedViewModel.form = DecimalFormat("#,###.##")
3 -> sharedViewModel.form = DecimalFormat("#,###.###")
4 -> sharedViewModel.form = DecimalFormat("#,###.####")
}
}
})
}

Related

Questions regarding sharedPreferences (Kotlin)

I am experimenting around with Kotlin's sharedPreferences, however I cannot seem to get the updated value to stick.
val sharedPreferences = getSharedPreferences("Files", Context.MODE_PRIVATE)
val editor = sharedPreferences.edit()
editor.putInt("numbers",1).apply()
val textview = findViewById<TextView>(R.id.textview)
textview.text = sharedPreferences.getInt("numbers",0).toString()
val button = findViewById<Button>(R.id.button)
button.setOnClickListener {
editor.putInt("numbers",2).apply()
textview.text = sharedPreferences.getInt("numbers",0).toString()
}
In the code above I set the initial value of the sharedPreference to 1, upon clicking the button the value will be updated to 2 and displayed.That works fine however when closing the app and reopening it, the value reverts to 1. Is there a way to permanatly keep the updated value?
You are setting it to that value every time you open the activity, since onCreate() is called every time it opens. You should check if the value is already set, and if it is, skip that line of code.
if ("numbers" !in sharedPreferences) {
val editor = sharedPreferences.edit()
editor.putInt("numbers",1).apply()
}
By the way, there is an extension function for editing without having to call apply and editor. repeatedly:
if ("numbers" !in sharedPreferences) {
sharedPreferences.edit {
putInt("numbers",1)
}
}

How to update data with SharedFlow in Android

In my application I want update data with SharedFlow and my application architecture is MVI .
I write below code, but just update one of data!
I have 2 spinners and this spinners data fill in viewmodel.
ViewModel code :
class MyViewModel #Inject constructor(private val repository: DetailRepository) : ViewModel() {
private val _state = MutableStateFlow<MyState>(MyState.Idle)
val state: StateFlow<MyState> get() = _state
fun handleIntent(intent: MyIntent) {
when (intent) {
is MyIntent.CategoriesList -> fetchingCategoriesList()
is MyIntent.PriorityList -> fetchingPrioritiesList()
}
}
private fun fetchingCategoriesList() {
val data = mutableListOf(Car, Animal, Color, Food)
_state.value = DetailState.CategoriesData(data)
}
private fun fetchingPrioritiesList() {
val data = mutableListOf(Low, Normal, High)
_state.value = DetailState.PriorityData(data)
}
}
With below codes I filled spinners in fragment :
lifecycleScope.launch {
//Send
viewModel.handleIntent(MyIntent.CategoriesList)
viewModel.handleIntent(MyIntent.PriorityList)
//Get
viewModel.state.collect { state ->
when (state) {
is DetailState.Idle -> {}
is DetailState.CategoriesData -> {
categoriesList.addAll(state.categoriesData)
categorySpinner.setupListWithAdapter(state.categoriesData) { itItem ->
category = itItem
}
Log.e("DetailLog","1")
}
is DetailState.PriorityData -> {
prioritiesList.addAll(state.prioritiesData)
prioritySpinner.setupListWithAdapter(state.prioritiesData) { itItem ->
priority = itItem
}
Log.e("DetailLog","2")
}
}
When run application not show me number 1 in logcat, just show number 2.
Not call this line : is DetailState.CategoriesData
But when comment this line viewModel.handleIntent(MyIntent.PriorityList) show me number 1 in logcat!
Why when use this code viewModel.handleIntent(MyIntent.CategoriesList) viewModel.handleIntent(MyIntent.PriorityList) not show number 1 and 2 in logcat ?
The problem is that a StateFlow is conflated, meaning if you rapidly change its value faster than collectors can collect it, old values are dropped without ever being collected. Therefore, StateFlow is not suited for an event-like system like this. After all, it’s in the name that it is for states rather than events.
It’s hard to suggest an alternative because your current code looks like you shouldn’t be using Flows at all. You could simply call a function that synchronously returns data that you use synchronously. I don’t know if your current code is a stepping stone towards something more complicated that really would be suitable for flows.

changing value of a LiveData on click of button only. But the observer's onChange is getting called even on onCreateView of fragment in Kotlin Android

I have a LiveData in my ViewModel:-
private val _toastMessage = MutableLiveData<Long>()
val toastMessage
get() = _toastMessage
And this is the only way I am changing it's value(on click of a submit button in the fragment):-
fun onSubmitClicked(<params>){
Log.i(LOG_TAG, "submit button clicked")
uiScope.launch {
if(!myChecksForEditTextValuesSucceeded())
{
_toastMessage.value = 0
}else{
_toastMessage.value = 1
}
}
}
And in the fragment, I have an observer for this LiveData:-
transactionViewModel.toastMessage.observe(viewLifecycleOwner, Observer { it->
when{
(it.compareTo(0) == 0) -> Toast.makeText(context, resources.getString(R.string.toast_msg_transaction_not_inserted), Toast.LENGTH_SHORT).show()
else -> Toast.makeText(context, resources.getString(R.string.toast_msg_transaction_inserted), Toast.LENGTH_SHORT).show()
}
})
Ideally, I am expecting the onChange of this Observer to be called only on clicking the submit button on my fragment. But, as I can see, it is also getting called even on onCreateView of my fragment.
What could be the possible reasons for this?
The issue is that LiveData pushes new values while you're observing it, but it also pushes the most recent value when you first observe it, or if the observer's Lifecycle resumes and the data has changed since it was paused.
So when you set toastMessage's value to 1, it stays that way - and ViewModels have a longer lifetime than Fragments (that's the whole point!) so when your Fragment gets recreated, it observes the current value of toastMessage, sees that it's currently 1, and shows a Toast.
The problem is you don't want to use it as a persistent data state - you want it to be a one-shot event that you consume when you observe it, so the Toast is only shown once in response to a button press. This is one of the tricky things about LiveData and there have been a bunch of workarounds, classes, libraries etc built around making it work
There's an old post here from one of the Android developers discussing the problem with this use case, and the workarounds available and where they fall short - in case anyone is interested! But like it says at the top, that's all outdated, and they recommend following the official guidelines.
The official way basically goes:
something triggers an event on the ViewModel
the VM updates the UI state including a message to be displayed
the UI observes this update, displays the message, and informs the VM it's been displayed
the VM updates the UI state with the message removed
That's not the only way to handle consumable events, but it's what they're recommending, and it's fairly simple. So you'd want to do something like this:
// making this nullable so we can have a "no message" state
private val _toastMessage = MutableLiveData<Long?>(null)
// you should specify the type here btw, as LiveData instead of MutableLiveData -
// that's the reason for making the Mutable reference private and having a public version
val toastMessage: LiveData<Long?>
get() = _toastMessage
// call this when the current message has been shown
fun messageDisplayed() {
_toastMessage.value = null
}
// make a nice display function to avoid repetition
fun displayToast(#StringRes resId: Int) {
Toast.makeText(context, resources.getString(resId), Toast.LENGTH_SHORT).show()
// remember to tell the VM it's been displayed
transactionViewModel.messageDisplayed()
}
transactionViewModel.toastMessage.observe(viewLifecycleOwner, Observer { it->
// if the message code is null we just don't do anything
when(it) {
0 -> displayToast(R.string.toast_msg_transaction_not_inserted)
1 -> displayToast(R.string.toast_msg_transaction_inserted)
}
})
You also might want to create an enum of Toast states instead of just using numbers, way more readable - you can even put their string IDs in the enum:
enum class TransactionMessage(#StringRes val stringId: Int) {
INSERTED(R.string.toast_msg_transaction_inserted),
NOT_INSERTED(R.string.toast_msg_transaction_not_inserted)
}
private val _toastMessage = MutableLiveData<TransactionMessage?>(null)
val toastMessage: LiveData<TransactionMessage?>
get() = _toastMessage
uiScope.launch {
if(!myChecksForEditTextValuesSucceeded()) toastMessage.value = NOT_INSERTED
else _toastMessage.value = INSERTED
}
transactionViewModel.toastMessage.observe(viewLifecycleOwner, Observer { message ->
message?.let { displayToast(it.stringId) }
// or if you're not putting the string resource IDs in the enum:
when(message) {
NOT_INSERTED -> displayToast(R.string.toast_msg_transaction_not_inserted)
INSERTED -> displayToast(R.string.toast_msg_transaction_inserted)
}
})
It can be a bit clearer and self-documenting compared to just using numbers, y'know?

Redundant recomposition happenning in my layout. Why does it recompose even though inputs haven't changed?

I have a SnapshotStateMap that I use to track updates in my layout, this map is stored in a viewmodel.
This the site call:
val roundState = viewModel.roundState
for (index in 0 until attempts) {
val state = roundState[index] ?: WordState.Empty
Row {
RoundWord(state, letters)
}
}
In my program there are changes to only one item at the time, so basically my train of thought is:
I add a new state or update the old in map -> I pass it to RoundWord -> If there is no state for index I pass in empty state -> RoundWord Composable relies on state to display the needed UI.
Here is the body of RoundWord
#Composable
private fun RoundWord(
state: WordState,
letters: Int,
) {
when (state) {
is WordState.Progress -> ProgressWord(letters)
is WordState.Empty -> WordCells(letters)
is WordState.Resolved -> WordCells(letters) { WiPLetter(state.value.word[it]) }
}
}
From what I understand if there is no state in roundState map for a given index I provide Empty state that is defined as an object in a sealed interface hierarchy. Same object -> no recomposition. But for some reason it recomposes every time. I have been at this for a few days now and despite going though tons of documentation I can't see what I am missing here. Why does this recomposition happens for empty state?

Android mutableStateOf() value property not changing

I have a data class.
data class ServiceInfoState(
val availableServices : List<Services> = emptyList(),
val loadingState : AvailableServicesState = AvailableServicesState.Loading
)
In my view model I keep the state as such:
private val _uiState = mutableStateOf(ServiceInfoState())
val uiState : State<ServiceInfoState> = _uiState
Later I create a new updated list of services and want to update the state.
_uiState.value = uiState.value.copy(availableServices = updated)
It is my understanding that this will create a new shallow copy of uiState.value, update the available services list, and assign this to the private property _uiState.value. This change to the value property should cause the UI which is watching the publicly exposed version uiState to recompose. This does not happen.
I then check the code like this:
println("List Equality: ${_uiState.value.availableServices === updated}")
val before = _uiState.value
_uiState.value = uiState.value.copy(availableServices = updated)
val after = _uiState.value
println("State Equality: ${before === after}")
Which prints out:
List Equality: false
State Equality: true
I agree that the list is not equal. That was my intention. To have a distinct list just in case. What surprises me is that the value property is equal before and after the copy. Doesn't copy return a new shallow reference? Is this not how this update should occur?
So I see why I am not getting a recomposition. Compose does not see the value property change because it did not change. How do I get it to change?
You can use SnapshotMutationPolicy to control how the result of mutableStateOf report and merge changes to the state object. Take a look at my answer here:
https://stackoverflow.com/a/74301717/8389251

Categories

Resources