Jetpack compose State Hoisting - android

In Jetpack Compose can I use state hoisting and pass down state / setState on multiple composables down the hierarchy (with different packages) ?
I have the following issue:
#Composable
fun JetpackComposeApp() {
var theme by rememberSaveable {
mutableStateOf(ThemeState(isDarkTheme = isDarkTheme))
}
...
NavHost(...) {
composable(route = MainNavRoutes.Settings) {
SettingsScreen(hiltViewModel(it), theme) { newTheme ->
theme = newTheme
}
}
}
}
The above works. SettingsScreen updates whole theme correctly.
But if I refactor Navhost to a different package with arguments (theme, setTheme), setter works but SettingsScreen doesn't get the updated theme. Theme is always the old one.
Is state hoisting limited to something ? Like package / composables optimization ?
Or I am missing something here ?
The above can be found in repo: link, files: MainActivity, SettingsScreen.

updating/observing state value should work even with deep call stacks, if I understand correctly your problem you should wrap setTheme in a rememberUpdatedState() within your SettingsScreen and access/hoist this updated value

Related

Manage condition logic in stateless compose in jetpack compose

I am learning State hosting in jetpack compose. I have created two separated function ContentStateful and ContentStateLess. In my ContentStateLess there is a lot of view inside them and I am checking some condition and change view accordingly. I am guessing that there is no condition/business logic inside Stateful compose. So what is the proper way of doing this kind of logic in here.
ContentStateful
#Composable
fun ContentStateful(
viewModel: PairViewModel = getViewModel()
) {
ContentStateLess(viewModel)
}
ContentStateLess
#Composable
fun ContentStateLess(
viewModel: PairViewModel
) {
Text()
Text()
Image()
if (viewModel.isTrue) {
Image()
// more item here
} else {
Text()
// more item here
}
Image()
}
So what is the best recommendation for this if - else logic in ContentStateLess(). Many Thanks
If you are building stateless Composables it's better not to pass anything like ViewModel. You can pass Boolean parameter instead. When you wish to move your custom Composable to another screen or another project you will need to move ViewModel either.
The reason Google recommends stateless Composables is it's difficult to test, you can easily test a Composable with inputs only.
Another thing you experience the more states inner composables have to more exposure you create for your composable being in a State that you might not anticipate.
When you build simple Composables with one, two, three layers might not be an issue but with more states and layers state management becomes a serious issue. And if you somehow forget or miss a state inside a Composable you might end up with a behavior that's not expected. So to minimize risks and make your Composables testable you should aim to manage your states in one place and possible in a state holder class that wraps multiple states.
#Composable
fun ContentStateLess(
firstOneTrue: Boolean
) {
Text()
Text()
Image()
if (firstOneTrue) {
Image()
// more item here
} else {
Text()
// more item here
}
Image()
}

Activity Launcher(File picker) is loading multiple times in single event - Jetpack compose

I am using a file picker inside a HorizontalPager in jetpack compose. When the corresponding screen is loaded while tapping the button, the launcher is triggered 2 times.
Code snippet
var openFileManager by remember {
mutableStateOf(false)
}
if (openFileManager) {
launcher.launch("*/*")
}
Button(text = "Upload",
onClick = {
openFileManager = true
})
Edited: First of all Ian's point is valid why not just launch it in the onClick directly? I also assumed that maybe you want to do something more with your true false value. If you want nothing but launch then all these are useless.
The screen can draw multiple times when you click and make openFileManager true so using only condition won't prevent it from calling multiple times.
You can wrap your code with LaunchedEffect with openFileManager as a key. The LaunchedEffect block will run only when your openFileManager change.
if (openFileManager) {
LaunchedEffect(openFileManager) {
launcher.launch("*/*")
}
}
You should NEVER store such important state inside a #Composable. Such important business logic is meant to be stored in a more robust holder like the ViewModel.
ViewModel{
var launch by mutableStateOf (false)
private set
fun updateLaunchValue(newValue: Boolean){
launch = newValue
}
}
Pass these to the Composable from the main activity
MyComposable(
launchValue = viewModel.launch
updateLaunchValue = viewModel::updateLaunchValue
)
Create the parameters in the Composable as necessary
#Comoosable
fun Uploader(launchValue: Boolean, onUpdateLaunchValue: (Boolean) -> Unit){
LaunchedEffect (launchValue){
if (launchValue)
launcher.launch(...)
}
Button { // This is onClick
onUpdateLaunchValue(true) // makes the value true in the vm, updating state
}
}
If you think it is overcomplicated, you're in the wrong paradigm. This is the recommended AND CORRECT way of handling state in Compose, or any declarative paradigm, really afaik. This keeps the code clean, while completely separating UI and data layers, allowing controlled interaction between UI and state to achieve just the perfect behaviour for the app.

SetTheme to an activity with PreferencesDataStore

I have switched my app from using SharedPreferences to PreferencesDataStore. I have also implemented a dark mode and several themes inside my app. For theming, I basically rely in each activity on this code:
val themePreferences = getSharedPreferences("THEME_PREFERENCES", MODE_PRIVATE)
val settingsPreferences = getSharedPreferences("SETTINGS_PREFERENCES", MODE_PRIVATE)
darkMode = settingsPreferences.getBoolean("darkMode", false)
setTheme(when (darkMode) {
true -> themePreferences.getInt("themeDark", R.style.AppThemeDark)
false -> themePreferences.getInt("themeLight", R.style.AppThemeLight)
})
The selected theme gets stored as an Integer, each once for the light mode as well as the dark mode. Now, I also want to adopt this section of my code. I get my darkMode boolean from my dataStore repository like this:
viewModel.storedDarkMode.observe(this) { darkMode = it }
If I work inside the observe(this) { ... } of the LiveData object it won't work.
Now, how can I change my code snippet shown above to PreferencesDataStore? Or is it actually better e.g. to make a separate class in order to observe the values from there? If yes, how could something like that look like? Or do you know some good example code following Android Architecture including custom themes with a dark mode where I can look at?
I am still learning a lot, any help for better understanding this is much appreciated!
Best regards, Markus
Edit:
runBlocking {
val darkMode = viewModel.darkModeFlow.first()
setTheme(when (darkMode) {
true -> viewModel.themeDarkFlow.first()
false -> viewModel.themeLightFlow.first()
})
}
I don't know if it is best practice or not, but I use runBlocking to get theme data from dataStore. It is always recommended that, we should never use runBlocking in our production code.
val preferences = runBlocking {
mainActivityViewModel.preferencesFlow.first()
}
setAppTheme(preferences.currentTheme)
binding = ActivityMainBinding.inflate(layoutInflater)
In setAppTheme method
private fun setAppTheme(theme: Boolean) {
mainActivityViewModel.darkMode = theme
//set your theme here
}
Now observe preferencesFlow for any change in theme value, and if the theme is changed then recreate()
mainActivityViewModel.preferencesLiveData.observe(this) {
if (it.currentTheme != mainActivityViewModel.selectedTheme) {
recreate()
}
}
As we can't load our UI, without getting the theme. It seemed right to use runBlocking.

Observe livedata and navigate in jetpack compose

I just started learning jetpack compose. I have a very basic question. My ViewModel has a SingleLiveEvent that I use to navigate to another screen.
private val _navigateToDetails: SingleLiveEvent<Movie> = SingleLiveEvent()
val navigateToDetails: MutableLiveData<Movie> = _navigateToDetails
I know that I can use Livedata as state to emit UI but how to use it to trigger some action within composable.
Previously I had used viewLifecycleOwner to observer the state as anyone would do like this.
viewModel.navigateToDetails.observe(viewLifecycleOwner) {
// navigate to details
}
How do I achieve the same thing in compose. I don't know if that's possible or not. Maybe I am not thinking this in compose way. Any help would be appreciated.
I would do something like to make sure I'm only doing it once:
#Composable
fun LoginScreen(viewModel: LoginViewModel) {
val loginState by viewModel.loginState.observeAsState(null)
val hasHandledNavigation = remember { mutableStateOf(false)}
if (loginState != null && !hasHandledNavigation.value ) {
navigateToWelcomeScreen()
else {
// rest of the Compose UI
}
}
UPDATE:
Option two, you can also just pass the action of going to next screen to viewmodel and fire it up there.
Actually, in compose we use mutableStateOf() over LiveData. In your viewmodel, you can change the type of the data holder from LiveData to mutableStateOf(...) which will allow you to directly use it in your Composables without explicitly calling observe()
Let's say you wish to store an integer of any kind in your viewmodel and update the state of your Composable based on that.
In your viewmodel,
var mInteger by mutableStateOf (0) //'by' helps treat state as data
fun updateMInteger(newValue: Int){
mInteger = newValue
}
In your Composable, directly call viewmodel.mInteger and Compose being built like that, automatically updates the UI, given that mInteger is being read from the Composable
Like
Text(viewModel.mInteger)

How to force jetpack compose to recompose?

Say that, I'm building a custom compose layout and populating that list as below
val list = remember { dataList.toMutableStateList()}
MyCustomLayout{
list.forEach { item ->
key(item){
listItemCompose( data = item,
onChange = { index1,index2 -> Collections.swap(list, index1,index2)})
}
}
This code is working fine and the screen gets recomposed whenever onChange lambda function is called, but when it comes to any small change in any item's property, it does not recompose, to elaborate that let's change the above lambda functions to do the following
{index1,index2 -> list[index1].propertyName = true}
Having that lambda changing list item's property won't trigger the screen to recompose. I don't know whether this is a bug in jetpack compose or I'm just following the wrong approach to tackle this issue and I would like to know the right way to do it from Android Developers Team. That's what makes me ask if there is a way to force-recomposing the whole screen.
You can't force a composable function to recompose, this is all handled by the compose framework itself, there are optimizations to determine when something has changed that would invalidate the composable and to trigger a recomposition, of only those elements that are affected by the change.
The problem with your approach is that you are not using immutable classes to represent your state. If your state changes, instead of mutating some deep variable in your state class you should create a new instance of your state class (using Kotin's data class), that way (by virtue of using the equals in the class that gets autogenerated) the composable will be notified of a state change and trigger a recomposition.
Compose works best when you use UDF (Unidirectional Data Flow) and immutable classes to represent the state.
This is no different than, say, using a LiveData<List<Foo>> from the view system and mutating the Foos in the list, the observable for this LiveData would not be notified, you would have to assign a new list to the LiveData object. The same principle applies to compose state.
you can recreate an entire composition using this
val text = remember { mutableStateOf("foo") }
key(text.value) {
YourComposableFun(
onClick = {
text.value = "bar"
}
) {
}
}
call this
currentComposer.composition.recompose()

Categories

Resources