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 ?
Related
I have created one chatsheetfragment(Bottomsheetdialogfragment). Whenever I open it I'm calling all the chats and binding in RecyclerView. So, my problem is the chat sheet is always reloading from onCreate() which eventually results refreshing the fragment every time. how to stop it.
And I'm using viewmodel using dagger-hilt . Viewmodel instance is also creating every time.
Tried opening as singleton instance but not worked and now I'm opening like below
private fun chatButton() {
binding.chatIv.setOnClickListener {
ChatSheetFragment().show(
supportFragmentManager,
ChatSheetFragment::class.java.simpleName
)
}
}
With ChatSheetFragment(), a brand new fragment is getting created and therefore a brand new ViewModel in case that you bind this ViewModel to that fragment.
This can be solved by binding that ChatSheetFragment to the parent activity/fragment ViewModel that can host the updated list.
So, in short:
Change the ViewModel in the ChatSheetFragment to either the parent fragment/activity (according to your desgin):
i.e., instead of ViewModelProvider(this)[MyChatViewModel::class.java] you'd replace this with requireParentFragment() or requireActivity() and replace MyChatViewModel with the one of the parent fragment/activity.
Move the list logic that you want to maintain from the chat fragment ViewModel to the parent ViewModel.
Another solution is not to create a brand new fragment with ChatSheetFragment() and just show the existing one; but not sure if that can affect the performance to keep it alive while you don't need it.
Edit:
problem to me is bottomsheetfragment is detaching and destroying itself whenever it dismiss. what can i do so that it can not be destroyed
This is right; calling dismiss() or even setting the BottomSheetBehavior state to STATE_HIDDEN will destroy the fragment.
But there is a workaround to just hide the decorView of the dialogFragment window whenever you want to hide the chat fragment like the following:
val chatDialogFragment = ChatSheetFragment()
// Hide the bottom sheet dialog fragment
chatDialogFragment.dialog.hide(); // equivalent to dialog.window.decorView.visiblity = View.GONE
// Show the bottom sheet dialog fragment
chatDialogFragment.dialog.show // equivalent to dialog.window.decorView.visiblity = View.VISIBLE
But you need to handle the situations when the DialogFragment can hide; here is a couple ones:
Dismiss on back press
Customize the dialog in onCreateDialog():
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return object : BottomSheetDialog(requireContext(), theme) {
override fun onBackPressed() {
this#BottomSheetFragment.dialog?.hide()
}
}
}
Dismiss on touch outside:
#SuppressLint("ClickableViewAccessibility")
override fun onStart() {
super.onStart()
val outsideView =
requireDialog().findViewById<View>(com.google.android.material.R.id.touch_outside)
isCancelable = false
dialog?.setCanceledOnTouchOutside(false)
outsideView.setOnTouchListener { _: View?, event: MotionEvent ->
if (event.action == MotionEvent.ACTION_UP) dialog?.hide()
false
}
Whenever you want to show the fragment again; just show its dialog without re-instantiating it as described above
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
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.
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.
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"