How to navigate back from fragments inside NavHostFragment of ViewPager2? - android

What is the proper way of navigating back from nested fragments of ViewPager2?
Despite using app:defaultNavHost="true"with FragmentContainerView pressing back button while in a nested fragment of a page calls Activity's back press instead of navigating back to previous fragment.

As per the Create a NavHostFragment documentation, app:defaultNavHost="true" calls setPrimaryNavigationFragment() when the Fragment is first added - it is setPrimaryNavigationFragment() that routes back button press events to that fragment automatically.
In a ViewPager2 though, it is the ViewPager2 that is responsible for creating and adding the Fragment. Since every level of the Fragment hierarchy needs to be the primary navigation fragment, adding a child fragment via XML still doesn't solve the missing link: that the Fragment in the ViewPager2 needs to be the primary navigation fragment.
Therefore, you need to hook into the callbacks for when a Fragment is made the active Fragment and call setPrimaryNavigationFragment(). ViewPager2 1.1.0-alpha01 adds exactly this API in the FragmentTransactionCallback, specifically, the onFragmentMaxLifecyclePreUpdated(), which is called whenever the Lifecycle state of a Fragment is changed: when it is changed to RESUMED, that Fragment is now the active fragment and should become the primary navigation Fragment as part of the onPost callback.
private class Adapter(parentFragment: Fragment) : FragmentStateAdapter(parentFragment) {
init {
// Add a FragmentTransactionCallback to handle changing
// the primary navigation fragment
registerFragmentTransactionCallback(object : FragmentTransactionCallback() {
override fun onFragmentMaxLifecyclePreUpdated(
fragment: Fragment,
maxLifecycleState: Lifecycle.State
) = if (maxLifecycleState == Lifecycle.State.RESUMED) {
// This fragment is becoming the active Fragment - set it to
// the primary navigation fragment in the OnPostEventListener
OnPostEventListener {
fragment.parentFragmentManager.commitNow {
setPrimaryNavigationFragment(fragment)
}
}
} else {
super.onFragmentMaxLifecyclePreUpdated(fragment, maxLifecycleState)
}
})
}
// The rest of your FragmentStateAdapter...
}

You need to override your parent activity's onBackPressed logic, you need to use https://developer.android.com/reference/androidx/navigation/NavController#popBackStack() to navigate up in your nav graph of nested fragment.

Here is the Java version of #ianhanniballake answer (yes im a luddite for not using kotlin yet - i need to know java inside out first before i learn anything else).
I havent unit tested this yet, but it "works"...
public class ViewPagerAdapter extends FragmentStateAdapter {
public ViewPagerAdapter(#NonNull FragmentManager fragmentManager,
#NonNull Lifecycle lifecycle) {
super(fragmentManager, lifecycle);
registerFragmentTransactionCallback(new FragmentTransactionCallback() {
#NonNull
#Override
public OnPostEventListener onFragmentMaxLifecyclePreUpdated(#NonNull Fragment fragmentArg,
#NonNull Lifecycle.State maxLifecycleState) {
if (maxLifecycleState == Lifecycle.State.RESUMED) {
return () -> fragmentArg.getParentFragmentManager().beginTransaction()
.setPrimaryNavigationFragment(fragmentArg).commitNow();
} else {
return super.onFragmentMaxLifecyclePreUpdated(fragmentArg, maxLifecycleState);
}
}
});
}
// remainder of your FragmentStateAdapter here
}

Related

Different FAB click listener for each different fragment on the Navigation

At current implementation of my app I am using the Navigation component. In my main content I have a floating action button for all. And at the same time I have 7 different fragment which are automatically handled by the navigation controller when transition is needed.
mAppBarConfiguration =
new AppBarConfiguration.Builder( navigationDrawerFragmentIds ).setDrawerLayout( drawer ).build();
NavController navController = Navigation.findNavController( this, R.id.nav_host_fragment );
Till today I used the setUserVisibleHint(boolean isVisibleToUser) method to override different behaviour(on click listener) for my single fab in the each different fragment.
Like below
// FIXME: 2.11.2019 Fix deprecated methods.
#Override public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint( isVisibleToUser );
if (isVisibleToUser && isResumed()) {
//Only manually call onResume if fragment is already visible
//Otherwise allow natural fragment lifecycle to call onResume
onResume();
} else {
// current fragment not visible
floatingActionButton.setOnClickListener( null );
}
}
#Override public void onResume() {
super.onResume();
// FIXME: 2.11.2019 Fix deprecated methods.
if (!getUserVisibleHint()) {
return;
}
// Set listener for float action button which has been defined in main activity.
// Here we will override the listener which can be work for our current fragment.
floatingActionButton = getActivity().findViewById( R.id.fab );
floatingActionButton.setOnClickListener( new View.OnClickListener() {
#Override public void onClick(View view) {
// do some stuff here.
}
} );
}
Since setUserVisibleHint is deprecated anymore, I wanted to replace new behaviour instead this method like suggested in the release note.
Max Lifecycle: You can now set a max Lifecycle state for a Fragment by
calling setMaxLifecycle() on a FragmentTransaction. This replaces the
now deprecated setUserVisibleHint(). FragmentPagerAdapter and
FragmentStatePagerAdapter have a new constructor that allows you to
switch to the new behavior.
And also navigation controller has default FragmentNavigator as I understood but I could not understand is this help to me to set the fragments life cycle or behaviour like this post. If I can set the behaviour like mentioned post then I can basically use the onResume and onStart (I assume that will work like this because when I select the BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT then the next fragment will not be initiated. only onCreateView called I assume.)
As I explained above that I am using navigation component to handle transition between my fragments. That is why I do not have any special view pager. But I can still get the information via below code the current destination and callback will handle whenever transition is occured between fragments.
navController.addOnDestinationChangedListener( new NavController.OnDestinationChangedListener() {
#Override
public void onDestinationChanged(#NonNull NavController controller, #NonNull NavDestination destination,
#Nullable Bundle arguments) {
Log.i( TAG, "onDestinationChanged: " + destination.getLabel());
}
} );
Then on my main activity I can set the different behaviour for my fab for each fragment. Up to now this is okey for me. But I wanted to learn is there any way that I can set the max life cycle of the fragment while I am using the Navigation component ?
Thanks in advance.

Loading Fragment UI on-demand

Problem:
I am currently running into a problem where my app is trying to load too many fragments when it opens for the first time.
I have BottomNavigationView with ViewPager that loads 4 fragments - each one of the Fragment contains TabLayout with ViewPager to load at least 2 more fragments.
As you can imagine, that is a lot of UI rendering (10+ fragments) - especially when some of these fragments contain heavy components such as calendar, bar graphs, etc.
Currently proposed solution:
Control the UI loading when the fragment is required - so until the user goes to that fragment for the first time, there is no reason to load it.
It seems like it's definitely possible as many apps, including the Play Store, are doing it. Please see the example here
In the video example above - the UI component(s) are being loaded AFTER the navigation to the tab is completed. It even has an embedded loading symbol.
1) I am trying to figure out how to do exactly that - at what point would I know that this fragment UI need to be created vs it already is created?
2) Also, what is the fragment lifecycle callback where I would start the UI create process? onResume() means UI is visible to the user so loading the UI there will be laggy and delayed.
Hope this is clear enough.
EDIT:
I'm already using the FragmentStatePagerAdapter as ViewPager adapter. I noticed that the super(fm) method in the constructor is deprecated now:
ViewPagerAdapter(FragmentManager fm) {
super(fm); // this is deprecated
}
So I changed that to:
ViewPagerAdapter(FragmentManager fm) {
super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
}
BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT: Indicates that only the current fragment will be in the Lifecycle.State.RESUMED state. All other Fragments are capped at Lifecycle.State.STARTED.
This seems useful as the onResume() of the Fragment will only be called when the Fragment is visible to the user. Can I use this indication somehow to load the UI then?
The reason your app loads multiple Fragments at the startup is most probably, you're initializing them all at once. Instead, you can initialize them when you need them. Then use show\ hide to attach\ detach from window without re-inflating whole layout.
Simple explanation: You'll create your Fragment once user clicks on BottomNavigationView's item. On clicked item, you'll check if Fragment is not created and not added, then create it and add. If it's already created then use show() method to show already available Fragment and use hide() to hide all other fragments of BottomNavigationView.
As per your case show()/hide is better than add()/replace because as you said you don't want to re-inflate the Fragment when you want show them
public class MainActivity extends AppCompatActivity{
FragmentOne frg1;
FragmentTwo frg2;
#Override
public boolean onNavigationItemSelected(MenuItem item){
switch(item.getId()){
case R.id.fragment_one:
if (frg2 != null && frg2.isAdded(){
fragmentManager.beginTransaction().hide(frg2).commit();
}
if(frg1 != null && !frg1.isAdded){
frg1 = new FragmenOne();
fragmentManager.beginTransaction().add(R.id.container, frg1).commit();
}else if (frg1 != null && frg1.isAdded) {
fragmentManager.beginTransaction().show(frg1).commit();
}
return true;
case R.id.fragment_two:
// Reverse of what you did for FragmentOne
return true;
}
}
}
And for your ViewPager as you can see from the example you're referring to; PlayStore is using setOffscreenPageLimit. This will let you choose how many Views should be kept alive, otherwise will be destroyed and created from start passing through all lifecycle events of the Fragment (in case view is Fragment). In PlayStore app's case that's probably 4-5 that why it started loading again when you re-selected "editor's choice" tab. If you do the following only selected and neighboring (one in the right) Fragments will be alive other Fragments outside screen will be destroyed.
public class FragmentOne extends Fragment{
ViewPager viewPager;
#Override
public void onCreateView(){
viewPager = .... // Initialize
viewpAger.setOffscreenPageLimit(1); // This will keep only 2 Fragments "alive"
}
}
Answer to both questions
If you use show/hide you won't need to know when to inflate your view. It will be handled automatically and won't be laggy since it's just attaching/detaching views not inflating.
It depends upon how you initialize your fragment in your activity. May be you are initializing all your fragment in onCreate method of your activity instead of that you can initialize it when BottomNavigation item is selected like below :
Fragment one,two,three,four;
#Override
public boolean onNavigationItemSelected(MenuItem item){
Fragment fragment;
switch(item.getId()){
case R.id.menu_one:{
if(one==null)
one = Fragment()
fragment = one;
break;
}
case R.id.menu_two:{
if(two==null)
two = Fragment()
fragment = two;
break;
}
}
getFragmentManager().beginTransaction().replace(fragment).commit();
}
To decide how many page is load in you view pager at one time you can use :
setOffscreenPageLimit.
viewPager.setOffscreenPageLimit(number)
To get the resume and pause functionality on fragments you can take an example from this link.
Please try this.
i was worked with the same kind of the Application, There were multiple tabs and also Tabs have multiple inner tabs.
i was used the concept of ViewPager method, In which there is one method of onPageSelected() for that method we were getting the page position.
By the Use of this position we are checking the current Fragment and called their custom method that we created inside that fragment like onPageSelected() defined inside that fragment.
With this custom method onPageSelected() inside the Fragment we checked that weather the list are available or not if list have data then we are not making the call of Api otherwise we are calling the Api and loading that list.
I think you have same kind of requirement to follow if your Tabs have inner Tab or viewpager you can follow same concept inside of that so if your current fragment of viewpager method onpageSelected called at that time your viewpager fragment initialized.
you have to call just initialization like data binding or view initialization need to be called in onCreate() method and other list attachment and api call to be managed by the custom method onPageSelected that will be called based on ViewPager onPageSelected.
let me Know if you need any help for same.
You can try to have Fragments with FrameLayouts only in ViewPager. The actual Fragments could be added to FrameLayout in onResume() (after checking if this Fragment isn't already attached). It should work if BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT works as expected.
I would recommend you use BottomNavigationView.setOnNavigationItemSelectedListener to toggle between the fragment UI whenever it is needed.
navigationView.setOnNavigationItemSelectedListener(item -> {
switch(item.getItemId()) {
case R.id.item1:
// you can replace the code findFragmentById() with findFragmentByTag("dashboard");
// if you only have one framelayout to hold the fragment
fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
if (fragment == null) {
fragment = new ExampleFragment();
// if the fragment is identified by tag, add another
// argument to this method:
// replace(R.id.fragment_container, fragment, "dashboard")
getSupportFragmentManager().begintransaction()
.replace(R.id.fragment_container, fragment)
.commit();
}
break;
}
}
The idea is simple, when the user swipes or selects a different tab, the fragment that was visible is replaced by the new fragment.
Just load fragments one by one. Create the main fragment layout with many placeholders and stubs and then just load them in the order you like.
Use FragmentTransaction.replace() from the main fragment after it loads.
Have you tried the setUserVisibleHint() method of a fragment
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
if(isVisibleToUser){
// Do you stuff here
}
}
This will only get called when a fragment is visible to the user
How about you maintain just one ViewPager? Sounds crazy? In that case, you just change the dataset of PagerAdapter when you switch between the bottom tabs. Let's see how you can accomplish this,
As you mentioned, you have 4 fragments, which are assigned to each individual tabs of the bottom navigation view. Each performs some redundant work i.e. holding a viewPager with tab layout and setting the same kind of adapters. So, if we can combine these 4 redundant tasks into one then we will be able to get rid of 4 fragments. And as there will be just one viewPager with one single adapter then we will be able to reduce the fragment loading count from ~10 to 2 if we set offScreenPageLimit to 1. Let's see some example,
activity.xml should look like
<LinearLayout>
<TabLayout />
<ViewPager />
<BottomNavigationView />
</LinearLayout>
It's optional but I would recommend to create a base PagerFragment abstract class with abstract method getTabTitle()
public abstract class PagerFragment extends Fragment {
public abstract String getTabTitle();
}
Now it's time to make our PagerAdapter class
public class SectionsPagerAdapter extends FragmentStatePagerAdapter {
public Map<Integer, List<PagerFragment>> map = ...; // If you are concerned about memory then I could recommend to store DataObject instead of PagerFragment and instantiate fragment on demand using that data.
public int currentTabId = R.id.first_bottom_tab_id;
private List<PagerFragment> getCurrentFragments() {
return map.get(currentTabId);
}
public void setCurrentTabId(int tabId) {
this.currentTabId = tabId;
}
public SectionsPagerAdapter(FragmentManager manager) {
super(manager);
}
#Override
public Fragment getItem(int position) {
return getCurrentFragments().get(position);
}
#Override
public int getCount() {
return getCurrentFragments().size();
}
#Override
public int getItemPosition(#NonNull Object object) {
return POSITION_NONE;
}
#Override
public CharSequence getPageTitle(int position) {
return getCurrentFragments().get(position).getTabTitle();
}
}
And finally, in Activity
SectionsPagerAdapter pagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
viewPager.setAdapter(pagerAdapter);
viewPager.setOffscreenPageLimit(1);
viewPagerTab.setViewPager(viewPager);
bottomNavigationView.setOnNavigationItemSelectedListener(menuItem -> {
pagerAdapter.setCurrentTabId(menuItem.getItemId())
pagerAdapter.notifyDataSetChanged();
viewPagerTab.setViewPager(viewPager);
}
This is the basic idea. You can mix some of your own ideas with it to make a wonderful result. Let me know if it is useful?
UPDATE
Answer to your questions,
I think with my solution you can achieve exactly the same behavior of the video as I already did it in a project. In my solution, if you set offset page limit to 1 then only adjacent fragment's is created in advance. So, fragment creation will be handled by adapter and viewpager you don't need to worry about it.
In my above solution, you should create UI in onCreateView().

How can I save fragment state when the user navigates between fragments in a viewpager?

I'm making an Android App, that uses BottomNavigationViewEx to have a Bottom Navigation widget with 5 sections/fragments, I manage them using a viewpager, but one of this fragment (fragment #3) also uses a tab layout to nest another 2 fragments and I need to keep the selected tab when the user navigates to other fragment using the BottomNavigation icons.
The problem is that I need save the state of the fragment #3 (juts to keep it simple, I call them in this post fragment #), that is the fragment that holds the tablayout.
Inside fragment #3 I'm calling:
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt("currentDirectoryFragmentId",tabLayout!!.selectedTabPosition)
}
but the method is never being called, and makes sense, because I really never destroy the parent activity, but onDestroy() is being called inside each fragment correctly.
So, How can I save the state of a fragment when the user navigates between fragments that are children of a same activity?
As stated in the comments. You can accomplish this by using a variable inside the parent activity and referring to and setting this variable inside the fragments' onPause() & onResume() methods.
Inside Parent Activity
public static int position = -1;
Inside Child Fragment
#Override
public void onPause() {
super.onPause();
MainActivity.position = viewPager.getCurrentItem();
}
#Override
public void onResume() {
viewPager.setCurrentItem(MainActivity.position);
super.onResume();
}

What is the right way to navigate between fragments in BottomNavigationView?

Problem in short:
I have an MainActivity that holds BottomNavigationView and FrameLayout on top of it. BottomNavigationView has 5 tabs and when tab is clicked, I add some fragment on that FrameLayout. But, from some fragment, I need to open another fragment. From that another fragment, I need to open the other one. Every time when I need to show fragment, I notify MainActivity from fragment, that it needs to add the another one. Every fragment checks does its activity implement interface. And it is annoying. So, if I have 100 fragments, MainActivity implements too many interfaces. It leads to boilerplate code. So, how to properly navigate between fragments if you have a lot?
Problem in detail:
Please, read problem in short section first.
As I've said I have BottomNavigationView that has 5 tabs. Let's call the fragments that responsible for each tab as FragmentA, FragmentB, FragmentC, FragmentD, FragmentE. I really know, how to show these fragments when tab is clicked. I just replace/add these fragments in activity. But, wait, what if you wanna go from FragmentA to FragmentF? After that from FragmentF to FragmentG? This is how I handle this problem: from FragmentF or FragmentG I notify MainActivity that I wanna change the fragment. But how they communicate with MainActivity? For this, I have interfaces inside of each fragment. MainActivity implements those interfaces. And here is problem. MainActivity implements too many interfaces that leads to boilerplate code. So, what is the best way to navigate through Fragments? I don't even touch that I also need to handle back button presses :)
Here is how my code looks like:
MainActivity implementing interfaces to change fragments if necessary:
class MainActivity : AppCompatActivity(), DashboardFragment.OnFragmentInteractionListener,
PaymentFragment.BigCategoryChosenListener, PaymentSubcategoryFragment.ItemClickedListener, PayServiceFragment.OnPayServiceListener, ContactListFragment.ContactTapListener, P2PFragment.P2PNotifier
Here is my PaymentFragment's onAttach method for example:
#Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof BigCategoryChosenListener) {
listener = (BigCategoryChosenListener) context;
} else {
throw new RuntimeException(context.toString()
+ " must implement BigCategoryChosenListener");
}
}
And using this listener I notify activity to change fragment. And in EACH fragment I should do so. I don't think that it is best practice. So, is it ok or there is a better way?
Ok What you need is something like this in activity where you would initialized on your BottomNavigationView.
bottomNavigationView.setOnNavigationItemSelectedListener(
new BottomNavigationView.OnNavigationItemSelectedListener() {
#Override
public boolean onNavigationItemSelected(#NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_1://Handle menu click -
//Call Navigator helper to replace Fragment to Fragment A
break;
case R.id.menu_2:
//Call Navigator helper to replace Fragment to Fragment B
break;
case R.id.menu_3:
//Call Navigator helper to replace Fragment to Fragment C
break;
}
return true;
}
});

How to tell when fragment is not visible in a NavigationDrawer

I am trying to tell when a user selects a different fragment in my navigation drawer. I was trying to use
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
}
How i switch fragments in my MainActivity:
override fun onNavigationItemSelected(item: MenuItem): Boolean {
// Handle navigation view item clicks here.
when (item.itemId) {
R.id.nav_camera -> {
// Handle the camera action
val fragment: HomeFragment = HomeFragment()
supportFragmentManager.beginTransaction().replace(R.id.content_main, fragment).commit()
}
R.id.nav_manage -> {
val fragment: SettingFragment = SettingFragment()
fragmentManager.beginTransaction().replace(R.id.content_main, fragment).commit()
}
R.id.nav_share -> {
onInviteClicked()
}
R.id.nav_send -> {
val emailIntent: Intent = Intent(android.content.Intent.ACTION_SEND)
emailIntent.type = Constants.FEEDBACK_EMAIL_TYPE
emailIntent.putExtra(android.content.Intent.EXTRA_EMAIL,
arrayOf(Constants.FEEDBACK_EMAIL_ADDRESS))
emailIntent.putExtra(android.content.Intent.EXTRA_SUBJECT,
Constants.FEEDBACK_EMAIL_SUBJECT)
startActivity(Intent.createChooser(
emailIntent, Constants.FEEDBACK_TITLE))
}
}
val drawer: DrawerLayout = findViewById(R.id.drawer_layout)
drawer.closeDrawer(GravityCompat.START)
return true
}
However this does not seem to get called at all. For example, in my NavigationDrawer activity, it shows Fragment A. The user opens the navigation drawer and selects Fragment B. setUserVisibleHint() does not get called in fragment A so my code can know it is no longer shown. I need my code that is isolated in fragment A to know when it is not shown so it can call .stop() on some variables. This is the same use case as onPause() in an activity.
You can simply call
if (myFragment.isVisible()) {...}
or another way is
public boolean isFragmentUIActive() {
return isAdded() && !isDetached() && !isRemoving();
}
Here are a few things I can think of...
Use a consistent fragment, either Support or Native, not both. And, some say the Support fragment is preferable (better maintained).
Make sure the fragment container is not hard coded in XML. If you intend to replace a fragment, then the initial fragment should be loaded dynamically by your code (you typically will load into a FrameLayout using the id as your R.id.{frameLayoutId}).
Do Use the Frament lifecycle events. onPause fires when you replace a fragment, so does onDetach. That will tell you when your old fragment is no longer visible (or will be invisible shortly). If it does not fire, then you have another issue in your code, possibly mixing of Fragment types, or a hardcoded fragment in XML?
Use setUserVisibleHint only in a fragment pager, or be prepared to set it manually. this answer has a little more to say about the use of setUserVisibleHint. When using a pager, multiple fragments can be attached at once, so an additional means (some call it lifecycle event) was needed to tell if a fragment was "really, truly" visible, hence setUserVisibleHint was introduced.
Bonus: If appropriate for your app, use the back stack for backing up by calling addToBackStack after replace. I add this mainly as an addition lifecycle item one would typically want in their app. The code looks like this...
// to initialize your fragment container
supportFragmentManager
.beginTransaction()
.add(R.id.content_fragment, fragment)
.addToBackStack("blank")
.commit()
// to update your fragment container
supportFragmentManager
.beginTransaction()
.replace(R.id.content_fragment, fragment)
.addToBackStack("settings")
.commit()
//in your XML, it can be as simple as adding the FrameLayout below,
// if you start with the Android Studio template for Navigation drawer,
// you can replace the call that includes the "content_main" layout
<!--<include layout="#layout/content_main" /> -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/content_fragment" />
I hope this helps.

Categories

Resources