Based on my theme I use different layouts in my app, so I cant use DataBinding in my use case because this would clutter my whole code with functions like following:
fun getCVAnswer() : CardView {
return when (ThemeManager.themeStyle()) {
ThemeManager.Style.Modern -> return (binding as MviGameActivityModern).cvAnswer1
ThemeManager.Style.Round -> return (binding as MviGameActivityRound).cvAnswer1
...
}
}
So I want to use an alternative solution, all my styles have the same layouts and same ids, so I can do following that works in all styles:
private val cvAnswer1 by lazy { findViewById<CardView>(R.id.cvAnswer1) }
Question
After screen rotation, I will leak the activity and cvAnswer1 has the wrong view in it (the one from my activity before rotation). Any ideas how to solve this?
Related
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()
}
I am trying to get a view model in two places, one in the MainActivity using:
val viewModel:MyViewModel by viewModels()
The Other place is inside a compose function using:
val viewModel:MyViewModel = hiltViewModel()
When I debug, it seems that those are two different objects. Is there anyway where I can get the same object in two places ?
Even though you solved your issue without needing the view model, the question remained unanswered so I am posting this in case someone else finds it helpful.
This answer explains how view model scopes are shared and how you can override it.
In case you are using Navigation component, this should help. However, if you don't want to pass down view models or override the provided ViewModelStoreOwners, you can access the parent activity's view model in any child composable like below.
val composeView = LocalView.current
val activityViewModel = composeView.findViewTreeViewModelStoreOwner()?.let {
hiltViewModel<MyViewModel>(it)
}
On iOS there is EmptyView here https://developer.apple.com/documentation/swiftui/emptyview. But I don't know how to implement it on Compose. If I have it, for some code is much easier for me. For example,
myList.map { item ->
if item is XItem -> EmptyView()
....
}
Don't tell me I need not it, I just know how to implement it. Thanks.
Compose is built much different than SwiftUI.
In SwiftUI you need to use EmptyView in two cases:
When you have a genetic parameter and it should be empty in some cases - e.g. you need to define some default type in case when the parameter is not specified.
When the context requires you to return some view.
On the other side, Compose doesn't have such problems in the first place, that's why no such view exists.
In cases when SwiftUI will give you an error around an empty #ViewBuilder block, Compose will be totally fine.
In your example you can use Unit:
myList.map { item ->
if item is XItem -> Unit
....
}
Or just empty braces:
myList.map { item ->
if item is XItem -> { }
....
}
If you'll find a case when you really need some empty view, you can use Box(Modifier) - it'll be an empty view with zero size.
I think you can use a Spacer component to display an empty space.
Spacer accepts Modifier object as a parameter, you can then use this modifier to set Spacer’s width or height or both.
For instance, you can draw a Spacer in your code but this needs to be done in a composable context or inside another composable.
#Composable
fun MyComposable(){
myList.map { item ->
if item is XItem -> Spacer(modifier = Modifier.size(100.dp, 100.dp))
....
}
}
You can easily create one yourself:
#Composable
fun EmptyView() {
}
which can be replaced / inlined by
{}
I'm making an implementation of PreferencesScreen in Compose and I've made all the components like PreferencesSwitch, CheckBox, etc.
Now I'm wondering if there is any way to make it so all the components can only be used inside the scope of the PreferencesScreen function and cannot be used outside of it.
Like for example, in LazyColumn, items can only be used inside LazyColumnScope. I looked at the implementation of it but it used the annotation #LazyScopeMarker so I'm assuming there's different markers for different scopes?
Expected Behaviour:
PreferencesScreen{
PreferencesCheckBox(...){ ... }
}
is possible but,
PreferencesCheckBox(...){ ... }
alone is not possible.
You can declare some scope same like LazyColumn does:
interface PreferencesScreenScope {
#Composable
fun PreferencesCheckBox()
}
private class PreferencesScreenScopeImpl: PreferencesScreenScope {
#Composable
override fun PreferencesCheckBox() {
}
}
interface/class ...Impl is used here to make sure that no other screen can reuse PreferencesScreenScopeImpl, also it adds testing possibility.
Use it in PreferencesScreen:
#Composable
fun PreferencesScreen(content: #Composable PreferencesScreenScope.() -> Unit ) {
PreferencesScreenScopeImpl().content()
}
Use PreferencesScreen like this:
PreferencesScreen {
PreferencesCheckBox()
}
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.