How to copy YouTube's app navigation logic

2019-02-21 00:58发布

问题:

I want to implement in my app navigation logic like in Youtube app. (BottomNavigationView + Fragment management). I want this, because these fragments are heavy, so I want them to be lazy initialized and then stored in backstack, I feel like YouTube is doing it this way. I have implemented BottomNagivationView but I have problems with Fragment Management.

My code:

bottomNavigationView.setOnTabSelectedListener { position, _ -> 
    setFragment(OnlinePageFragment.Page.values()[position])
}

where Pages is enum

enum class Page(index: Int, val klass: Class<*>) {
        ONE(0, OnePageFragment::class.java),
        TWO(1, TwoPageFragment::class.java),
        THREE(2, ThreePageFragment::class.java)
    }

and here is my setFragment function

fun setFragment(page: OnlinePageFragment.Page) {
    var fragment: Fragment? = supportFragmentManager.findFragmentByTag(page.klass.name)
    val tag = page.klass.name

    if (fragment == null)
        fragment = OnlinePageFragment.newInstance(page, null)

    val ft = supportFragmentManager.beginTransaction()
    with(ft) {
        replace(R.id.fragmentContainer, fragment, tag)
        addToBackStack(tag)
        commit()
    }

}

override fun onBackPressed() {
    if (supportFragmentManager.backStackEntryCount == 1) finish()
    else super.onBackPressed()
}

And it's working, but not as good as YouTube app. YouTube app has some magic behaviour i.e. it keeps only one transaction per each fragment, while my app allows to create "infinite" backstack of transactions. Do you have any ideas how it works in YouTube app ?

回答1:

Without using view pager you can manage it. I have implemented Please check this. https://github.com/sandeshsk/BackStackFragmentRedirectsToHome

Please update if there is any issue.

This is a method which assigns fragment

public void addFragment(FragmentManager fragmentManager,
                               Fragment fragment,
                               int containerId,boolean isFromHome){

    fragmentManager.popBackStack(null,FragmentManager.POP_BACK_STACK_INCLUSIVE);

    FragmentTransaction fragmentTransaction=fragmentManager.beginTransaction();
    if(isFromHome){
        fragmentTransaction.replace(containerId,fragment);
    }else{
        fragmentTransaction.add(new HomeFragment(),"Home");
        fragmentTransaction.addToBackStack("Home");
    }
    fragmentTransaction.replace(containerId,fragment).commit();

}

This is your navigation item listener

 private BottomNavigationView.OnNavigationItemSelectedListener mOnNavigationItemSelectedListener
        = new BottomNavigationView.OnNavigationItemSelectedListener() {

    @Override
    public boolean onNavigationItemSelected(@NonNull MenuItem item) {
        switch (item.getItemId()) {
            case R.id.navigation_home:
               if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
                    addFragment(getSupportFragmentManager(), new HomeFragment(), R.id.frame, true);
                }else{
                    getSupportFragmentManager().popBackStack();
                }
                return true;
            case R.id.navigation_dashboard:
                addFragment(getSupportFragmentManager(),new DashboardFragment(),R.id.frame,false);
                return true;
            case R.id.navigation_notifications:
                addFragment(getSupportFragmentManager(),new NotificationFragment(),R.id.frame,false);
                return true;
            case R.id.navigation_setting:
                addFragment(getSupportFragmentManager(),new SettingFragment(),R.id.frame,false);
                return true;
        }
        return false;
    }
};

onBackPressed method

 @Override
public void onBackPressed() {
    if(getSupportFragmentManager().getBackStackEntryCount()>0){
        navigation.setSelectedItemId(R.id.navigation_home);
    }else {
        super.onBackPressed();
    }
}


回答2:

I guess the best way here is to use ViewPager to store root fragments. Navigation bar will change pages of ViewPager.

Root fragments will store content fragments inside their ChildFragmentManagersand implement logic of navigation between content fragments.

Also you will need to implement logic of delegation backpress events from activity to root fragments, to perform navigation in their backstack.

UPD #1
Make sure, that root fragments are not destroyed inside ViewPager, use setOffscreenPageLimit()

UPD #2 If you don't want to use ViewPager, you can use code from this answer to manage root fragments.



回答3:

Since san's answer was showing fragments as expected, but has some bugs that call onCreateView on HomeFragment twice each time the fragment is changed. Additionally changing page is instantiating new fragment each time the page is changed.
So I took the code and modified my setPage and it's working great.

Here is how:

private void setPage(Page page) {
    FragmentManager fragmentManager = getSupportFragmentManager();
    String tag = page.name();

    FragmentTransaction transaction = fragmentManager.beginTransaction();

    // detach everything
    for (Fragment fragment : fragmentManager.getFragments()) {
        transaction.detach(fragment);
    }

    // Retrieve fragment instance, if it was already created
    Fragment fragment = fragmentManager.findFragmentByTag(tag);
    if (fragment == null) { // If not, crate new instance and add it
        try {
            fragment = (Fragment) page.clazz.newInstance();
            transaction.add(R.id.frame, fragment, tag);
        } catch (InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
    } else { // otherwise just attach it
        transaction.attach(fragment);
    }
    transaction.commit();
}

onBackPressed implementation

@Override
public void onBackPressed() {
    if (!getSupportFragmentManager().findFragmentByTag(Page.HOME.name()).isDetached()) {
        super.onBackPressed();
    } else {
        navigation.setSelectedItemId(R.id.navigation_home);
    }
}