I have a parent Fragment that has ViewPager which holds another Fragments(child). Some child Fragment can have list. If the user press a back button the list will scroll up if can scroll vertically, another press on back button will move the ViewPager to first item (Fragment).
While I can create an approach like this on child Fragment.
if (adapter.currentList.isNotEmpty() && recyclerView.canScrollVertically(-1))
recyclerView.smoothScrollToPosition(0)
else {
try {
val parentFragment: HomeFragment = parentFragment as HomeFragment
parentFragment.onBackPressed()
} catch (e: Exception) {
FirebaseCrashlytics.getInstance().recordException(e)
}
}
And a public method on parent Fragment like this
fun onBackPressed() {
if (viewPager.currentItem != 0)
viewPager.setCurrentItem(0, true)
else
requireActivity().onBackPressed()
}
I am not sure if this is the best thing to do since I read that Fragments should better not communicate directly with each other, instead communication should be handle by the host Activity or shared ViewModel. But doing so seems an overkill and I do not feel the use of LiveData just for this case.
I would go with the SharedViewModel approach, its kinda built for this. Your child Fragments are effecting the ViewPager that belongs to the ParentFragment. They are effectively part of the same View but the child Fragments don't have access to the View that contains them. So using a SharedViewModel can bridge the gap.
What I would do is make a well defined interface that manipulates the ViewPager and its Adapter as you see fit for your application. That interface should then be accessible through the SharedViewModel.
In the interface you could have a function called navigateToFragment(Class fragmentClass); In this function you would then need to find a fragment via its class inside the Adapter, and then use the ViewPager to go to it. Ie navigateToFragment(HomeFragment.class);
Related
I have parent Fragment that contains child fragment. Inside child fragment I have ViewPager with fragments. My question is how can I share ViewModel between parent child and fragments in viewpager and makeing Viewmodel visible only on ParentFragment scope?
what do you mean when you say "visible only on ParentFragment scope"?
According Google's document, there is one way that you can share ViewModel.
Check this document: https://developer.android.com/topic/libraries/architecture/viewmodel#sharing
Shortly, your parent fragment and child fragment will use the same ViewModel. Your parent fragment will call the function of ViewModel to change the data, your child fragment just observer the LiveData of ViewModel.
You can place your sharedViewModel inside your Activity. Then you can access it from every fragment which in attached to this activity with this code:
(requireActivity() as MainActivity).viewModel;
With this approach, you can set data from one fragment and observe data from another fragment. So, you enable communication between two fragments.
Setting data:
viewModel.liveDataObject.value = value
Observing data:
viewModel.liveDataObject.observe(viewLifecycleOwner) {}
I am using Navigation component in my App, using google Advanced Sample(here).
my problem is when going back to a fragment, the scrolling position does not lost but it rearranges items and moves highest visible items so that top of those item align to top of recyclerview. please see this:
before going to next fragment:
and after back to fragment:
this problem is matter because some times clicked item goes down and not seen until scroll down.
how to prevent this behavior?
please consider:
this problem exist if using navigation component to change fragment. if start fragment using supportFragmentManager.beginTransaction() or start another activity and then go to this fragment it is OK. but if I navigate to another fragment using navigation component this problem is exist.(maybe because of recreating fragment)
also this problem exist if using fragment in ViewPager. i.e recyclerView is in a fragment that handle with ViewPagerAdapter and viewPager is in HomeFragment that opened with Navigation component. if recyclerView is in HomeFragment there is no problem.
no problem with LinearLayoutManager. only with StaggeredGridLayoutManager.
there is not difference if using ViewPager2 and also FragmentStatePagerAdapter
I try to prevent recreate of fragment(by this solution) but not solved.
UPDATE:
you can clone project with this problem from here
When using Navigation Component + ViewPager + StaggeredGridLayoutManager, wrong recyclerView.computeVerticalScrollOffset() has been returned during Fragment recreate.
In general, all layout managers bundled in the support library already know how to save and restore scroll position, but in this case, we had to take responsibility for this.
class TestFragment : Fragment(R.layout.fragment_test) {
private val testListAdapter: TestListAdapter by lazy {
TestListAdapter()
}
private var layoutManagerState: Parcelable? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
postListView.apply {
layoutManager = StaggeredGridLayoutManager(
2, StaggeredGridLayoutManager.VERTICAL
).apply {
gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
}
setHasFixedSize(true)
adapter = testListAdapter
}
testListAdapter.stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT
}
override fun onPause() {
saveLayoutManagerState()
super.onPause()
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
restoreLayoutManagerState()
}
private fun restoreLayoutManagerState () {
layoutManagerState?.let { postListView.layoutManager?.onRestoreInstanceState(it) }
}
private fun saveLayoutManagerState () {
layoutManagerState = postListView.layoutManager?.onSaveInstanceState()
}
}
Source code: https://github.com/dautovicharis/MyStaggeredListSample/tree/q_65539771
The Navigation Component behavior is normal when you navigate from one fragment to another. I mean, onDestroyView() method from the previous fragment is executed, so it means that your view is destroyed, but not the fragment. Remembering that fragment has two lifecycles one for the fragment and another one for the view, There was a video about it.
Also, there were issues registered in issue tracker in order to avoid this behavior in some cases and the GitHub issues:
https://issuetracker.google.com/issues/127932815
https://github.com/android/architecture-components-samples/issues/530
The problem is that when you have fragment that is heavy to recreate, is easier to do not destroy it and just add one fragment. So, when you go back it is not recreated. But, for this behavior is not part of navigation component.
Solutions
The easiest solution is to not use navigation component and work with the tradicional way, as you can see this works perfectly in you use case.
You can use the traditional way just for this use case, and use the navigation component for other cases.
You can inflate this view in an activity. So you are adding un activity
But if the previous tree options is not possible. You can try the following:
If you are using viewModel, you can use SaveState. Basically, it can save the data from your fragment, it is like a map data structure, so you can save positions from your list or recycler view. When go back to this fragment, get the position from this saveState object and use scrollToPosition method in order to add the real position.
Recycler view have methods for restore positions. You can see the uses cases for that, because first you need the data and then add the real position, for more details you can visit this link. This configuration for recycler view is useful also when you lose memory and you need to recreate the recycler view with asynchronous data.
Finally, if you want to understand more about how fragment works with navigation component, you can see this link
While Navigation component of JetPack looks pretty promising I got to a place where I could not find a way to implement something I wanted.
Let's take a look at a sample app screen:
The app has one main activity, a top toolbar, a bottom toolbar with fab attached.
There are 2 challenges that I am facing and I want to make them the right way.
1. I need to implement fragment transactions in order to allow replacing the fragment on the screen, based on the user interaction.
There are three ways I can think of and have this implemented:
the callbacks way. Having a interface onFragmentAction callback in fragment and have activity implement it. So basically when user presses a button in FragmentA I can call onFragmentAction with params so the activity will trigger and start for example transaction to replace it with FragmentB
implement Navigation component from JetPack. While I've tried it and seems pretty straightforward, I had a problem by not being able to retrieve the current fragment.
Use a shared ViewModel between fragment and activity, update it from the fragment and observe it in the activity. This would be a "replacement" of the callbacks
2. Since the FAB is in the parent activity, when pressed, I need to be able to interact with the current visible fragment and do an action. For instance, add a new item in a recyclerview inside the fragment. So basically a way to communicate between the activity and fragment
There are two ways I can think of how to make this
If not using Navigation then I can use findFragmentById and retrieve the current fragment and run a public method to trigger the action.
Using a shared 'ViewMode' between fragment and activity, update it from activity and observe it in the fragment.
So, as you can see, the recommended way to do navigation would be to use the new 'Navigation' architecture component, however, at the moment it lacks a way to retrieve the current fragment instance so I don't know how to communicate between the activity and fragment.
This could be achieved with shared ViewModel but here I have a missing piece: I understand that fragment to fragment communication can be made with a shared ViewModel. I think that this makes sense when the fragments have something in common for this, like a Master/Detail scenarion and sharing the same viewmodel is very useful.
But, then talking between activity and ALL fragments, how could a shared ViewModel be used? Each fragment needs its own complex ViewModel. Could it be a GeneralViewModel which gets instantiated in the activity and in all fragments, together with the regular fragment viewmodel, so have 2 viewmodels in each fragment.
Being able to talk between fragments and activity with a viewmodel will make the finding of active fragment unneeded as the viewmodel will provide the needed mechanism and also would allow to use Navigation component.
Any information is gladly received.
Later edit. Here is some sample code based on the comment bellow. Is this a solution for my question? Can this handle both changes between fragments and parent activity and it's on the recommended side.
private class GlobalViewModel ():ViewModel(){
var eventFromActivity:MutableLiveData<Event>
var eventFromFragment:MutableLiveData<Event>
fun setEventFromActivity(event:Event){
eventFromActivity.value = event
}
fun setEventFromFragment(event:Event){
eventFromFragment.value = event
}
}
Then in my activity
class HomeActivity: AppCompatActivity(){
onCreate{
viewModel = ViewModelProviders.of(this, factory)
.get(GlobalViewModel::class.java)
viewModel.eventsFromFragment.observe(){
//based on the Event values, could update toolbar title, could start
// new fragment, could show a dialog or snackbar
....
}
//when need to update the fragment do
viewModel.setEventFromActivity(event)
}
}
Then in all fragments have something like this
class FragmentA:Fragment(){
onViewCreated(){
viewModel = ViewModelProviders.of(this, factory)
.get(GlobalViewModel::class.java)
viewModel.eventsFromActivity.observe(){
// based on Event value, trigger a fun from the fragment
....
}
viewModelFragment = ViewModelProviders.of(this, factory)
.get(FragmentAViewModel::class.java)
viewModelFragment.some.observe(){
....
}
//when need to update the activity do
viewModel.setEventFromFragment(event)
}
}
Hello i create class that implement BottomSheetDialogFragment with dynamic content. The content is a Fragment. So when initialize the BottomSheet i passing fragment object, and attach it to specific Container ID inside this BottomSheetDialogFragment. Looks like this :
private fun attachContentFragment() {
val transaction = childFragmentManager.beginTransaction()
transaction.apply {
replace(R.id.flContent, state.layoutContent)
commit()
}
}
state.layoutContent is my attached Fragment
I need to dismiss the BottomSheet if every action called in that fragment.
As far as i know, i need to get the object of BottomSheet that hold me(Fragment) and dismiss it.
But how i can get that BottomSheet object?
Thanks
So, technically it is a fragment inside fragment situation. I think there is several solutions here:
Call Activity from your child fragment. BottomSheetDialogFragment will subscribe to Activity for such events and react on them.
Get the instance of a BottomSheetDialogFragment by calling proper FragmentManager (which possible is an Activity one). You can get an instance of a fragment byTag for example.
Or you can call getParentFragment from a child Fragment.
In my parent Fragment's onCreateView method, I invoke the Child Fragment Manager to insert a Fragment into the layout. This child fragment contains a few FloatingActionButtons.
After I commit the transaction, I then check the contents of my underlying list (powering a RecyclerView), and based on whether any values are present, I either hide/unhide some of the FloatingActionButtons.
However it's telling me that the buttons are null! Does committing a Fragment transaction not call all of its typical lifecycle events first? Is there a way to force it to wait? Is there a better practice for this?
The commit is not synchronous. There's no guarantee when the fragment's life cycle will execute.
If you need to know when the fragment is "ready", implement a callback from the fragment to the hosting activity letting it know.
class CustomB extends CustomA {
interface Listener {
void onViewCreated();
}
public View onCreateView() {
View v = super.onCreateView();
if (getActivity() instanceof Listener) {
((Listener)getActivity()).onViewCreated();
}
return v;
}
}
Generally speaking you should try to design your fragments to be self-contained. If the fragment is tightly bound to the activity, consider implementing the functionality as a custom View instead.