Fragment的一般使用

本文介绍了Android Fragment的基本使用,包括生命周期、创建方法、与ViewPager的结合使用,并着重讲解了在ViewPager中处理Fragment可见性的问题,以及Fragment与Activity之间的数据传递。强调了Fragment管理的最佳实践,如统一管理Fragment,避免手动调用生命周期方法,以及如何处理Fragment的隐藏和可见状态变化。

一.前言

  1. 本文基于androidX包下的Fragment,与support包下的Fragment"略"有不同,android.app包下的Fragment已于API 28被废弃
  2. 这篇文章应该算是一篇使用Fragment后的总结,不会讲特殊的使用技巧和使用角度,不会涉及过深的源码分析,仅仅说明一般业务场景下如何正确使用Fragment
  3. 很早以前就想写关于Fragment,但是那个时候因为对Fragment没有足够的场景使用积累,学习也只是为了为学习而学习,所以没有多少心得。自从接手了新项目,遇到了一些使用上的坑之后,心得就有了(笑哭),因为Fragment使用起来的确不想Activity那样趁手。所以有了这篇一般使用

二.Fragment初衷

android3.0推出之前,谷歌的开发人员就发现,市面上有很多app在同一个界面上展现是分区块的,不同的区块对应不同的界面展现和交互逻辑,当然也对应着不同的代码处理,可这些代码都是在同一个Activity中,造成Activity的代码很臃肿,不易维护。同时android平板开始兴起,如何在更大的屏幕上呈现更多的内容也是对开发者提出的一个问题。所以Fragment应运而生。就像下图一样,整个屏幕被分成了三个碎片(fragment),每个碎片将有自己不同的UI展现和交互逻辑。
如果仅仅是UI和交互代码交给Fragment,那它还不如叫Adapter。很显然,谷歌希望你能将管理区块的全部处理代码交给Fragment,包括Activity已有的生命周期,所以Fragment也有了生命周期。

三.使用

(一).原则

  1. 一个Activity尽量在一处代码块统一管理Fragment
  2. 和Activity一样,不需要由开发者手动调用生命周期
  3. 尽量像Activity一样将Fragment对象交由系统处理
    下面的代码为一个App主页面,底部tab切换的管理类,分别为构造方法和tab切换处理show()方法
public TabbarManager(FragmentActivity fragmentActivity, int fragmentContentId,
                            int tabCount, ArrayList<MainBaseFragment> fragmentList,
                            ArrayList<ImageButton> tabImgBtnList,
                            ArrayList<Drawable> tabImgCkedList, ArrayList<Drawable> tabImgUnckedList) {
        this.fragmentActivity = fragmentActivity;
        this.fragmentContentId = fragmentContentId;
        this.tabCount = tabCount;
        this.fragmentList = fragmentList;
        this.tabImgBtnList = tabImgBtnList;
        this.tabImgCkedList = tabImgCkedList;
        this.tabImgUnckedList = tabImgUnckedList;

        MainBaseFragment fragment = fragmentList.get(0);
        FragmentTransaction fragmentTrans = fragmentActivity.getSupportFragmentManager().beginTransaction();
        fragmentTrans.add(fragmentContentId, fragment);
        fragmentTrans.commit();
        fragment.fragmentDidAppear();

        setTabImgBtnSelected(0);
    }
    public void show(int index) {
        if (index < 0 || index >= tabCount) {
            return;
        }

        if (index == lastTabIndex) {
            if (fragmentList.size() > lastTabIndex){
                BBTMainBaseFragment fragment = fragmentList.get(lastTabIndex);
                fragment.onTabReselected();
            }
            return;
        }

        FragmentTransaction fragTrans =
                fragmentActivity.getSupportFragmentManager().beginTransaction();
        for (int i = 0; i < tabCount; i++ ) {
            ImageButton imgBtn = tabImgBtnList.get(i);
            BBTMainBaseFragment fragment = fragmentList.get(i);
            Drawable tabImgCked = tabImgCkedList.get(i);
            if ( i == index ) {
                // 显示tabbar
                imgBtn.setBackground(tabImgCked);

                // 显示内容区
                if (fragment.isAdded()) {
                    fragment.fragmentDidAppear();
                    fragment.onResume();
                } else {
                    fragTrans.add(fragmentContentId, fragment);
                    fragment.fragmentDidLoad();
                }
                fragTrans.show(fragment);
                fragment.onTabSelected();
            } else if ( i == lastTabIndex ) {
                // 隐藏tabbar
                Drawable tabImgUncked = tabImgUnckedList.get(i);
                imgBtn.setBackground(tabImgUncked);
                // 隐藏内容区
                fragment.onPause();
                fragment.fragmentDidHide();
                fragTrans.hide(fragment);
            }
        }
        fragTrans.commitAllowingStateLoss();
        lastTabIndex = index;
    }

上述代码就呈现了两种错误的使用方式,在构造方法中和tab切换处理方法中分别使用了FragmentTransaction,导致页面中应该只存在4个Fragment内存对象,结果出现了5个,造成了页面切换时页面错乱的情况。并且在show()方法中手动调用了生命周期,导致该生命周期下的相关逻辑被调用多次。
我们再来看看Activity中的Fragment创建时的问题

 assortmentFragment = new BBTAssortmentFragment();
        newUserCenterFragment = new BBTNewUserCenterFragment();
        homePageFragment = new BBTHomePageFragment();
        newHomePageFragment = new BBTNewHomePageFragment();

        homePageFragment.setOnLongClickCommentListener(new BBTHomePageFragment.OnLongClickCommentListener() {
            @Override
            public void onClickActionBar(String title) {
            }

            @Override
            public void onClickComment(String content) {
                mCopyContent = content;
                mContextMenuType = ContextMenuType.ContextMenu_Copy;
                openMenu(tabLayout1);
            }
        });


        fragmentList.add(newHomePageFragment);
        fragmentList.add(homePageFragment);
        fragmentList.add(assortmentFragment);
        fragmentList.add(newUserCenterFragment);

这仅仅是部分代码,但是因为每次都是通过new创建出来的,忽略了系统恢复的那部分Fragment,导致当Activity被系统销毁再被恢复时,有多余的Fragment内存。

(二).创建

Fragment有两种创建方式,一种是在XML布局中直接填入,另外一种是在Java代码动态添加,也正是因为第一种创建方式,导致谷歌辟谣了好一阵“Fragment is not view”(汗)。而且这种方式因为灵活度不高,现在大家一般很少使用这种方式。
下面的代码展示了一个可从系统复用的Fragment的创建方式:

MainFragment mainFragment;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();


        mainFragment = (MainFragment) getSupportFragmentManager().findFragmentByTag(MainFragment.TAG);

        if(mainFragment == null){
            mainFragment =  MainFragment.newInstance("Say something, I'm giving up on you");
        }

        if(mainFragment.isAdded()){
            fragmentTransaction.show(mainFragment);
        } else {
            fragmentTransaction.add(R.id.fl_container, mainFragment, MainFragment.TAG);
        }

        fragmentTransaction.commitAllowingStateLoss();
    }
public static final String TAG = MainFragment.class.getSimpleName();

    String showText;
    public static MainFragment newInstance(String showText){
        MainFragment mainFragment = new MainFragment();
        Bundle bundle = new Bundle();
        bundle.putString("showText", showText);
        mainFragment.setArguments(bundle);

        return mainFragment;
    }

    @Override
    public void setArguments(@Nullable Bundle args) {
        super.setArguments(args);
        if(args != null){
            showText = args.getString("showText");
        }
    }

不要使用有参构造方法创建Fragment,系统在恢复Activity时同时会恢复Fragment,而在恢复Fragment的时候调用的就是空参构造,可参考:浅析Fragment为什么需要空的构造方法
我们使用有参构造方法,无非是想传入一些初始化参数,可参考上面代码,复写setArguments()方法

(三).生命周期

  1. 一般生命周期
  • onAttach(Context context)
    与Activity相绑定的时候调用,因为getActivity()方法会返回null, 导致App运行时崩溃,在这里可以将Context对象转换成一个全局的Activity对象,来代替getActivity()方法。这是网上的一般做法,但笔者并不推荐,因为这个全局的Activity对象其实也不能保证它的非空性,所以还是建议调用getActivity()方法并进行判空
  • onCreate(@Nullable Bundle savedInstanceState)
    这个方法对应了Fragment的真正创建,使用的场景不多,可以初始化一些数据,比如初始化EventBus。
  • onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)
    设置Fragment所显示的UI界面布局文件,建议仅仅设置布局文件
  • onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState)
    用于初始化绑定布局文件中的View
  • onActivityCreated(@Nullable Bundle savedInstanceState)
    Fragment所绑定的Activity已经onCreated(),而Fragment自己的布局也已经添加,一般很少使用
  • onStart()
    同Activity的相同生命周期
  • onResume()
    同Activity的相同生命周期
  • onPause()
    同Activity的相同生命周期
  • onStop()
    同Activity的相同生命周期
  • onDestroyView()
    在页面销毁过程中,首先卸掉Fragment自己绑定的View层级
  • onDestroy()
    Fragment进行销毁,可以在这里进行一些释放资源,内存的操作
  • onDetach()
    Fragment与Activity进行解绑
  1. 其他需要开发者关注的生命周期
  • onSaveInstanceState(@NonNull Bundle outState)
    保存页面数据,尽量不要将大内存数据保存,而是保存一些轻量的,如id值这种
  • onHiddenChanged(boolean hidden)
    Fragment是否处于隐藏状态,对于使用了后退栈管理的多个Fragment以及页面使用多个Fragment用于tab间切换的情况,可以用这个方法判断Fragment是否对用户可见,但仍然需要结合onResume()进行判断处理。如果进行Activity间的切换,Fragment的onResume()会被回调,但是onHiddenChanged()方法不会

(四).与ViewPager的结合使用

1.使用

先展示一个我们一般使用的,看是正确使用的代码:

    ViewPager viewPager;

    List<Fragment> fragmentList;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_view_pager);
        viewPager = findViewById(R.id.viewPager);

        fragmentList = new ArrayList<>();
        fragmentList.add(new ViewPagerFragment1());
        fragmentList.add(new ViewPagerFragment2());
        fragmentList.add(new ViewPagerFragment3());

        viewPager.setAdapter(new FragmentPageAdapter(getSupportFragmentManager()));

        viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

            }

            @Override
            public void onPageSelected(int position) {

            }

            @Override
            public void onPageScrollStateChanged(int state) {

            }
        });
    }

    private void setCurrentFragmentDoSomeThing(){
        int position = viewPager.getCurrentItem();
        Fragment fragment= fragmentList.get(position);
        if(fragment != null && fragment instanceof ViewPagerFragment1){
            ((ViewPagerFragment1)fragment).doSomeThing();
        }
    }


    class FragmentPageAdapter extends FragmentStatePagerAdapter{

        public FragmentPageAdapter(FragmentManager fm) {
            super(fm);
        }

        @Override
        public Fragment getItem(int position) {
            return fragmentList.get(position);
        }

        @Override
        public int getCount() {
            return fragmentList.size();
        }
    }

好像没什么问题,对吧,没错,正常情况下是没有问题的。但是在Activity被销毁重建情况下
setCurrentFragmentDoSomeThing()方法其实是不能够正常生效的,是因为FragmentStatePagerAdapter内部维护的FragmentList和页面中我们维护的fragmentList在Fragment内存对象上并不能对应上。所以这里需要做些小改动。


    private void setCurrentFragmentDoSomeThing(){
        if(adapter != null){
            Fragment fragment= adapter.getCurrentFragment();
            if(fragment != null && fragment instanceof ViewPagerFragment1){
                ((ViewPagerFragment1)fragment).doSomeThing();
            } 
        }
    }


    class FragmentPageAdapter extends FragmentStatePagerAdapter{

        Fragment currentFragment;
        public FragmentPageAdapter(FragmentManager fm) {
            super(fm);
        }

        @Override
        public Fragment getItem(int position) {
            return fragmentList.get(position);
        }

        @Override
        public int getCount() {
            return fragmentList.size();
        }

        @Override
        public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
            super.setPrimaryItem(container, position, object);
            if(object instanceof Fragment){
                currentFragment = (Fragment)object;
            }
        }
        
        public Fragment getCurrentFragment(){
            return currentFragment;
        }
    }

2.关于setUserVisibleHint()

FragmentStatePagerAdapter源码中涉及到了这个方法,贴下源码注释:

    /**
     * Set a hint to the system about whether this fragment's UI is currently visible
     * to the user. This hint defaults to true and is persistent across fragment instance
     * state save and restore.
     *
     * <p>An app may set this to false to indicate that the fragment's UI is
     * scrolled out of visibility or is otherwise not directly visible to the user.
     * This may be used by the system to prioritize operations such as fragment lifecycle updates
     * or loader ordering behavior.</p>
     *
     * <p><strong>Note:</strong> This method may be called outside of the fragment lifecycle.
     * and thus has no ordering guarantees with regard to fragment lifecycle method calls.</p>
     *

对于这个方法我是这样理解的,当我们结合系统的onHiddenChanged()方法,onResume()方法, onPause()方法使用来判断Fragment可见性时,系统的判断逻辑不能给出正确的判断结果,不能满足我们的要求时,我们可以调用这个方法,来告诉系统,当前的Fragment就是可见/不可见的,这样我们在Fragment中再通过getUserVisibleHint()方法,来获取我们设置的状态进行一些逻辑处理。一般的,我们并不需要调用这样的方法,但对于ViewPager不然,因为ViewPager有缓存机制,会提前加载未显示的页面,进而调用onResume()方法,但实际上被缓存的页面用户还没有看到,我们可以手动调用此方法做一些不可见性的逻辑处理。
如果你头一次知道这个方法也没有关系,它已经过时了。被更新的具有Lifecycle能力的方法代替了:


    /**
     * Set a hint to the system about whether this fragment's UI is currently visible
     * to the user. This hint defaults to true and is persistent across fragment instance
     * state save and restore.
     *
     * <p>An app may set this to false to indicate that the fragment's UI is
     * scrolled out of visibility or is otherwise not directly visible to the user.
     * This may be used by the system to prioritize operations such as fragment lifecycle updates
     * or loader ordering behavior.</p>
     *
     * <p><strong>Note:</strong> This method may be called outside of the fragment lifecycle.
     * and thus has no ordering guarantees with regard to fragment lifecycle method calls.</p>
     *
     * @param isVisibleToUser true if this fragment's UI is currently visible to the user (default),
     *                        false if it is not.
     *
     * @deprecated Use {@link FragmentTransaction#setMaxLifecycle(Fragment, Lifecycle.State)}
     * instead.
     */
    @Deprecated
    public void setUserVisibleHint(boolean isVisibleToUser) {
        if (!mUserVisibleHint && isVisibleToUser && mState < STARTED
                && mFragmentManager != null && isAdded() && mIsCreated) {
            mFragmentManager.performPendingDeferredStart(this);
        }
        mUserVisibleHint = isVisibleToUser;
        mDeferStart = mState < STARTED && !isVisibleToUser;
        if (mSavedFragmentState != null) {
            // Ensure that if the user visible hint is set before the Fragment has
            // restored its state that we don't lose the new value
            mSavedUserVisibleHint = isVisibleToUser;
        }
    }
    /**
     * Set a ceiling for the state of an active fragment in this FragmentManager. If fragment is
     * already above the received state, it will be forced down to the correct state.
     *
     * <p>The fragment provided must currently be added to the FragmentManager to have it's
     * Lifecycle state capped, or previously added as part of this transaction. The
     * {@link Lifecycle.State} passed in must at least be {@link Lifecycle.State#CREATED}, otherwise
     * an {@link IllegalArgumentException} will be thrown.</p>
     *
     * @param fragment the fragment to have it's state capped.
     * @param state the ceiling state for the fragment.
     * @return the same FragmentTransaction instance
     */
    @NonNull
    public FragmentTransaction setMaxLifecycle(@NonNull Fragment fragment,
            @NonNull Lifecycle.State state) {
        addOp(new Op(OP_SET_MAX_LIFECYCLE, fragment, state));
        return this;
    }

(五)getView()和requireView()

这只是一个非常小的知识点了,如果我们在Fragment的onCreateView()方法中设置了UI布局,那么后续通过getView()方法是可以获取到根布局的,不过如果没有设置,我们获取的时候会是null,androidx包下的Fragment新加入了一个requireView()方法,作用和逻辑与getView()一样,只不过当获取不到view时会直接抛出异常。

    /**
     * Get the root view for the fragment's layout (the one returned by {@link #onCreateView}).
     *
     * @throws IllegalStateException if no view was returned by {@link #onCreateView}.
     * @see #getView()
     */
    @NonNull
    public final View requireView() {
        View view = getView();
        if (view == null) {
            throw new IllegalStateException("Fragment " + this + " did not return a View from"
                    + " onCreateView() or this was called before onCreateView().");
        }
        return view;
    }

(六)给Activity传值

Activity给Fragment传值使用setArguments()方法,那Fragment给Activity传值呢?网上搜了一遍,貌似官方没有提供直接Api,所以其实现方式就不那么优雅了。
Fragment设置回调接口,Activity进行实现:

   private MediaPreviewBaseActionListener previewBaseActionListener;

    public interface MediaPreviewBaseActionListener {

        void onClickPage(MediaItem mediaItem);

        void onPageSelected(int position, MediaItem mediaItem);
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        // 确认容器 Activity 已实现该回调接口。否则,抛出异常
        try {
            previewBaseActionListener = (MediaPreviewBaseActionListener) context;
        } catch (ClassCastException e) {
            throw new ClassCastException(context.toString()
                    + " must implement MediaPreviewBaseActionListener");
        }
    }

public abstract class MediaPreviewBaseActivity extends BBTBaseActivity implements MediaPreviewBaseFragment.MediaPreviewBaseActionListener 

    @Override
    public void onClickPage(MediaItem mediaItem) {
        finish();
    }

    @Override
    public void onPageSelected(int position, MediaItem mediaItem) {

    }

四.结语

因为目前项目中关于Fragment的使用场景相对简单,更复杂的场景和问题我也没有遇见过,比如一直为人诟病的Fragment嵌套问题,我仅仅是知道有问题具体什么场景下有什么问题我也不清楚,因为知道有问题,所以项目中我也竭力避免这样使用,今天总结的这些我也是在断断续续的改bug过程中学到的,算是一个使用阶段总结。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值