Shared ViewModel in scope of parent fragment using Navigation Component - android

I am trying to use the same instance of ViewModel in Parent Fragment and its children, using Navigation Component. The hierarchy is as follows: Single Activity that has navigationHost. This host has 3 child fragments, A, B and C. The last fragment has also navigationHost with 2 fragments: X and Y. The below graph illustrates the hierarchy.
Expected:
I would like to share the same instance of fragment C ViewModel with fragment X and Y.
Current:
The ViewModel of fragment C is initialized twice: Once when fragment C is initialized and second time when fragment X is initialized. The Fragment X is set as a default destination in the fragment C nav graph. When I am changing the default destination to Y, the ViewModel is initialized in C and Y.
What I tried already:
In child viewModels I use this:
val viewModel: ParentViewModel =
ViewModelProvider(findNavController().getViewModelStoreOwner(R.id.parent_graph)).get(
ParentViewModel::class.java
)
In parent viewModel I use this:
val viewModel by viewModels<ParentViewModel>()
I've also tried to inject the viewModel using Koin sharedViewModel with scope of fragment:
val viewModel by sharedViewModel<ParentViewModel>(from = { parentFragment!! })
Also no luck.
Is it possible or maybe it is a bug in navigation library?

A NavHostFragment is a fragment itself, so your structure is actually
Fragment C -> NavHostFragment -> Fragment X
-> Fragment Y
I.e., the parentFragment you get from Fragment X is not Fragment C - it is the NavHostFragment you added in between the two.
Therefore if you want to get a ViewModel from Fragment C, you'd need to use requireParentFragment().requireParentFragment() - the parent of your NavHostFragment is Fragment C.

Cannot find a parameter with this name: from
------------update----------------
for those facing the same issue, check here koin issue discuss about, and maybe here might be helpful.
I'm using
//child fragment
private val viewModel: TripParentViewModel by viewModel(owner = { ViewModelOwner.Companion.from(requireParentFragment().requireParentFragment().viewModelStore)})
//parent fragment
private val parentViewModel by viewModel<TripParentViewModel>()
as solution,
class TripParentViewModel:ViewModel() {
var count = 0
fun test(){
when(count){
0 -> Timber.d("first click")
1 -> Timber.d("second click")
2 -> Timber.d("third click")
}
Timber.d(count.toString())
count++
}
}
currently, I run this when change fragment, I didn't see any problem so far, if anything goes wrong, I will update here
koin_version = "2.2.1"
navigation_version = "2.3.5"

Related

one ViewModel per each composable page inside HorizontalPager

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

UI Testing Viewpager with NavGraphViewModel using Espresso

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.

LiveData is not getting observed for one specific scenario

I have 3 LiveData objects in my ViewModel, I'm applying transformations to these, the problem is 2 LiveData are getting observed while the other one is not, I've tried different solutions like changing the way ViewModel is initialized or the way LiveData is initialized but nothing has worked for me.
class MyClass : ViewModel() {
init {
_originallist.value = Instance.getOrignalList()
}
// observed in Fragment A
val a: LiveData<List<A>> = Transformations.map(_searchText, ::extractA)
// observed in Fragment B
val b: LiveData<List<B>> = Transformations.map(_originallist, ::extractB)
// observed in Fragment C
val c: LiveData<List<C>> = Transformations.map(_originalList, ::extractC)
// Called from Fragment D to change the _searchText liveData
fun setSearchText(text: String) {
_searchText.value = text
}
fun extractA(text: String): List<A> {
val temp = ArrayList<A>()
list.foreach {
if (it.contains(text, false) temp . add (it)
}
return temp
}
fun extractB(list: List<B>): List<B> {
// do something
}
fun extractC(list: List<C>): List<C> {
// do something
}
}
If you have noticed that the LiveData b and c are getting initialized just once hence I'm able to see the data in my RecyclerView, but for the LiveData A, the search text can change based on user input, this is where my fragment is not observing this live data.
Things to note: This is a common ViewModel for my 3 viewPager fragments, LiveData a is observed in one fragment, B in another and C in another.
Eventually, I have to apply the search for other 2 fragments as well.
When I was debugging the observer lines in my fragment was getting skipped, another thing I would like to point out is that the code in all 3 fragments is same except that they are observing different LiveData
EDIT: What i have noticed now is that, since i'm calling the setSearchText() from Fragment D i'm able to observe the changes of LiveData A in Fragment D but i want to observe that in Fragment A but not able to.
I have a search bar in fragment D and bottom of that i have a view pager with 3 fragments, all 4 fragments have a common viewModel, Am i doing something wrong here or is this not the way to implement this?
TIA
Finally found the root cause, the problem was that the viewModel was getting its own lifecycle owner in each of fragment, the solution to this was to declare and initialize the viewModel object in the parent activity of the fragments and use its instace in the fragment to observe the LiveData
The problem is:
Your function extractA in
val a: LiveData<List<A>> = Transformations.map(_searchText, ::extractA)
will only be executed when the value of _searchText will change.
That's how Transformations work, they apply the given function whenever the value changes.

AndroidX Test - ActivityScenario onView() can't find nested child fragment

I have an activity MyActivity which in turn holds a fragment MyFragment. In MyFragment I show a BottomSheetDialogFragment using the childFragmentManager as below:
bottomSheetFragment.show(childFragmentManager, "BottomSheetFragment")
The below is my test case (using AndroidX test / Robolectric) running in JVM.
#Test
fun isBottomSheetShownTest() {
val scenario = ActivityScenario.launch(MyActivity::class.java)
FakeViewModle.showBottomSheet() // Posts on a LiveData to show the bottom sheet
scenario.onActivity {
val fragment = it.supportFragmentManager.fragments[0]
val bottomSheet = fragment.childFragmentManager.findFragmentByTag("BottomSheetFragment")!!
val view = bottomSheet.view!!
val findViewById = view.findViewById<View>(R.id.bottomSheetTitle)
val visibility = findViewById.visibility // View is present and Visible
}
Espresso.onView(ViewMatchers.withId(R.id.bottomSheetTitle)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
// Throws androidx.test.espresso.NoMatchingViewException: No views in hierarchy found matching
}
The above test case fails with "androidx.test.espresso.NoMatchingViewException: No views in hierarchy found matching".
But within the scenario.onActivity {}, I am able to see the inflated BottomSheet fragment. On debugging, I found that the BottomSheet is fully inflated as is in Resumed state.
My Questions are:
What does Espresso.onView() hold? When I checked the View hierarchy, I see the MyActivity and MyFragment but not the BottomSheet.
Is it not possible to get the nested child fragment using Espresso.onView() ?
How do I assert if a View is present in my BottomSheet, by using Espresso.onView() instead of querying the scenario.onActivity ?

Fragments not added to backstack using Navigation Components

Information:
I'm programmatically inserting a NavHostFragment for each feature of the app. Each NavHostFragment has it's own Navigation Graph. Dagger is providing them by using a FragmentFactory specific to each feature. It's a Single Activity structure with MVVM architecture.
Repo: https://github.com/mitchtabian/DaggerMultiFeature/tree/nav-component-backstack-bug
checkout the branch "nav-component-backstack-bug".
The Problem
When navigating into the graph the fragments are not being added to the backstack. The only fragment that's added is whichever one has most recently been visited. So the stack size always stays at one.
Originally I thought it was because I wasn't setting the FragmentFactory to the ChildFragmentManager but that doesn't change anything. See the code snippets below for the relevant code. Or checkout the project and run it. I have logs printing out the fragments currently in the backstack from the ChildFragmentManager and also the SupportFragmentManager. Both have a constant size of 1.
Feature1NavHostFragment.kt
This is one of the custom NavHostFragment's. The create() function in the companion object is how I create them.
class Feature1NavHostFragment
#Inject
constructor(
private val feature1FragmentFactory: Feature1FragmentFactory
): NavHostFragment(){
override fun onAttach(context: Context) {
((activity?.application) as BaseApplication)
.getAppComponent()
.feature1Component()
.create()
.inject(this)
childFragmentManager.fragmentFactory = feature1FragmentFactory
super.onAttach(context)
}
companion object{
const val KEY_GRAPH_ID = "android-support-nav:fragment:graphId"
#JvmStatic
fun create(
feature1FragmentFactory: Feature1FragmentFactory,
#NavigationRes graphId: Int = 0
): Feature1NavHostFragment{
var bundle: Bundle? = null
if(graphId != 0){
bundle = Bundle()
bundle.putInt(KEY_GRAPH_ID, graphId)
}
val result = Feature1NavHostFragment(feature1FragmentFactory)
if(bundle != null){
result.arguments = bundle
}
return result
}
}
}
MainActivity.kt
This is an example of how I create the NavHostFragment's in MainActivity.
val newNavHostFragment = Feature1NavHostFragment.create(
getFeature1FragmentFactory(),
graphId
)
supportFragmentManager.beginTransaction()
.replace(
R.id.main_nav_host_container,
newNavHostFragment,
getString(R.string.NavHostFragmentTag)
)
.setPrimaryNavigationFragment(newNavHostFragment)
.commit()
Feature1MainFragment.kt
And here is an example of how I'm navigating to other fragments in the graph.
btn_go_next.setOnClickListener {
findNavController().navigate(R.id.action_feature1MainFragment_to_feature1NextFragment)
}
Summary
Like I said, in every fragment I'm printing the backstack for both the ChildFragmentManager and the SupportFragmentManager. Both have a constant size of one. It's as if the fragments are being replaced as I navigate into the graph instead of added to the stack.
Anyways, thanks for reading this and I would appreciate any insights.
Looks like a misunderstanding on my part (and a bug, also on my part).
If you loop through the fragments in the childFragmentManager it only ever shows the top-most fragment for some reason.
Example
val navHostFragment = supportFragmentManager
.findFragmentByTag(getString(R.string.NavHostFragmentTag)) as NavHostFragment
val fragments = navHostFragment.childFragmentManager.fragments
for(fragment in fragments){
// Only prints a single fragment, no matter the backstack size
}
However, if you print the backstack size like this, you will get the correct answer.
val navHostFragment = supportFragmentManager
.findFragmentByTag(getString(R.string.NavHostFragmentTag)) as NavHostFragment
val backstackCount = navHostFragment.childFragmentManager.backStackEntryCount
println("backstack count: $backstackCount")
At the end of the day this misunderstanding caused me to believe the fragments were not being added to the backstack. All is good.

Categories

Resources