I'm converting my project from view system to compose. In one of app page's in old view system I have a fragment with one viewPager which just make some pages of same fragment with different data to show. While each fragment has it's own lifecycle I can have multiple isolated viewModel per each fragment. In Compose as far as I know viewModel life cycle is attached to navigation graph, therefor each time I try to access viewModel it just return same viewModel object that's created in first call. how can I achieve same view system behavior in compose?
this is simplified version of what my app is doing now
#Composable
fun MainScreen(mainViewModel: MainViewModel = hiltViewModel()) {
val pages = mainViewModel.pages.collectAsState()
val pagerState = rememberPagerState(pageCount = pages.size)
HorizontalPager(state = pagerState) {
ChildScreen()
}
}
#Composable
fun ChildScreen(childViewModel:ChildViewModel = hiltViewModel()){
}
here childViewModel is always one object for all pages
Related
I have a Compose screen, with two separate components:
horizontal scroll of items
vertical scroll of card items, which need to be paginated
I also have a ViewModel attached to that screen which provides the state:
val viewState: StateFlow<MyScreenState> = _viewState
...
data class MyScreenState(
val horizontalObjects: List<MyObject>,
val verticalCardsPaged: Flow<PagingData<MyCard>>
)
The cards are paged, the horizontal list doesn't have to be. In the Compose screen, I receive and use the state like so:
val state = viewModel.viewState.collectAsState().value
MyScreen(state)
...
#Composable
fun MyScreen(state: MyScreenState) {
val cards: LazyPagingItems<MyCard> = state.verticalCardsPaged.collectAsLazyPagingItems()
LazyRow {
items(state.horizontalObjects) {
...
}
}
LazyColumn {
items(cards) {
...
}
}
}
So I have a Flow inside Flow, effectively. It all seems to be working ok, but I'm not sure if I should be combining them instead of nesting? What would be the preferred approach here?
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.
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.
I currently have an app which uses a One Activity-Many Fragment approach, and within this app is a fragment which shares significant data with its children, and so I have used navGraphViewModels scoped to a nested nav graph as so:
private val viewModel by navGraphViewModels<MySharedViewModel>(R.id.nested_nav_graph)
The parent fragment contains a viewPager, and the fragments passed to that viewPager all share the same viewModel as the parent.
My issue with using this approach is that when it comes to UI testing involving navGraphViewModels using Espresso, I was getting the error "View XXX does not have a NavController set." I managed to fix this for the parent fragment with the below:
val navController = TestNavHostController(ApplicationProvider.getApplicationContext())
// This allows fragments to use by navGraphViewModels()
navController.setViewModelStore(ViewModelStore())
UiThreadStatement.runOnUiThread {
navController.setGraph(R.navigation.nested_nav_graph)
}
val scenario =
launchFragmentInContainer(themeResId = R.style.AppTheme) {
MyFragment().also { fragment ->
fragment.viewLifecycleOwnerLiveData.observeForever { viewLifecycleOwner ->
if (viewLifecycleOwner != null) {
Navigation.setViewNavController(fragment.requireView(), navController)
}
}
}
}
return navController
}
However, as the parent fragment then loads its child fragments into the viewPager and these also require a NavController, my tests don't proceed past the #Before block. Any help regarding how to set the navController to the child fragment would be appreciated.
It seems like recommended pattern for fields in viewmodel is:
val selected = MutableLiveData<Item>()
fun select(item: Item) {
selected.value = item
}
(btw, is it correct that the selected field isn't private?)
But what if I don't need to subscribe to the changes in the ViewModel's field. I just need passively pull that value in another fragment.
My project details:
one activity and a bunch of simple fragments replacing each other with the navigation component
ViewModel does the business logic and carries some values from one fragment to another
there is one ViewModel for the activity and the fragments, don't see the point to have more than one ViewModel, as it's the same business flow
I'd prefer to store a value in one fragment and access it in the next one which replaces the current one instead of pass it into a bundle and retrieve again and again manually in each fragment
ViewModel:
private var amount = 0
fun setAmount(value: Int) { amount = value}
fun getAmount() = amount
Fragment1:
bnd.button10.setOnClickListener { viewModel.setAmount(10) }
Fragment2:
if(viewModel.getAmount() < 20) { bnd.textView.text = "less than 20" }
Is this would be a valid approach? Or there is a better one? Or should I just use LiveData or Flow?
Maybe I should use SavedStateHandle? Is it injectable in ViewModel?
To answer your question,
No, It is not mandatory to use LiveData always inside ViewModel, it is just an observable pattern to inform the caller about updates in data.
If you have something which won't be changed frequently and can be accessed by its instance. You can completely ignore wrapping it inside LiveData.
And anyways ViewModel instance will be preserved and so are values inside it.
And regarding private field, MutableLiveData should never be exposed outside the class, as the data flow is always from VM -> View which is beauty of MVVM pattern
private val selected = MutableLiveData<Item>()
val selectedLiveData : LiveData<Item>
get() = selected
fun select(item: Item) {
selected.value = item
}