How to ignore/skip the initial value that StateFlow emitted? - android

In my Android project, I'm using DataStore and StateFlow to store and change the UI state. The problem is, every time the app starts, because of the necessary initial value of StateFlow, the corresponding theme setting will always set to the initial value before set to the value read from DataStore, and this will lead to a visual hit(change from Light to Dark or Dark to light) for users.
The following text shows the detail on achieving that(I use this sample code as reference):
Get the value from DataStore as a Flow and assign it to observeSelectedDarkMode(): Flow<String> function in my AppRepository class.
class AppRepository(
val context: Context
) : AppRepository {
private val dataStore = context.dataStore
override fun observeSelectedDarkMode(): Flow<String> = dataStore.data.map { it[KEY_SELECTED_DARK_MODE] ?: DEFAULT_VALUE_SELECTED_DARK_MODE }
...
}
Convert it to StateFlow using .stateIn() in my SettingsViewModel class.
class SettingsViewModel(
private val appRepository: AppRepository
) : ViewModel() {
val selectedDarkModel = appRepository.observeSelectedDarkMode().stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(),
AppDataStore.DEFAULT_VALUE_SELECTED_DARK_MODE
)
...
}
Use .collectAsState() in my CampusHelperTheme() fun to observe the change.
#Composable
fun CampusHelperTheme(
appContainer: AppContainer,
content: #Composable () -> Unit
) {
val settingsViewModel: SettingsViewModel = viewModel(
factory = SettingsViewModel.provideFactory(appContainer.appRepository)
)
val selectedDarkMode by settingsViewModel.selectedDarkModel.collectAsState()
val darkTheme = when (selectedDarkMode) {
"On" -> true
"Off" -> false
else -> isSystemInDarkTheme()
}
val context = LocalContext.current
val colorScheme = if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
...
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
It seems that everything is good, but when the selectedDarkMode value is different from its default value AppDataStore.DEFAULT_VALUE_SELECTED_DARK_MODE(Follow System), the app will turn from Dark to Light or from Light to Dark, which would give users a bad experience.
So what I want is let the app skip the initial value of StateFlow and then the app can set to what the user choose once it launch, then there will not have a visual hit to users.
I search on this site, and found this ask: How to use DataStore with StateFlow and Jetpack Compose?, but it has no content about the initial value I am looking for, I also found this ask: Possible to ignore the initial value for a ReactiveObject?, but it is in C# and ReactiveUI, which is not suitable for me.
How should I achieve this?

Related

How to update properties of data class show compose ui can observe the changes

I have a CounterScreenUiState data class with a single property called counterVal (integer). If I am updating the value of my counter from viewModel which of the following is the correct approach?
Approach A:
data class CounterUiState(
val counterVal: Int = 0,
)
class CounterViewModel : ViewModel() {
var uiState by mutableStateOf(CounterUiState())
private set
fun inc() {
uiState = uiState.copy(counterVal = uiState.counterVal + 1)
}
fun dec() {
uiState = uiState.copy(counterVal = uiState.counterVal - 1)
}
}
or
Approach B:
data class CounterUiState(
var counterVal: MutableState<Int> = mutableStateOf(0)
)
class CounterViewModel : ViewModel() {
var uiState by mutableStateOf(CounterUiState())
private set
fun inc() {
uiState.counterVal.value = uiState.counterVal.value + 1
}
fun dec() {
uiState.counterVal.value = uiState.counterVal.value - 1
}
}
For the record, I tried both approach and both works well without unnecessary re-compositions.
Thanks in Advance!!!
So to summarize, "implementation" and "performance" wise, your'e only
choice is A.
This is not true. It's a common pattern that is used other Google's sample apps, JetSnack for instance, and default functions like rememberScrollable or Animatable are the ones that come to my mind. And in that article it's also shared as
#Stable
class MyStateHolder {
var isLoading by mutableStateOf(false)
}
or
#Stable
class ScrollState(initial: Int) : ScrollableState {
/**
* current scroll position value in pixels
*/
var value: Int by mutableStateOf(initial, structuralEqualityPolicy())
private set
// rest of the code
}
Animatable class
class Animatable<T, V : AnimationVector>(
initialValue: T,
val typeConverter: TwoWayConverter<T, V>,
private val visibilityThreshold: T? = null,
val label: String = "Animatable"
) {
internal val internalState = AnimationState(
typeConverter = typeConverter,
initialValue = initialValue
)
/**
* Current value of the animation.
*/
val value: T
get() = internalState.value
/**
* The target of the current animation. If the animation finishes un-interrupted, it will
* reach this target value.
*/
var targetValue: T by mutableStateOf(initialValue)
private set
}
Omitted some code from Animatable for simplicity but as can be seen it's a common pattern to use a class that hold one or multiple MutableStates. Even type AnimationState hold its own MutableState.
You can create state holder classes and since these are not e not variables but states without them changing you won't have recompositions unless these states change. The thing needs to be changed with option B is instead of using
data class CounterUiState(
var counterVal: MutableState<Int> = mutableStateOf(0)
)
You should change it to
class CounterUiState(
var counterVal by mutableStateOf(0)
)
since you don't need to set new instance of State itself but only the value.
And since you already wrap your states inside your uiState there is no need to use
var uiState by mutableStateOf(CounterUiState())
private set
you can have this inside your ViewModel as
val uiState = CounterUiState()
or inside your Composable after wrapping with remember
#Composable
fun rememberCounterUiState(): CounterUiState = remember {
CounterUiState()
}
With this pattern you can store States in one class and hold variables that should not trigger recomposition as part of internal calculations and it's up to developer expose these non-state variables based on the design.
https://github.com/android/compose-samples/blob/main/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Search.kt
#Stable
class SearchState(
query: TextFieldValue,
focused: Boolean,
searching: Boolean,
categories: List<SearchCategoryCollection>,
suggestions: List<SearchSuggestionGroup>,
filters: List<Filter>,
searchResults: List<Snack>
) {
var query by mutableStateOf(query)
var focused by mutableStateOf(focused)
var searching by mutableStateOf(searching)
var categories by mutableStateOf(categories)
var suggestions by mutableStateOf(suggestions)
var filters by mutableStateOf(filters)
var searchResults by mutableStateOf(searchResults)
val searchDisplay: SearchDisplay
get() = when {
!focused && query.text.isEmpty() -> SearchDisplay.Categories
focused && query.text.isEmpty() -> SearchDisplay.Suggestions
searchResults.isEmpty() -> SearchDisplay.NoResults
else -> SearchDisplay.Results
}
}
Also for skippibility
Compose will treat your CounterUiState as unstable and down the road
it will definitely cause you headaches because what ever you do,
This is misleading. Most of the time optimizing for skippability is premature optimization as mentioned in that article and the one shared by originally Chris Banes.
Should every Composable be skippable? No.
Chasing complete skippability for every composable in your app is a
premature optimization. Being skippable actually adds a small overhead
of its own which may not be worth it, you can even annotate your
composable to be non-restartable in cases where you determine that
being restartable is more overhead than it’s worth. There are many
other situations where being skippable won’t have any real benefit and
will just lead to hard to maintain code. For example:
A composable that is not recomposed often, or at all.

Type mismatch in compose state hoisting

I am attempting to get one property to bind between a ViewModel and a #Composable.
I am getting the following error
Type mismatch.
Required:String
Found:MutableState<String.Companion>
I don't understand what I am doing wrong.
//Reusable input field
#Composable
fun MyTextField(
value: String,
onValueChange: (String) -> Unit,
placeHolder: String
) {
OutlinedTextField(
value = value,
onValueChange = onValueChange,
placeholder = {Text(placeHolder)},
)
}
// ViewModel
class MyViewModel : ViewModel () {
var MyVariable = mutableStateOf(String)
}
// stateful comp
#Composable
fun MyScreen(
viewModel: MyViewModel = MyViewModel()
) {
MyContent(
myVariable = vm.myVariable,
setMyVariable = { vm.myVariable = it }
)
}
// stateless Comp
#Composable
fun MyContent(
myVariable: String,
setMyVariable: (String) -> Unit
)
{
Column() {
MyTextField(value = myVariable, onValueChange = setMyVariable, placeholder = "Input Something")
Text("Your variable is $myVariable" )
}
You are initializing your variable as a MutableState<T> type data-holder.
What you've posted in the question won't even compile but this is what I assume you did in your actual code
var myVar = mutableStateOf ("")
Now, mutableStateOf("") returns a MutableState<String> type object, which you are passing around in your methods.
On the other hand, your methods expect a String type object, not MutableState<String>. Hence, you could either extract the value from your variable and pass that around,
myVar.value
Or do it the preferred way, and use kotlin property delegation.
Initialize the variable like this
var myVar by mutableStateOf ("")
The by keyword acts as a delegate for the initializer and returns a String value instead of a MutableState<T>. This updates and triggers recompositions just as it's other counterpart but is far cleaner in code and keeps the boilerplate to a minimum.
Also, you seem to be pretty new to compose and kotlin, so consider taking the Compose pathway to learn the basics. Just look it up and the first official Android developers link will take you there.
EDIT: FINAL ANSWER
ViewModel
ViewModel{
var v by mutableStateOf ("")
fun setV(v: String) {
this.v = v
}
}
Composable calling site
MyComposable(
value = viewModel.v
onValueChange = viewModel::setV
)
Composable declaration
fun MyComposable (
value: String,
onValueChange: (String) -> Unit
) {
TextField (
value = value
onValueChange = onValueChange
)
}
This is the proper state-hoisting linking where the state variable is updated properly and hence, read well. Also, sir, you'd know what state-hoisting is if you'd actually read through the docs and taken the codelabs. I'm serious, TAKE the codelabs (in the Compose pathway). That's the reason your question was downvoted so many times, because you use terms like state-hoisting as if you understand it, but then you don't have half the implementation that it promotes.
Replace
MyContent(
myVariable = vm.myVariable,
setMyVariable = { vm.myVariable = it }
)
with
MyContent(
myVariable = vm.myVariable.value,
setMyVariable = { vm.myVariable.value = it }
)
and you need to define your variable like this
val MyVariable = mutableStateOf<String>("initial value here")

Jetpack Compose State Hoisting, Previews, and ViewModels best practices

So it seems like the recommended thing in Jetpack Compose is to hoist state out of your composables, to make them stateless, reusable, and testable, and allow using them in previews easily.
So instead of having something like
#Composable
fun MyInputField() {
var text by remember { mutableStateOf("") }
TextField(value = text, onValueChange = { text = it })
}
You'd hoist the state, like this
#Composable
fun MyInputField(text: String, onTextChange: (String) -> Unit) {
TextField(value = text, onValueChange = onTextChange)
}
This is fine, however what of some more complex uses?
Let's pretend I have a screen represented by a composable, with multiple interactions between the View and the ViewModel. This screen is split into multiple inner composable (think for instance one for a header, one for the body, which in turn is split into several smaller composables)
You can't create a ViewModel (with viewModel() at least, you can instantiate one manually) inside a composable and use this composable in a Preview (previews don't support creating viewmodel like this)
Using a ViewModel inside the inner composables would make them stateful, wouldn't it ?
So the "cleanest" solution I see, would be to instantiate my viewmodel only at the highest composable level, and then pass to the children composables only vals representing the state, and callbacks to the ViewModel functions.
But that's wild, I'm not passing down all my ViewModel state and functions through individual parameters to all composables needing them.
Grouping them in a data class for example could be a solution
data class UiState(
val textInput: String,
val numberPicked: Int,
……
and maybe create another one for callbacks ?
But that's still creating a whole new class just to mimic what the viewmodel already has.
I don't actually see what the best way of doing this could be, and I find nothing about that anywhere
A good way to manage complex states is to encapsulate required complex behavior into a class and use remember function while having stateless widgets as most as you can and change any properties of state whenever it's required.
SearchTextField is a component that uses only state hoisting, SearchBar has back arrow and SearchTextField and also itself is a stateless composable. Communication between these two and parent of Searchbar is handled via callback functions only which makes both SearchTextField re-suable and easy to preview with a default state in preview. HomeScreen contains this state and where you manage changes.
Full implementation is posted here.
#Composable
fun <R, S> rememberSearchState(
query: TextFieldValue = TextFieldValue(""),
focused: Boolean = false,
searching: Boolean = false,
suggestions: List<S> = emptyList(),
searchResults: List<R> = emptyList()
): SearchState<R, S> {
return remember {
SearchState(
query = query,
focused = focused,
searching = searching,
suggestions = suggestions,
searchResults = searchResults
)
}
}
remember function to keep state for this only to be evaluated during the composition.
class SearchState<R, S>(
query: TextFieldValue,
focused: Boolean,
searching: Boolean,
suggestions: List<S>,
searchResults: List<R>
) {
var query by mutableStateOf(query)
var focused by mutableStateOf(focused)
var searching by mutableStateOf(searching)
var suggestions by mutableStateOf(suggestions)
var searchResults by mutableStateOf(searchResults)
val searchDisplay: SearchDisplay
get() = when {
!focused && query.text.isEmpty() -> SearchDisplay.InitialResults
focused && query.text.isEmpty() -> SearchDisplay.Suggestions
searchResults.isEmpty() -> SearchDisplay.NoResults
else -> SearchDisplay.Results
}
}
And change state in any part of UI by passing state to other composable or by ViewModel as
fun HomeScreen(
modifier: Modifier = Modifier,
viewModel: HomeViewModel,
navigateToTutorial: (String) -> Unit,
state: SearchState<TutorialSectionModel, SuggestionModel> = rememberSearchState()
) {
Column(
modifier = modifier.fillMaxSize()
) {
SearchBar(
query = state.query,
onQueryChange = { state.query = it },
onSearchFocusChange = { state.focused = it },
onClearQuery = { state.query = TextFieldValue("") },
onBack = { state.query = TextFieldValue("") },
searching = state.searching,
focused = state.focused,
modifier = modifier
)
LaunchedEffect(state.query.text) {
state.searching = true
delay(100)
state.searchResults = viewModel.getTutorials(state.query.text)
state.searching = false
}
when (state.searchDisplay) {
SearchDisplay.InitialResults -> {
}
SearchDisplay.NoResults -> {
}
SearchDisplay.Suggestions -> {
}
SearchDisplay.Results -> {
}
}
}
}
Jetmagic is an open source framework that deals exactly with this issue while also solving other major issues that Google neglected when developing Compose. Concerning your request, you don't pass in viewmodels at all as parameters. Jetmagic follows the "hoisted state" pattern, but it manages the viewmodels for you and keeps them associated with your composables. It treats composables as resources in a way that is similar to how the older view system treats xml layouts. Instead of directly calling a composable function, you ask Jetmagic's framework to provide you with an "instance" of the composable that best matches the device's configuration. Keep in mind, under the older xml-based system, you could effectively have multiple layouts for the same screen (such as one for portrait mode and another for landscape mode). Jetmagic picks the correct one for you. When it does this, it provides you with an object that it uses to manage the state of the composable and it's related viewmodel.
You can easily access the viewmodel anywhere within your screen's hierarchy without the need to pass the viewmodel down the hierarchy as parameters. This is done in part using CompositionLocalProvider.
Jetmagic is designed to handle the top-level composables that make up your screen. Within your composable hierarchy, you still call composables as you normally do but using state hoisting where it makes sense.
The best thing is to download Jetmagic and try it out. It has a great demo that illustrates the solution you are looking for:
https://github.com/JohannBlake/Jetmagic

Jetpack Compose: How to change theme from light to dark mode programmatically onClick

TL;DR change the theme and recompose the app between light and dark themes onClick.
Hello! I have an interesting issue I have been struggling to figure out and would love some help. I am trying to implement a settings screen which lets the user change the theme of the app (selecting Dark, Light, or Auto which matches system setting).
I am successfully setting the theme dynamically via invoking the isSystemInDarkTheme() function when choosing the color palette, but am struggling to recompose the app between light and dark themes on the click of a button.
My strategy now is to create a theme model which hoists the state from the settings component which the user actually chooses the theme in. This theme model then exposes a theme state variable to the custom theme (wrapped around material theme) to decide whether to pick the light or dark color palette. Here is the relevant code -->
Theme
#Composable
fun CustomTheme(
themeViewModel: ThemeViewModel = viewModel(),
content: #Composable() () -> Unit,
) {
val colors = when (themeViewModel.theme.value.toString()) {
"Dark" -> DarkColorPalette
"Light" -> LightColorPalette
else -> if (isSystemInDarkTheme()) DarkColorPalette else LightColorPalette
}
MaterialTheme(
colors = colors,
typography = typography,
shapes = shapes,
content = content
)
}
Theme model and state variable
class ThemeViewModel : ViewModel() {
private val _theme = MutableLiveData("Auto")
val theme: LiveData<String> = _theme
fun onThemeChanged(newTheme: String) {
when (newTheme) {
"Auto" -> _theme.value = "Light"
"Light" -> _theme.value = "Dark"
"Dark" -> _theme.value = "Auto"
}
}
}
Component (UI) code
#Composable
fun Settings(
themeViewModel: ThemeViewModel = viewModel(),
) {
...
val theme: String by themeViewModel.theme.observeAsState("")
...
ScrollableColumn(Modifier.fillMaxSize()) {
Column {
...
Card() {
Row() {
Text(text = theme,
modifier = Modifier.clickable(
onClick = {
themeViewModel.onThemeChanged(theme)
}
)
)
}
}
}
Thanks so much for your time and help! ***I have elided some code here in the UI component, it is possible I have left out some closure syntax in the process.
One possibility, shown in the Jetpack theming codelab, is to set the darkmode via input parameter, which ensures the theme will be recomposed when the parameter changes:
#Composable
fun CustomTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: #Composable () -> Unit
) {
MaterialTheme(
colors = if (darkTheme) DarkColors else LightColors,
content = content
)
}
In your mainActivity you can observe changes to your viewModel and pass them down to your customTheme:
val darkTheme = themeViewModel.darkTheme.observeAsState(initial = true)
CustomTheme(darkTheme.value){
//yourContent
}
This way your compose previews can simply be styled in dark theme:
#Composable
private fun DarkPreview() {
CustomTheme(darkTheme = true) {
content
}
}
In case you want a button/switch to change the theme and make it persistent as setting, you can also achieve this by using Jetpack DataStore (recommended) or SharedPreferences, get your theme state in MainActivity and pass it to your Theme composable, and wherever you want to modify it.
You can find a complete working example with SharedPreferences in this GitHub repo.
This example is using a Singleton and Hilt for dependencies and is valid for all the preferences you want store.
Based on the docs, the official way to handle theme changes triggered by a user's action (ie. choice of a theme other than the system one through a custom built setting) is to use
AppCompatDelegate.setDefaultNightMode()
This call alone will take care of most things, including restarting any activity (thus, recomposing). For this to work, we need:
The Activity which calls setContent to extend AppCompatActvity
The user's choice to be persisted and applied at each start of the app (through AppCompatDelegate)
To define whether dark mode is enabled, your CustomTheme should also consider the value of the user's defaultNightMode preference:
#Composable
fun CustomTheme(
isDark: Boolean = isNightMode(),
content: #Composable () -> Unit
) {
MaterialTheme(
colors = if (darkTheme) DarkColors else LightColors,
content = content
)
}
#Composable
private fun isNightMode() = when (AppCompatDelegate.getDefaultNightMode()) {
AppCompatDelegate.MODE_NIGHT_NO -> false
AppCompatDelegate.MODE_NIGHT_YES -> true
else -> isSystemInDarkTheme()
}
this is nice to have as it avoids the need to get this value in an Activity just to pass it to the theme with CustomTheme(isDark = isDark).
This article goes through all of the above providing more details.
It might not be the recommended way, but one option is to use the recreate method (available since API level 11).
In order to use it outside of the activity and within your composable, you could pass the function call. Taking your code as a basis
class SomeActivity {
override fun onCreate(savedInstanceState: Bundle?) {
...
setContent {
...
themeViewModel.onRecreate = { recreate() }
CustomTheme(themeViewModel) {
...
}
}
}
}
In your viewModel
class ThemeViewModel: ViewModel() {
lateinit var onRecreate: () -> Unit
private val _theme = MutableLiveData("Auto")
val theme: LiveData<String> = _theme
fun onThemeChanged(newTheme: String) {
when (newTheme) {
"Auto" -> _theme.value = "Light"
"Light" -> _theme.value = "Dark"
"Dark" -> _theme.value = "Auto"
}
onRecreate
}
}
I just made it working with a simple model.
Here is how you do it.
Declare a public variable isUiModeDark
lateinit var isUiModeDark : MutableState<Boolean?>
Then, inside your theme's Lambda:
isUiModeIsDark = remember { mutableStateOf(userSelectedThemeIsNight(context)) }
val colorScheme = when (isUiModeIsDark.value) {
true -> DarkColorScheme
false -> LightColorScheme
else -> if (systemTheme) DarkColorScheme else LightColorScheme
}
.
.
.
MaterialTheme(
colorScheme = colorScheme,
content = content
)
Now how to call recomposition? Simple, just change the value of isUiModeDark in your settings.
true means dark theme app override is selected
false means light theme app override is selected
null means No theme app override is selected and system theme is followed.
As to why this works just read the docs on MutableState and remember. Basically
any change in MutableStates value causes recomposition. Neither Live data nor any other alternatives work until or unless they are derivates of the type State. The composable is basically just subscribed to the changes made to MutableState or any derivates of the State class.

Jetpack Compose saving state on orientation change

I am using Android Jetpack's Compose and have been trying to figure out how to save state for orientation changes.
My train of thought was making a class a ViewModel. As that generally worked when I would work with Android's traditional API.
I have used remember {} and mutableState {} to update the UI when information has been changed.
Please validate if my understanding is correct...
remember = Saves the variable and allows access via .value, this allows values to be cache. But its main use is to not reassign the variable on changes.
mutableState = Updates the variable when something is changed.
Many blog posts say to use #Model, however, the import gives errors when trying that method.
So, I added a : ViewModel()
However, I believe my remember {} is preventing this from working as intended?
Can I get a point in the right direction?
#Composable
fun DefaultFlashCard() {
val flashCards = remember { mutableStateOf(FlashCards())}
Spacer(modifier = Modifier.height(30.dp))
MaterialTheme {
val typography = MaterialTheme.typography
var question = remember { mutableStateOf(flashCards.value.currentFlashCards.question) }
Column(modifier = Modifier.padding(30.dp).then(Modifier.fillMaxWidth())
.then(Modifier.wrapContentSize(Alignment.Center))
.clip(shape = RoundedCornerShape(16.dp))) {
Box(modifier = Modifier.preferredSize(350.dp)
.border(width = 4.dp,
color = Gray,
shape = RoundedCornerShape(16.dp))
.clickable(
onClick = {
question.value = flashCards.value.currentFlashCards.answer })
.gravity(align = Alignment.CenterHorizontally),
shape = RoundedCornerShape(2.dp),
backgroundColor = DarkGray,
gravity = Alignment.Center) {
Text("${question.value}",
style = typography.h4, textAlign = TextAlign.Center, color = White
)
}
}
Column(modifier = Modifier.padding(16.dp),
horizontalGravity = Alignment.CenterHorizontally) {
Text("Flash Card application",
style = typography.h6,
color = Black)
Text("The following is a demonstration of using " +
"Android Compose to create a Flash Card",
style = typography.body2,
color = Black,
textAlign = TextAlign.Center)
Spacer(modifier = Modifier.height(30.dp))
Button(onClick = {
flashCards.value.incrementQuestion();
question.value = flashCards.value.currentFlashCards.question },
shape = RoundedCornerShape(10.dp),
content = { Text("Next Card") },
backgroundColor = Cyan)
}
}
}
data class Question(val question: String, val answer: String) {
}
class FlashCards: ViewModel() {
var flashCards = mutableStateOf( listOf(
Question("How many Bananas should go in a Smoothie?", "3 Bananas"),
Question("How many Eggs does it take to make an Omellete?", "8 Eggs"),
Question("How do you say Hello in Japenese?", "Konichiwa"),
Question("What is Korea's currency?", "Won")
))
var currentQuestion = 0
val currentFlashCards
get() = flashCards.value[currentQuestion]
fun incrementQuestion() {
if (currentQuestion + 1 >= flashCards.value.size) currentQuestion = 0 else currentQuestion++
}
}
There is another approach to handle config changes in Compose, it is rememberSaveable. As docs says:
While remember helps you retain state across recompositions, the state is not retained across configuration changes. For this, you must use rememberSaveable. rememberSaveable automatically saves any value that can be saved in a Bundle. For other values, you can pass in a custom saver object.
It seems that Mohammad's solution is more robust, but this one seems simpler.
UPDATE:
There are 2 built-in ways for persisting state in Compose:
remember: exists to save state in Composable functions between recompositions.
rememberSaveable: remember only save state across recompositions and doesn't handle configuration changes and process death, so to survive configuration changes and process death you should use remeberSaveable instead.
But there are some problems with rememberSaveable too:
Supports primitive types out of the box, but for more complex data, like data class, you must create a Saver to explain how to persist state into bundle,
rememberSaveable uses Bundle under the hood, so there is a limit of how much data you can persist in it, if data is too large you will face TransactionTooLarge exception.
with above said, below solutions are available:
setting android:configChangesin Manifest to avoid activity recreation in configuration changes. (not useful in process death, also doesn't save you from being recreated in Wallpaper changes in Android 12)
Using a combination of ViewModel + remeberSaveable + data persistance in storage
=======================================================
Old answer
Same as before, you can use Architecture Component ViewModel to survive configuration changes.
You should initialize your ViewModel in Activity/Fragment and then pass it to Composable functions.
class UserDetailFragment : Fragment() {
private val viewModel: UserDetailViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return ComposeView(context = requireContext()).apply {
setContent {
AppTheme {
UserDetailScreen(
viewModel = viewModel
)
}
}
}
}
}
Then your ViewModel should expose the ViewState by something like LiveData or Flow
UserDetailViewModel:
class UserDetailViewModel : ViewModel() {
private val _userData = MutableLiveData<UserDetailViewState>()
val userData: LiveData<UserDetailViewState> = _userData
// or
private val _state = MutableStateFlow<UserDetailViewState>()
val state: StateFlow<UserDetailViewState>
get() = _state
}
Now you can observe this state in your composable function:
#Composable
fun UserDetailScreen(
viewModel:UserDetailViewModel
) {
val state by viewModel.userData.observeAsState()
// or
val viewState by viewModel.state.collectAsState()
}

Categories

Resources