Show Snackbar in MainActivity (root #Composable) from any child composable (Jetpack Compose) - android

I would like to show a snackbar in the MainActivity (root composable) from any child #Composable.
My first thought was to provide the SnackbarHostState using CompositionLocalProvider but that doesn't seem to work (or I'm doing it incorrectly).
val mainSnackBarHostState = remember { SnackbarHostState() }
val SnackBarHostStateProvider = compositionLocalOf<SnackbarHostState> { mainSnackBarHostState }
CompositionLocalProvider(SnackBarHostStateProvider provides mainSnackBarHostState) {
MainScreenNavigationConfigurations(navController)
}
My child #Composable can't seem to find/access SnackBarHostStateProvider.
Any thoughts?

The best way, I'd say, is to store the state of the snackbar (visible/invisible) in your viewmodel, and let the snackbar read from there. Whenever and wherever from you want to toggle the state, just change the value in the viewmodel, and that should do it
If you are unfamiliar with viewmodel, it is the recommended and standard way to build apps, and remember, in Compose, the recommended way is to store state in the viewmodel not as regular variables, but as stateholders.
For instance, in your use case, you can store the visibility status of your snackbar as mutableStateOf(false), for am initial visibility value of false.
This assumes that you have access to your viewmodel from all over your app, which usually developers do, wherever they need to update state, so, best of luck

Probably you need to declare the variable SnackBarHostStateProvider as a package-level variable, to be able to access it from the provider and the children.
See also this related answer: https://stackoverflow.com/a/69905470/293878

Related

Why Compose remembers a state of an object rather than the object itself?

I learned that Compose remembers a state in a way such as:
var text by remember { mutableStateOf("") }
So in this case, it remembers a MutableState of a String. My question is why it wants to remember a thing called "MutableState" rather than just the String itself, and why it requires an extra layer?
I know its late, but here's what I understand with remember.
I have a simple Todo app where a list of todos are hoisted in a viewwmodelusing a SnapshotStatelist, this list is rendered by a LazyColumn where each todo model has its own remembered state where I do some pretty basic UI functionality (e.g card elevation, visibility of some icons). Any changes I make to a todo should propagate back up to the mutableStateList (e.g deleting a todo), SnapshotStateList will then notify the LazyColumn to perform a recomposition, however when I edit a Todo (e.g, modifying the title), I also have to update the item composable that holds this todo(some UI changes), then I got stuck up as I can't figure out why the item composable is not recomposing even if I was able to verify that the SnapShotStateList item is modified by using the code below
val todoList = viewModel.todos
val snapshot = Snapshot.takeMutableSnapshot()
snapshot.enter {
for (todo in todoList) {
Log.e("TodoModel", todo.title)
}
}
Code that modifies the list
val index = todos.indexOfFirst { it.id == modifiedModel.id }
todos[index] = todos[index].copy(//..some properties to be copied)
I verified that any modification I make on a todo reflects back to its host list, but the item composable that renders a todo item doesn't trigger a re-composition. I kept reading some posts and carefully thinking object references and trying to understand my code based on this object reference thought, I thought that there must be something that holds the previous state of the item and the changes is not applied to the remember, until I found that you can supply a key to a remember where it will be the thing that will decide if remember needs to re-calculate. Now I found out that remember only remembers a state (by state I dont mean compose state, but state in general) on initial composition, it will keep that initial structure/state for as long as the entire composable it is part of is still running, in this case all the way up to the parent composable (i.e my DashboardScreen), what made my remember re-calculate is I supplied the key with the todo object itself
val itemState: ItemCardState = remember(key1 = todoModel) {
ItemCardState(todoModel = todoModel)
}
This way, when a change happens to the SnapShotStateList, an item's remember will see the same object reference (data class) but with changes applied to it. remember caches the initial state and will hold it forever unless you supply a key that you think might change and will let remember re-calculate a new initial state to be remembered, in my case I supplied the key as the todo object itself and when remember sees the same object reference but with a different value, it will re-calculate.
Having this understanding now, I can't imagine a way when there is no layer that will hold an object (remember) and prevent unnecessary re-composition when the state of an object changes.
Just sharing what I learned, also open for discussion that I may have said in a wrongful way.
remember is used for storing objects to have it when a recomposition happens. Mutable state is used for triggering recomopsition, you can check this answer for more details.
by is delegation that is a feature of Kotlin which translates the code
var text = remember { mutableStateOf("") }
text.value = "newString"
you basically store a trigger and value inside a remember. when you change MutableState.value new recomposition occurs and in this new recomposition you get the latest value of MutableState.
There are also usecases of remember without needing MutableState like a Paint or custom object when something else triggers the recomposition like canvas touch position for instance.
you remember object since you won't instantiate it.
val paint = remember {Paint()}
var offset by remember {mutableStateOf(Offset.Zero)
then when offset changes with user touching screen you trigger recomposition but since and you don't need to instantiate Paint object again.
remember only and remember with MutableState has different usecases.
Mutable state is needed for two reasons:
Saving mutable state between recompositions. remember is gonna save result of lambda calculation, but if you change a variable later - remember cannot save and track it. The solution is to have a state holder - an object that's created by mutableStateOf, saved by remember, will always be the same, but you can mutate it properties, in this case value (which is hidden when you're using delegation with by).
Triggering recomposition. If you just create a class, save it with remember and update a property, Compose won't know that it was changed, and that view update it needed - that's why a special Compose State was created, which notifies a view that it needs to be recomposed.
You can continue deepening your knowledge with state in Compose documentation and Thinking in Compose.

Changing Data Class From Live Data

I have a BaseViewModel that basically has the function to get the user data like so:
abstract class BaseViewModel(
private val repository: BaseRepository
) : ViewModel() {
private var _userResponse: MutableLiveData<Resource<UserResponse>> = MutableLiveData()
val userResponse: LiveData<Resource<UserResponse>> get() = _userResponse
fun getUserData() = viewModelScope.launch {
_userResponse.value = Resource.Loading
_userResponse.value = repository.getLoggedInUserData()
}
}
In my Fragment, I access this data by just calling viewModel.getUserData(). This works. However, I'd like to now be able to edit the data. For example, the data class of UserResponse looks like this:
data class UserResponse(
var id: Int,
var username: String,
var email: String
)
In other fragments, I'd like to edit username and email for example. How do I do access the UserResponse object and edit it? Is this a good way of doing things? The getUserData should be accessed everywhere and that is why I'm including it in the abstract BaseViewModel. Whenever the UserResponse is null, I do the following check:
if (viewModel.userResponse.value == null) {
viewModel.getUserData()
}
If you want to be able to edit the data in userResponse, really what you're talking about is changing the value it holds, right? The best way to do that is through the ViewModel itself:
abstract class BaseViewModel(
private val repository: BaseRepository
) : ViewModel() {
private var _userResponse: MutableLiveData<Resource<UserResponse>> = MutableLiveData()
val userResponse: LiveData<Resource<UserResponse>> get() = _userResponse
fun setUserResponse(response: UserResponse) {
_userResponse.value = response
}
...
}
This has a few advantages - first, the view model is responsible for holding and managing the data, and provides an interface for reading, observing, and updating it. Rather than having lots of places where the data is manipulated, those places just call this one function instead. That makes it a lot easier to change things later, if you need to - the code that calls the function might not need to change at all!
This also means that you can expand the update logic more easily, since it's all centralised in the VM. Need to write the new value to a SavedStateHandle, so it's not lost if the app goes to the background? Just throw that in the update function. Maybe persist it to a database? Throw that in. None of the callers need to know what's happening in there
The other advantage is you're actually setting a new value on the LiveData, which means your update behaviour is consistent and predictable. If the user response changes (either a whole new one, or a change to the current one) then everything observeing that LiveData sees the update, and can decide what to do with it. It's less brittle than this idea that one change to the current response is "new" and another change is "an update" and observers will only care about one of those and don't need to be notified of the other. Consistency in how changes are handled will avoid bugs being introduced later, and just make it easier to reason about what's going on
There's nothing stopping you from updating the properties of the object held in userResponse, just like there's nothing stopping you from holding a List in a LiveData, and adding elements to that list. Everything with a reference to that object will see the new data, but only if they look at it. The point of LiveData and the observer pattern is to push updates to observers, so they can react to changes (like, say, updating text displayed in a UI). If you change one of the vars in that data class, how are you going to make sure everything that needs to see those changes definitely sees them? How can you ensure that will always happen, as the app gets developed, possibly by other people? The observer pattern is about simplifying that logic - update happens, observers are notified, the end
If you are going to do things this way, then I'd still recommend putting an update function in your VM, and let that update the vars. You get the same benefits - centralising the logic, enabling things like persistence if it ever becomes necessary, etc. It could be as simple as
fun setUserResponse(response: UserResponse) {
_userResponse.value?.run {
id = response.id
username = response.username
email = response.email
}
}
and if you do decide to go with the full observer pattern for all changes later, everything is already calling the function the right way, no need for changes there. Or you could just make separate updateEmail(email: String) etc functions, whatever you want to do. But putting all that logic in the VM is a good idea, it's kinda what it's there for
Oh and you access that object through userResponse.value if you want to poke at it - but like I said, better to do that inside a function in the VM, keep that implementation detail, null-safety etc in one place, so callers don't need to mess with it
The ideal way to update userResponse you should change/edit _userResponse so that your userResponse we'll give you the updated data.
it should be something like this
_userResponse.value = Resource<UserResponse>()

Other state management options that I may use with Jetpack Compose, beyond State hoisting?

I am learning Jetpack compose, and I have been seen so far that lifting the state up to a composable's caller to make a composable stateless is the way to go. I`ve been using this pattern in my Compose apps.
For an app state that I need to modify from many different places of the tree, I will have to pass around a lot of callbacks, This can become difficult to manage.
I have some previous experience with Flutter. The way Flutter deals with providing a state to its descendants in the tree to overcome the above, is to use other mechanisms to manage state, namely Provider + ChangeNotifier.
Basically, with Provider, a Provider Widget is placed in the tree and all the children of the provider will have access to the values exposed by it.
Are there any other mechanisms to manage state in Jetpack Compose apps, besides State hoisting? And, what would you recommend?
If you need to share some data between views you can use view models.
#Composable
fun TestScreen() {
val viewModel = viewModel<SomeViewModel>()
Column {
Text("TestScreen text: ${viewModel.state}")
OtherView()
}
}
#Composable
fun OtherView() {
val viewModel = viewModel<SomeViewModel>()
Text("OtherScreen text: ${viewModel.state}")
}
class SomeViewModel: ViewModel() {
var state by mutableStateOf(UUID.randomUUID().toString())
}
The hierarchy topmost viewModel call creates a view model - in my case inside TestScreen. All children that call viewModel of the same class will get the same object. The exception to this is different destinations of Compose Navigation, see how to handle this case in this answer.
You can update a mutable state property of view model, and it will be reflected on all views using that model. Check out more about state in Compose.
The view model lifecycle is bound to the compose navigation route (if there is one) or to Activity / Fragment otherwise, depending on where setContent was called from.

Using mutableStateOf instead of observeAsState

I'm working with Jetpack Compose in an Android app and had the problem that my uiState (LiveData) was set to its initial value on every recomposition, since I've initialized it like
val authUiState: AuthUIState by authenticationViewModel.uiState.observeAsState(AuthUIState.Loading)
It was set to Loading on every recomposition before it was set to the correct value.
When I tried to Remember the value, I learned that we can't use observeAsState within the remember block and finally changed it to
val authUiState = remember{ mutableStateOf(authenticationViewModel.uiState.value) }.value
This works, but I'm not quite sure, if this is the common and good way to solve this.
What do you think? Should I do it differently? Do you need more information?
See if the uiState inside your viewmodel is something like a LiveData Object, (which is kinda what it seems like from the code), the recommended way is to store it in the viewmodel itself as mutable state.
var uiState by mutableStateOf (initialValue)
private set //Do not allow external modifications to maintain consistency of state
fun onUiStateChange(newValue: Any){
uiState = newValue
}
You just need to initialise it as a MutableState, in the rest of the code, to update, delete or whatever you want to do with it, just treat it as a regular variable. Compose will trigger recomposition every time the value is updated.
The following code snippet below will almost certainly not work, because here, the state is whatever you wrap inside mutableStateOf(), which is just a simple value which will be fetched once from the viewmodel and then remembered throughout recompositions, so no code change will be triggered here
val authUiState by remember{ mutableStateOf(authenticationViewModel.uiState.value) }
Storing state in the viewmodel as mutableState, is as far as my knowledge extends, the best practice in compose. You will see the same in the 'State Codelab' from Android developers
Good luck

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