Composable architecture with viewmodels and LiveData/StateFlow - android

I've created simple application to scan products barcode and retrieve information from API by this code. My whole application is routed by this composable:
#Composable
fun AppNavigationHost(navController: NavHostController, modifier: Modifier = Modifier) {
val sharableViewModel: SharableViewModel = hiltViewModel()
NavHost(navController = navController, startDestination = Main.route, modifier = modifier) {
composable(route = Main.route) {
MainScreen(viewModel = sharableViewModel, onSuccessfulProductScan = {
LaunchedEffect(sharableViewModel.product) {
navController.navigate(ProductDetail.route) {
popUpTo(Main.route)
}
}
})
}
composable(route = ProductDetail.route) {
ProductDetailsScreen(viewModel = sharableViewModel)
}
}
}
It works in a simple way:
From MainScreen i call viewModel.findProduct when button is clicked.
When product not exists, it has state NOT_FOUND and simply returns message, that product does not exist. My state has type mutableStateFlow<ProductState>. When It was LiveData, I couldn't navigate between my composables.
When product exists, I update my product in viewModel of type mutableLiveData<Product>. When I change it to StateFlow, my whole app crushes.
ProductDetailsScreen observe product from viewModel. When it's filled with data, composable updates it's view with product information.
Now, I don't understand few things, like:
I must initialize my SharedViewModel in shareable place like AppNavigationHost. When I add hiltViewModel() as a default parameter in those composables, my app instantly crashes. Why I can't inject viewModel with it's state as a separated parameter in composable?
Why I need to use StateFlow when I have to navigate between composables, why LiveData is not enought to handle it? Is there any possibility to use StateFlow instead of LiveData through navigated views without handling LaunchedEffect scope?
Why I need to use LiveData to persist object in ViewModel between composables? StateFlow is bounded to viewModel, not to composable. It should "emit" my product once and next view should retrieve that event.
I wish I could understand well basics of composable concept, but some of them are not clear enough for me, e.g. passing data in other way than shareable view model.

Related

How to pass an object through Navigation in Android compose?

I am using Navigation Compose, and I am trying to pass an entire object to the next screen, So I am using a shared ViewModel through hilt, and created a mutablestate variable of that object and want to get its value in the next screen.
Like This
var campaign = mutableStateOf<Campaign?>(null)
private set
fun addCampaign(campaign: Campaign){
this.campaign.value = campaign
}
where Campaign is just a data-class.
In my Screen in Navigation
LazyColumn (
content =
{
items(viewModel.campaignListCurrent){ campaign ->
CampaignItem(
image = campaign.brand?.image ?: "",
title = campaign.name ,
id = campaign.id ,
description =campaign.description ,
date =campaign.createdAt ) {
viewModel.addCampaign(
CampaignsViewModel.Campaign(...) // the "campaign" object is used to fill this
)
Timber.tag("CampaignObject").v(viewModel.campaign.value.toString())
viewModel.changed = true
navController.navigate(Screen.CampaignDetailsScreen.route)
}
}
})
I can see when I log the data that it was stored successfully. yet in the next screen when I get the data from the same ViewModel, it's null. even though its a mutablestate and supposed to change its value and be observables, I don't think I get mutablestate behavior at all, and any link for a proper explanation for it will be appreciated.
val campaign by remember { viewModel.campaign }
Timber.v("CampaignDetailsScreen2: " + campaign.toString())
can someone explain to me why it doesn't work here, even if I used Launched effect??
why doesn't change its value here?
I managed to solve this by Creating a SharedViewModel instance in the NavGraph to make sure it is the same Instance used in both composable functions.
#Composable
fun CampaignsScreen(
navController : NavController,
viewModel: CampaignsViewModel
) {
....
#Composable
fun CampaignDetailsScreen(
navController : NavController,
viewModel: CampaignsViewModel
) {
and in my NavGraph I passed the ViewModel as HiltViewmodel in the constructor and from it to the two screens
#Composable
fun SetUpNavGraph (navController : NavHostController,
campaignViewModel: CampaignsViewModel = hiltViewModel()) {
...........
loginNavGraph(navController = navController)
homeNavGraph(navController = navController, campaignsViewModel = campaignViewModel)
}
}
and in the home NavGraph I passed the parametere and its the same Instance in both
fun NavGraphBuilder.homeNavGraph(
navController : NavHostController,
campaignsViewModel: CampaignsViewModel
){
navigation(startDestination = Screen.HomeScreen.route, route = HOME_GRAPH_ROUTE) {
................
composable(Screen.Campaigns.route){
CampaignsScreen(navController = navController, viewModel = campaignsViewModel)
}
composable(Screen.CampaignDetailsScreen.route){
CampaignDetailsScreen(navController, viewModel = campaignsViewModel)
}
}
}
You can use backstackEntry and PreviouseBackStack to solve this but it won't work if you have a JSON Object within your object as you will need to Serialize and Deserialize this object manually and it will take more time and code to achieve the same goal for a shared ViewModel to pass Parclized object. until Google Solve passing Parclized objects in Compose Navigation I recommend backStack Entry if you don't have a JSON object within your paralyzed object and if you do, use a shared ViewModel between the tow composable function.
Why would it work if you are just remembering a local value and not even reading from the proper model?
val campaign = viewModel.campaign
Can't just use remember willy-nilly without understanding the proper usage.

How to safely (lifecycle aware) .collectAsState() a StateFlow?

I'm trying to follow the official guidelines to migrate from LiveData to Flow/StateFlow with Compose, as per these articles:
A safer way to collect flows from Android UIs
Migrating from LiveData to Kotlin’s Flow
I am trying to follow what is recommended in the first article, in the Safe Flow collection in Jetpack Compose section near the end.
In Compose, side effects must be performed in a controlled
environment. For that, use LaunchedEffect to create a coroutine that
follows the composable’s lifecycle. In its block, you could call the
suspend Lifecycle.repeatOnLifecycle if you need it to re-launch a
block of code when the host lifecycle is in a certain State.
I have managed to use .flowWithLifecycle() in this way to make sure the flow is not emmiting when the app goes to the background:
#Composable
fun MyScreen() {
val lifecycleOwner = LocalLifecycleOwner.current
val someState = remember(viewModel.someFlow, lifecycleOwner) {
viewModel.someFlow
.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.stateIn(
scope = viewModel.viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = null
)
}.collectAsState()
}
I find this very "boilerplatey" -there must be something better. I would like to have StateFlow in the ViewModel, instead of Flow that gets converted to StateFLow in the #Composable, and use .repeatOnLifeCycle(), so I can use multiple .collectAsState() with less boilerplate.
When I try to use .collectAsState() inside a coroutine (LaunchedEffect), I obviously get an error about .collectAsState() having to be called from the context of #Composable function.
How can I achieve similar functionality as with .collectAsState(), but inside .repeatOnLifecycle(). Do I have to use .collect() on the StateFlow and then wrap the value with State? Isn't there anything with less boilerplate than that?
From "androidx.lifecycle:lifecycle-runtime-compose:2.6.0-alpha01" you can use the collectAsStateWithLifecycle() extension function to collect from flow/stateflow and represents its latest value as Compose State in a lifecycle-aware manner.
import androidx.lifecycle.compose.collectAsStateWithLifecycle
#Composable
fun MyScreen() {
val state by viewModel.state.collectAsStateWithLifecycle()
}
Source: Android Lifecycle release
After reading a few more articles, including
Things to know about Flow’s shareIn and stateIn operators
repeatOnLifecycle API design story
and eventually realising that I wanted to have the StateFlow in the ViewModel instead of within the composable, I came up with these two solutions:
1. What I ended up using, which is better for multiple StateFlows residing in the ViewModel that need to be collected in the background while there is a subscriber from the UI (in this case, plus 5000ms delay to deal with configuration changes, like screen rotation, where the UI is still interested in the data, so we don't want to restart the StateFlow collecting routine). In my case, the original Flow is coming from Room, and been made a StateFlow in the VM so other parts of the app can have access to the latest data.
class MyViewModel: ViewModel() {
//...
val someStateFlow = someFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = Result.Loading()
)
val anotherStateFlow = anotherFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = Result.Loading()
)
//...
}
Then collected in the UI:
#Composable
fun SomeScreen() {
var someUIState: Any? by remember { mutableStateOf(null)}
var anotherUIState: Any? by remember { mutableStateOf(null)}
LaunchedEffect(true) {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.someStateFlow.collectLatest {
someUIState = it
}
}
launch {
viewModel.anotherStateFlow.collectLatest {
anotherUIState = it
}
}
}
}
}
2. An extension function to alleviate the boilerplate when collecting a single StateFlow as State within a #Composable. This is useful only when we have an individual HOT flow that won't be shared with other Screens/parts of the UI, but still needs the latest data at any given time (hot flows like this one created with the .stateIn operator will keep collecting in the background, with some differences in behaviour depending on the started parameter). If a cold flow is enough for our needs, we can drop the .stateIn operator together with the initial and scope parameters, but in that case there's not so much boilerplate and we probably don't need this extension function.
#Composable
fun <T> Flow<T>.flowWithLifecycleStateInAndCollectAsState(
scope: CoroutineScope,
initial: T? = null,
context: CoroutineContext = EmptyCoroutineContext,
): State<T?> {
val lifecycleOwner = LocalLifecycleOwner.current
return remember(this, lifecycleOwner) {
this
.flowWithLifecycle(
lifecycleOwner.lifecycle,
Lifecycle.State.STARTED
).stateIn(
scope = scope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = initial
)
}.collectAsState(context)
}
This would then be used like this in a #Composable:
#Composable
fun SomeScreen() {
//...
val someState = viewModel.someFlow
.flowWithLifecycleStateInAndCollectAsState(
scope = viewModel.viewModelScope //or the composable's scope
)
//...
}
Building upon OP's answer, it can be a bit more light-weight by not going through StateFlow internally, if you don't care about the WhileSubscribed(5000) behavior.
#Composable
fun <T> Flow<T>.toStateWhenStarted(initialValue: T): State<T> {
val lifecycleOwner = LocalLifecycleOwner.current
return produceState(initialValue = initialValue, this, lifecycleOwner) {
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
collect { value = it }
}
}
}

Android compose Navigation and ViewModel constructor calls

Currently I'm using compose with navigation and viewmodels.
The code of my NavHost is the following
composable(MyRoute.name + "/{param}") { backStackEntry ->
val param = backStackEntry.arguments?.getString("id") ?: ""
val viewModel = hiltViewModel<MyViewModel>()
MyComposable(
viewModel = viewModel
)
}
The issue I'm facing is that viewModel.init is called an infinite number of times (I guess it is recomposition), but the viewModel is supposed to have only one instance that outlives the lifecycle of the composables.
Use a LaunchedEffect to run your network call.
See this for reference.

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

Is there a better way to update text in jetpack compose?

Is there a better way to update the text value here by the value from the database?
#Composable
private fun DisplayShops() {
var shopid by remember { mutableStateOf("")}
SideEffect {
val value = GlobalScope.async {
val res = withContext(Dispatchers.Default) {
getDbData() // this gets the database data
delay(1000)
shopid=shop_id// the shop_id is variable defined in the activity and it has the value retrieved from the database
}
}
}
Text(text =shopid)
}
That's not a good solution for 2 reasons:
the code will run at each recomposition, because you are using SideEffect, you probably want to use LaunchedEffect instead
placing your business logic in your composables is not the right solution, makes your composables tightly coupled to your business layer and hard to test
You should consider creating a ViewModel that will fetch the data from the database and then expose the value you want to display from the ViewMOdel using a MutableState object that you can then observe in your composable.
You can read this for more details.

Categories

Resources