How can we save and restore state for Android StateFlow? - android

We can create LiveData or StateFlow in a similar way as below
val _liveData = MutableLiveData(0)
val _stateFlow = MutableStateFlow(0)
But In LiveData, we can ensure the data is saved and restored by using
val _liveData: MutableLiveData<Int> = savedStateHandle.getLiveData("Key", 0)
For StateFlow, is there a way (or API) to keep the last value and restore it, like what we have in LiveData?

I use the conventional way in ViewModel to store and restore the value
private val _stateFlow = MutableStateFlow(
savedStateHandle.get("key") ?: InitialValue
)
And when setting the value
_stateFlow.value = ChangeValue
savedStateHandle.set("key", ChangeValue)
Not ideal, but at least we have a way to save the latest value.
And thanks to https://www.reddit.com/user/coffeemongrul/, who provided a proposal to wrap it around with a class as per https://www.reddit.com/r/androiddev/comments/rlxrsr/in_stateflow_how_can_we_save_and_restore_android/
class MutableSaveStateFlow<T>(
private val savedStateHandle: SavedStateHandle,
private val key: String,
defaultValue: T
) {
private val _state: MutableStateFlow<T> =
MutableStateFlow(savedStateHandle.get<T>(key) ?: defaultValue)
var value: T
get() = _state.value
set(value) {
_state.value = value
savedStateHandle.set(key, value)
}
fun asStateFlow(): StateFlow<T> = _state
}

Can you try this? I haven't tried it yet but I think it might work.
private val _stateFlow: MutableStateFlow<Int>?
get()= savedStateHandle.get<MutableStateFlow<Int>>("KEY")

I can propose one more solution. A simple extension that will update SavedStateHandle once MutableStateFlow will be updated.
fun <T> SavedStateHandle.getMutableStateFlow(
scope: CoroutineScope,
key: String,
initialValue: T
): MutableStateFlow<T> {
return MutableStateFlow(get(key) ?: initialValue).apply {
scope.launch(Dispatchers.Main.immediate) {
this#apply.collect { value ->
set(key, value)
}
}
}
}

Related

How to read from DataStore Preferences to string?

Im trying to use datastore inside Composable to read user data but cant read the value as string to put inside Text.
That's the datastore
private val Context.userPreferencesDataStore: DataStore<Preferences> by preferencesDataStore(
name = "user"
)
private val USER_FIRST_NAME = stringPreferencesKey("user_first_name")
suspend fun saveUserToPreferencesStore(context: Context) {
context.userPreferencesDataStore.edit { preferences ->
preferences[USER_FIRST_NAME] = "user1"
}
}
fun getUserFromPreferencesStore(context: Context): Flow<String> = context.userPreferencesDataStore.data
.map { preferences ->
preferences[USER_FIRST_NAME] ?: ""
}
and inside Composable:
#Composable
fun myComposable() {
var context = LocalContext.current
LaunchedEffect( true){
saveUserToPreferencesStore(context )
}
Text(getUserFromPreferencesStore(context ))
}
so in your code, getUserFromPreferencesStore() is returning a Flow. so you should collect that as flow, and then compose will auto update once the data is being changed. For example (something similar to this):
val user by getUserFromPreferencesStore(context).collectAsStateWithLifecycleAware(initValue)

Jetpack compose data store keeps recomposing screen

I'm migrating from Shared preference to data store using jetpack compose. everything works fine (data is saved and can be retreated successfully). However, whenever a Data is retrieved, the composable keeps on recomposing endlessly. I'm using MVVM architecture and below is how I have implemented data store.
Below is declared in my AppModule.kt
App module in SingletonComponent
#Provides
#Singleton
fun provideUserPreferenceRepository(#ApplicationContext context: Context):
UserPreferencesRepository = UserPreferencesRepositoryImpl(context)
Then here's my ViewModel:
#HiltViewModel
class StoredUserViewModel #Inject constructor(
private val _getUserDataUseCase: GetUserDataUseCase
): ViewModel() {
private val _state = mutableStateOf(UserState())
val state: State<UserState> = _state
fun getUser(){
_getUserDataUseCase().onEach { result ->
val name = result.name
val token = result.api_token
_state.value = UserState(user = UserPreferences(name, agentCode, token, balance))
}.launchIn(viewModelScope)
}}
Finally, Here's my Repository Implementation:
class UserPreferencesRepositoryImpl #Inject constructor(
private val context: Context
): UserPreferencesRepository {
private val Context.dataStore by preferencesDataStore(name = "user_preferences")
}
private object Keys {
val fullName = stringPreferencesKey("full_name")
val api_token = stringPreferencesKey("api_token")
}
private inline val Preferences.fullName get() = this[Keys.fullName] ?: ""
private inline val Preferences.apiToken get() = this[Keys.api_token] ?: ""
override val userPreferences: Flow<UserPreferences> = context.dataStore.data.catch{
// throws an IOException when an error is encountered when reading data
if (it is IOException) {
emit(emptyPreferences())
} else {
throw it
}
}.map { preferences ->
UserPreferences(name = preferences.fullName, api_token = preferences.apiToken)
}.distinctUntilChanged()
I don't know what causes the composable to recompose. Below Is the composable:
#Composable
fun LoginScreen(
navController: NavController,
userViewModel: StoredUserViewModel = hiltViewModel()
) {
Log.v("LOGIN_SCREEN", "CALLED!")
userViewModel.getUser()
}
If anyone can tell me where I've done wrong please enlighten me. I have tried to change the implementation in AppModule for UserPreferencesRepository but no luck.
Below is UseState.kt which is just a data class
data class UserState(
val user: UserPreferences? = null
)
Below is UserPreferences.kt
data class UserPreferences(val name: String, val api_token: String)
I also faced such problem. The solution was became to navigate with LauchedEffect in composable.
before:
if (hasFlight) {
navController.navigate(Screen.StartMovingScreen.route)
}
after:
if (hasFlight) {
LaunchedEffect(Unit) {
navController.navigate(Screen.StartMovingScreen.route)
}
}
This is expected behaviour: you're calling getUser on each recomposition.
#Composable function is a view builder, and should be side-effects free.
Instead you can use special side effect function, like LaunchedEffect, which will launch job only once, until it's removed from view tree or key argument is changed:
LaunchedEffect(Unit) {
userViewModel.getUser()
}
But this also will be re-called in case of configuration change, e.g. screen rotation. To prevent this case, you have two options:
Call getUser inside view model init: in this case it's guarantied that it's called only once.
Create some flag inside view model to prevent redundant request.
More info about Compose side effects in documentation.

Observe flow as Compose string state

I have a Composable and a viewmodel (VM) for it. The VM gets some data from a kotlin flow which I would like to expose as a State
Usually I would have the VM expose a state like this:
var title by mutableStateOf("")
private set
And I could use it in the Composable like this
Text(text = viewModel.title)
But since the data comes from a flow, i have to expose it like this
#Composable
fun title() = flowOf("TITLE").collectAsState(initial = "")
And have to use it in the Composable like this
Text(text = viewModel.title().value)
I try to minimize boilerplate code, so the .value kind of bothers me. Is there any way to collect the flow as state, but still expose it as viewModel.title or viewModel.title() and get the actual String and not the state object?
You can use delegated property.If your program just read it.
class FlowDeletedProperty<T>(val flow: Flow<T>, var initialValue: T, val scope: CoroutineScope) :
ReadOnlyProperty<ViewModel, T> {
private var _value = mutableStateOf(initialValue)
init {
scope.launch {
flow.collect {
_value.value = it
}
}
}
override fun getValue(thisRef: ViewModel, property: KProperty<*>): T {
return _value.value
}
}
fun <T> ViewModel.flowDeletedProperty(flow: Flow<T>, initialValue: T) =
FlowDeletedProperty(flow, initialValue, viewModelScope)
in viewModel
val a = flow {
while (true) {
kotlinx.coroutines.delay(100L)
println("out ")
emit((100..999).random().toString())
}
}
val title by flowDeletedProperty(a,"")
in ui
Text(text = viewModel.title)

Jetpack compose why viewstate data class

I don't understand why in a lot of Google's official examples, they use Flow combine function to combine from 2-10 different flows into a viewstate data object.
Is there a specific reason to do this? (other than possibly tidier code?)
They even make a boolean that is set in a button onClick into a flow for the sake of making the boolean into that viewstate object. (For example, the selectedCategory variable below is changed in some kind of tabview callback)
Personally I would have made that boolean variable into a MutableState. I don't see why to make it into a flow...
data class DiscoverViewState(
val categories: List<Category> = emptyList(),
val selectedCategory: Category? = null
)
class DiscoverViewModel(
...
) : ViewModel() {
private val _state = MutableStateFlow(DiscoverViewState())
val state: StateFlow<DiscoverViewState>
get() = _state
}
#Composable
fun Discover(
modifier: Modifier = Modifier
) {
val viewModel: DiscoverViewModel = viewModel()
val viewState by viewModel.state.collectAsState()
}

Best way to execute custom logic when setting value to MutableLiveData

What is the most recommended approach to execute custom logic when setting value of MutableLiveData?
I have a ViewModel with several properties isConnecting and isConnected.
I want to set isConnecting to false when isConected is changed
class MyViewModel : ViewModel() {
private var _isConnecting = MutableLiveData<Boolean>()
val isConnecting: LiveData<Boolean>
get () = _isConnecting
private var _isConnected = MutableLiveData<Boolean>()
val isConnected: LiveData<Boolean>
get () = _isConnected
}
One way to do it is creating a function inside MyViewModel and set both properties:
fun setConnected(value: Boolean) {
_isConnected.value = value
_isConnecting.value = false
}
This is okay, but one must never set _isConnected manually and always use function setConnected(). It can not be guaranteed and thus there may be bugs.
Another way to do it is to make MyViewModel observe its own MutableLiveData:
class MyViewModel : ViewModel() {
// ...
private val isConnectedObserver = Observer<Boolean> {
_isConnecting.value = false
}
init {
isConnected.observeForever(isConnectedObserver)
}
override fun onCleared() {
super.onCleared()
isConnected.removeObserver(isConnectedObserver)
}
}
This avoids problem of first approach, but is just awful.
But is there a better way? For example using setters somehow?
Use MediatorLiveData to observe other LiveData objects and react on onChanged events from them:
class MyViewModel : ViewModel() {
private var _isConnecting = MediatorLiveData<Boolean>().also { liveData ->
liveData.addSource(_isConnected) { connected ->
// isConnected changed, some logic here
if (connected) {
liveData.value = false
}
}
}
val isConnecting: LiveData<Boolean>
get() = _isConnecting
private var _isConnected = MutableLiveData<Boolean>()
val isConnected: LiveData<Boolean>
get() = _isConnected
}

Categories

Resources