#一.前言
本文基于androidX包下的Fragment,与support包下的Fragment"略"有不同,android.app包下的Fragment已于API 28被废弃
这篇文章应该算是一篇使用Fragment后的总结,不会讲特殊的使用技巧和使用角度,不会涉及过深的源码分析,仅仅说明一般业务场景下如何正确使用Fragment
很早以前就想写关于Fragment,但是那个时候因为对Fragment没有足够的场景使用积累,学习也只是为了为学习而学习,所以没有多少心得。自从接手了新项目,遇到了一些使用上的坑之后,心得就有了(笑哭),因为Fragment使用起来的确不想Activity那样趁手。所以有了这篇一般使用
#二.Fragment初衷
android3.0推出之前,谷歌的开发人员就发现,市面上有很多app在同一个界面上展现是分区块的,不同的区块对应不同的界面展现和交互逻辑,当然也对应着不同的代码处理,可这些代码都是在同一个Activity中,造成Activity的代码很臃肿,不易维护。同时android平板开始兴起,如何在更大的屏幕上呈现更多的内容也是对开发者提出的一个问题。所以Fragment应运而生。就像下图一样,整个屏幕被分成了三个碎片(fragment),每个碎片将有自己不同的UI展现和交互逻辑。
如果仅仅是UI和交互代码交给Fragment,那它还不如叫Adapter。很显然,谷歌希望你能将管理区块的全部处理代码交给Fragment,包括Activity已有的生命周期,所以Fragment也有了生命周期。
#三.使用
##(一).原则
一个Activity尽量在一处代码块统一管理Fragment
和Activity一样,不需要由开发者手动调用生命周期
尽量像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()方法
##(三).生命周期
一般生命周期
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进行解绑
其他需要开发者关注的生命周期
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过程中学到的,算是一个使用阶段总结。