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()
}
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 have a Screen ( composable function ) that gets It's data from view model ( a list and two function to remove and add data in it ).
#Composable
fun MainScreen(
notes: List<Note>,
onAddNote: (Note) -> Unit,
onRemoveNote: (Note) -> Unit
){}
Now when i call this function inside the composable of my Nav host, I get errors stating that i should fill the parameters.
#Composable
fun NotesNavigation(){
val navController = rememberNavController()
NavHost(navController = navController, startDestination = Navigation.MainScreen.name){
composable(Navigation.MainScreen.name){
MainScreen() // error here
}
}
}
Now I am wondering what is the best practice to sort it out, do i need to provide default values for my parameters like supplying an empty list
or
there is better way to get around it.
You can set default values to the MainScreenFunction, but since you are using navigation, this would become useless. I would suggest to set the viewmodel as a parameter. The viewmodel should still be passed through the navigation though.
I don't know if you use any dependency injection. If so, that would make it a bit easier. Then you can set it up like this:
#Composable
fun MainScreen(
viewModel: MainScreenViewModel = getViewModel() //If using Koin DI
){
...
}
This way, the navigation doesn't have to know about the viewmodel. You can still set it though, if you do need a different viewModel then the one injected for example.
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
{}
In the case of using Di, the way it is written on the official Android website is as follows
// import androidx.hilt.navigation.compose.hiltViewModel
#Composable
fun MyApp() {
NavHost(navController, startDestination = startRoute) {
composable("example") { backStackEntry ->
// Creates a ViewModel from the current BackStackEntry
// Available in the androidx.hilt:hilt-navigation-compose artifact
val exampleViewModel = hiltViewModel<ExampleViewModel>()
ExampleScreen(exampleViewModel)
}
/* ... */
}
}
Then if there are a lot of other #Composable functions in the ExampleScreen, like this
ExampleScreen() {
A()
B()
}
A() {
TopBar()
BottomBar()
....
}
B() ...
If both A() and its sub-functions need to use things in vm, don't you have to pass the vm parameters one by one? Because if vm is created in these functions, it is not a singleton(Because navigation compose affects the viewModel, each time you switch the page, these viewModels will be recreated as a new one). When I was puzzled, I saw this design idea on the official website again:
Pass explicit parameters
The general idea is that I should pass the logic code of the child function in the parent function, e.g. in ExampleScreen write:
ExampleScreen() {
val vm = hilt<VM>()
A(onClick = vm.onClick, ...)
B(...)
}
So my question is, if I have a lot of nested functions, don't I need to write a logical parameter in each function? So if I want to create a vm directly in each function, but it is not a singleton, what should I do? Im confused
You've done the right thing by injecting the viewmodel at the top-level. It's now up to you to decide how you want to pass it down. They're just functions in the end.
You can pass the viewmodel down everywhere, pass down only specific members or pass nothing down.
Do what makes sense and iterate if it doesn't work.
I have a ViewModel that takes a string as an argument
class ComplimentIdeasViewModel(ideaCategory : String) : ViewModel() {
//some code here
}
What is the best way to initiate this ViewModel inside a composable fun without using a ViewModel factory and Hilt? A simple statement seems to achieve this inside a composable fun
#Composable
fun SampleComposableFun() {
val compIdeasViewModel = remember { ComplimentIdeasViewModel("someCategory") }
}
There is no warning in Android studio when I try to do this, but this seems too easy to be true, I am able to do this without Dependency Injection and with a ViewModelFactory class. Am I missing something here?
I've tried how you have written yours out and I had issues with screen rotation resetting the view model. I suspect you may too.
I was able to fix it by utilizing the the factory parameter on viewModel() for this, which worked well for me. See this answer on a similar question with example on how to use it: jetpack compose pass parameter to viewModel
This will not provide you the correct instance if viewmodel. See if you store some state in the viewmodel, then using the factory to initialise it is necessary to ensure that you get the same and latest copy of the viewmodel currently present. There is no error since the syntactic implementation is correct. I do not know of any way to do this because most of the times, you don't need to. Why don't you initialise it in the top-level container, like the activity? Then pass it down wherever necessary.
Create a CompositionLocal for your ViewModel.
val YourViewModel = compositionLocalOf { YourViewModel(...) }
Then initialise it (You'd likely use the ViewModelProvider.Factory here). And then provide that to your app.
CompositionLocalProvider(
YourViewModel provides yourInitialisedViewModel,
) {
YourApp()
}
Then reference it in the composable.
#Composable
fun SampleComposableFun(
compIdeasViewModel = YourViewModel.current
) {
...
}
Note, the docs say that ViewModels are not a good fit for CompositionLocals because they will make your composable harder to test, make your composables tied to this app and make it harder to use #Preview.
Some get pretty angry about this. However, if you manage to mock out the ViewModel, so you can test the app and use #Preview and your composables are tied to the app and not generic, then I see no problem.
You can mock a ViewModel fairly simply, providing its dependencies are included as parameters (which is good practice anyway).
open class MockedViewModel : MyViewModel(
app = Application(),
someOtherDependeny = MockedDependecy(),
)
The more dependencies your ViewModel has the more mocking you'll need to do. But I've not found it prohibitive and including the ViewModel as a default parameter has massively sped up development.