#一.前言

  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为什么需要空的构造方法]([https://blog.csdn.net/ruancoder/article/details/52001801\]\(https://blog.csdn.net/ruancoder/article/details/52001801\)

)

我们使用有参构造方法,无非是想传入一些初始化参数,可参考上面代码,复写setArguments()方法

##(三).生命周期

  1. 一般生命周期

  2. 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. 其他需要开发者关注的生命周期

  2. 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过程中学到的,算是一个使用阶段总结。

results matching ""

    No results matching ""