Fragment是Android开发中经常使用的组件之一,也是最容易出问题的组件,为了更好地使用它,对此进行一个简单的总结。java
说明: 因为v4包中的Fragment具备更好的兼容性,且可随时更新到指定版本,因此本文的讨论仅限v4包中的Fragment。git
使用Fragment的方式有2种:github
在布局文件中使用标签,将其中的name属性值设置为须要加载的Fragment的全路径;编程
当系统在建立Activity时,会实例化布局中指定的Fragment,并调用它的onCreateView方法,以返回的View来替换元素。网络
在建立Fragment的时候须要注意,Android不推荐使用自定义构造方法的方式来建立Fragment,可以使用官方推荐的方式来传参:框架
不提倡的方式:异步
public ChatFragment(int id, String name) {
this.id = id;
this.name = name;
}
复制代码
推荐的方式:ide
public static Fragment newInstance(int id, String name) {
Fragment fragment = new ChatFragment();
Bundle bundle = new Bundle();
bundle.putInt("id", id);
bundle.putString("name", name);
fragment.setArguments(bundle);
return fragment;
}
复制代码
固然,也可以使用Fragment提供的静态初始化方法来构造Fragment:模块化
/**
* @param context 加载Fragment的Activity实例
* @param fname 须要加载的Fragment的全路径名 [其本质是经过反射调用的]
* @param args 须要传递的参数
* @return
*/
public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) {
}
// 示例
Bundle bundle = new Bundle();
bundle.putInt("id", id);
bundle.putString("name", name);
Fragment.instantiate(this, "com.sxu.fragment.ChatFragment", bundle);
复制代码
而后使用FragmentTransaction将Fragment提交给FragmentManager:布局
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
transaction.add(R.id.container_layout, ContentFragment.newInstance(0);
transaction.commit();
复制代码
相比Activity, Fragment的生命周期要复杂不少,使用官方的一张图来展现:
下面对Fragment生周期中几个核心的阶段说明一下:
onAttach是Activity与Fragment关联时被调用此,可在此方法中保存Activity实例解决getActivity为空的问题。与之对应的是onDetach, 表示与Activity解除绑定;
与Activity同样,Fragment也可使用onSaveInstanceState在退到后台时保存页面数据,但它没有提供onRestoreInstanceState方法,因此可在onCreate中进行恢复操做;
加载Fragment View的地方,相似于Activity中的setContentView, 它返会Fragment加载的View。从图上能够看出, Fragment从回退栈中返回时,会今后方法开始调用,因此可将Fragment加载的View以成员变量的形式保存,当其为空时进行再加载操做, 从而避免View的屡次加载。
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
if (mContentView == null) {
mContentView = inflater.inflate(R.layout.fragment_content_layout, container, false);
}
return mContentView;
}
复制代码
FragmentView加载完成后onViewCreated会被调用,调用时机先于onActivityCreated,其中的参数View就表示Fragment加载的布局,因此可在此方法中获取布局中的各个View。
与之对应的是onDestroyView, 它会销毁Fragment中的View.
Activity的onCreate执行完成后被调用,Fragment中的逻辑一般在此方法中执行。
Fragment虽然有完整的生命周期,但仍然须要以Activity为宿主来存在,因此它的生命周期与Activity生命周期有着直接的关系,如图所示:
从图上能够看出,Fragment的生命周期和Activity基本保持一致。
与Activity不一样的是,Fragment生命周期并老是在页面可见性发生变化时变化。在如下场景中,Fragment的可见性发生变化时,不会调用生命周期的任何方法。
Fragment在显示或隐藏时会回调onHiddenChanged, 参数hidden为true表示Fragment被隐藏, 不然表示被显示;
ViewPager中已加载的Fragment在切换时会调用setUserVisibleHint, 参数isVisibleToUser为true表示Fragment被切换到当前页.因为ViewPager中的Fragment在首次加载时,也会调用setUserVisibleHint,致使出现监听重复的问题,因此在setUserVisibleHint须要添加条件判断:
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isResumed() && isVisibleToUser) {
// 页面被显示
}
}
复制代码
因此,监听Fragment页面的显示,除了监听Fragment生命周期中的onPause/onResume方法,还须要监听onHiddenChanged方法和setUserVisibleHint。
在统计Fragment页面的显示时长时,须要综合考虑这几个方法,具体见Android无埋点方案实践项目——Tracker中对Fragment生命周期的监听过程FragmentLifecycleListener。
懒加载(或者叫延迟加载),也就是延迟数据的请求过程。经常使用于ViewPager+Fragment模式中,不一样的Fragment可能使用不一样的接口,在页面显示的时候,可能会同时请求offscreenPageLimit 个接口,致使页面出现卡顿。为了解决这种问题,可延迟未显示的Fragment的数据请求过程,即在Fragment显示时,再进行网络请求。Fragment中的setUserVisibleHint方法在ViewPager中的Fragment显示时被调用,因此咱们可在其中实现数据的请求。
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser && !dataRequested) {
requestData();
}
}
复制代码
关于这二者的区别,可从源码来看:
@Nullable
public Context getContext() {
return mHost == null ? null : mHost.getContext();
}
@Nullable
final public FragmentActivity getActivity() {
return mHost == null ? null : (FragmentActivity) mHost.getActivity();
}
复制代码
从它们的实现来看,都是直接返回mHost对象中的成员,mHost的类型为FragmentHostCallback,它的构造方法以下:
public FragmentHostCallback(Context context, Handler handler, int windowAnimations) {
this(context instanceof Activity ? (Activity) context : null, context, handler,
windowAnimations);
}
FragmentHostCallback(FragmentActivity activity) {
this(activity, activity /*context*/, activity.mHandler, 0 /*windowAnimations*/);
}
FragmentHostCallback(Activity activity, Context context, Handler handler,
int windowAnimations) {
mActivity = activity;
mContext = context;
mHandler = handler;
mWindowAnimations = windowAnimations;
}
复制代码
经过对源码进行搜索,发现只是FragmentActivity中直接调用了FragmentHostCallback的构造方法:
class HostCallbacks extends FragmentHostCallback<FragmentActivity> {
public HostCallbacks() {
super(FragmentActivity.this /*fragmentActivity*/);
}
...
}
复制代码
就目前源码的实现来讲,这二者没有任何区别,引用的都是它所在的Activity的实例,可是它提供的公开的构造方法的实现却说明: getContext()为空的可能性能更大。 因此,在Fragment中获取Context实例时最好使用getActivity()。
在使用Fragment的过程当中,getActivity()为null的异常应该是最多见的。其根本缘由:Fragment与以前关联的Activity失去了联系!
使用Fragment时,咱们的Activity继承的都是FragmentActivity, FragmentActivity在被异常关闭时会保存已加载的Fragment,具体以下:
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
markFragmentsCreated();
// 保存Fragment
Parcelable p = mFragments.saveAllState();
if (p != null) {
outState.putParcelable(FRAGMENTS_TAG, p);
}
...
}
复制代码
而后在其onCreate中对保存的Fragment进行了恢复:
protected void onCreate(@Nullable Bundle savedInstanceState) {
mFragments.attachHost(null /*parent*/);
super.onCreate(savedInstanceState);
...
if (savedInstanceState != null) {
Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
mFragments.restoreAllState(p, nc != null ? nc.fragments : null);
}
...
}
复制代码
虽然对Fragment进行了恢复,并与其关联了新的Activity实例,但Fragment以前关联的Activity实例已被销毁,若是这些Fragment中有一些延时任务,并使用了getActivity(), 就会出现空指针异常。
这是从根本上杜绝getActivity为空的方案。平常开发中,使用的延时任务最多的状况莫过于网络请求和Handler。 对于网络请求,封装的时候最好考虑Activity/Fragment的生命周期, 在onDestroy和onDetach取消网络请求,上传/下载等大数据量的网络请求,可引用ApplicationContext,放在后台服务进行执行。对于Handler, 只须要在onDetach中清除任务便可:
@Override
public void onDetach() {
super.onDetach();
handler.removeCallbacksAndMessages(null);
}
复制代码
经过保存的Activity实例替代getActivity。Activity虽然重建了,但以前的实例由于Fragment的持有而不会被内存清理,会形成短暂性的内存泄漏。
@Override
public void onAttach(Context context) {
super.onAttach(context);
this.mContext = context;
}
复制代码
具体使用哪一种方法,看本身的需求,若是项目框架良好,团队又有良好的编程规范,天然是推荐第一种。不然仍是使用第二种方案,虽然会形成短暂性的内存泄漏,倒也不会有什么大的影响。
FragmentActivity默认状况下在异常销毁时会保存Fragment,并在onCreate中进行恢复,而在重建时又会建立新的Fragment,就会出现页面重叠的问题。同时致使内存中出现n(n+1)/2个Fragment实例,这会大大增长内存消耗。这里可采用如下两种方案进行优化:
只在首次或没有Fragment实例存在的时候才建立新的Fragment:
protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState);
FragmentManager fm = getSupportFragmentManager();
// 从新关联保存的Fragment
if (savedInstanceState == null || fm.getFragments().size() == 0) {
FragmentTransaction transaction = fm.beginTransaction();
transaction.add(R.id.container_layout, ContentFragment.newInstance(fm.getFragments().size()));
transaction.commit();
}
...
复制代码
}
Activity被异常关闭时,不要保存Fragment:
@Override public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) { FragmentManager fm = getSupportFragmentManager(); List fragmentList = fm.getFragments(); if (fragmentList.size() == 0) { super.onSaveInstanceState(outState, outPersistentState); } }
getSupportFragmentManager是V4包中管理Activity中的Fragment的管理器,而getChildFragmentManager是管理Fragment中的Fragment的管理器,也就是Fragment嵌套时应该使用getChildFragmentManager而不是getSupportFragmentManager。
动态建立Fragment时,须要事务的配合,事务添加完成后须要提交,FragmentTransaction中提供了多个提交方法:
commit(); // 异步提交
commitNow(); // 同步提交
commitAllowingStateLoss(); // 异步提交,容许状态丢失
commitNowAllowingStateLoss(); // 同步提交,容许状态丢失
复制代码
commit()和commitNow()不容许在onSaveInstanceState后调用,不然会抛出java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState异常,由于onSaveInstanceState就是用来保存Fragment的状态,onSaveInstanceState后面再次提交事务,与这些事务关联的Fragment的状态就会丢失,因此抛出了异常。在这种状况下,若是肯定状态丢失不会产生影响,可以使用commitAllowingStateLoss()或commitNowAllowingStateLoss()。
与Activity相似,Fragment也有回退栈的概念,使用addBackToStack方法便可将Fragment添加到回退栈中(注意:须要在commit以前调用),加入到回退栈中的Fragment在执行Actiivty的onBackPressed()方法时,会逐渐出栈。
执行replace操做时,若是添加到回退栈中,则被替换的Fragment的onDestroy()和onDetach()不会被调用,按返回键可返回到以前的Fragment,并从onCreateView()开始调用。若是不添加到回退栈中,则会调用被替换Fragment的onDestroy()和onDetach()方法。
==建议:== Activity中的第一个Fragment不要添加到回退栈中,不然返回时须要多执行一次onBackPressed()(具体UI表现:第一个Fragment出栈后出现空白页面);
与其将Fragment与Activity比较,倒不如将其与View进行比较,毕竟它们都是不可单独存在的元素,都须要Activity做为宿主。Fragment与View很像,能够动态建立,也能够在布局文件中定义,因此可视为是加入了生命周期的View. 只不过它的管理须要借助于FragmentManager和FragmentTransaction.
在使用Fragment过程当中,常常会遇到一些UI问题:
Fragment不像Activity,没有主题的概念,若是其中加载的布局没有设置背景,默认就是透明的。若是Activity只加载了一个Fragment,看起来背景就是主题的背景,当添加多个Fragment的时候,就会发现页面出现重叠,由于背景是透明的,此时须要为Fragment中的View设置背景。
基于减小页面重绘的原则,可以使用如下方案:
Fragment中的View默认状况下是不可点击的,因此不会拦截事件。一般须要将Fragment中的根布局View的clickable属性设置为true,以屏蔽事件穿透。
有时候将当前页面切换到后台,而后恢复到前台时会发现页面内容丢失。出现这个问题,缘由主要出如今onSaveInstanceState方法,有时候可能不须要保存Fragment的状态,因此在super.onSaveInstanceState以前清空了FragmentManager中的Fragment,当页面被切换到前台时,就会出现页面为空的问题。至于如何正确保存/恢复Fragment的状态,前面的页面重叠部分已提供了解决方案。
ViewPager+Fragment是一个经典组合,基本上每一个APP中都会使用它。但在使用过程当中有一些细节须要关注。下面以一个实例来讲明一下:
fragmentList.add(new FirstFragment());
fragmentList.add(new SecondFragment());
fragmentList.add(new ThirdFragment());
viewPager.setAdapter(new MyFragmentPagerAdapter(getSupportFragmentManager()));
public static class MyFragmentPagerAdapter extends FragmentPagerAdapter {
public MyFragmentPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
return fragmentList.get(position);
}
@Override
public int getCount() {
return fragmentList.size();
}
}
复制代码
这种写法应该很熟悉,一般状况下不会出现什么问题,但当Activity重建时,就会发现,也不会有大的问题,只是多建立了几个Fragment而已,下面看一下FragmentPagerAdapter的实现:
public Object instantiateItem(ViewGroup container, int position) {
final long itemId = getItemId(position);
String name = makeFragmentName(container.getId(), itemId);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
mCurTransaction.attach(fragment);
} else {
fragment = getItem(position);
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), itemId));
}
return fragment;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
...
mCurTransaction.detach((Fragment)object);
}
复制代码
能够看到Adapter在初始化Item的时候,会先查看该position是否已存在Fragment,若是存在就从新attach, 没有的时候才会调用getItem建立新的Fragment。而destroyItem的实现说明已加载的Fragment,若是不在mOffscreenPageLimit范围内,也只是detach掉了,其Fragment实例仍然是存在的,也就是说,每一个position上的Fragment仅会建立一次(每一个position上的getItem只被调用一次),即使是Activity被重建。也就是说,FragmentPagerAdapter内置了Fragment从新关联的功能。
再回到上面的问题,若是在onCreate中建立Fragment, 那么每次Activity重建时,都会建立新的Fragment,然而这些新建立的Fragment并无什么用,由于FragmentPagerAdapter关联的仍是以前存在的Fragment。因此推荐将Fragment的构造写在getItem中。
@Override
public Fragment getItem(int position) {
return ContentFragment.newInstance(position) ;
}
复制代码
Fragment设置转场动画使用setCustomAnimations方法,它有2个重载方法,详细介绍以下:
/**
* @param entry 新fragment进入的动画,只对add和replace操做有效
* @param exit 当前fragment退出的动画,只对replace和remove操做有效
* @return
*/
public FragmentTransaction setCustomAnimations(@AnimatorRes @AnimRes int entry, @AnimatorRes @AnimRes int exit);
/**
* @param enter 新页面进入的动画
* @param exit 当前页面退出的动画
* @param popEnter 当前页面进入的动画
* @param popExit 新页面退出的动画
* @return
*/
public FragmentTransaction setCustomAnimations(@AnimatorRes @AnimRes int enter, @AnimatorRes @AnimRes int exit,
@AnimatorRes @AnimRes int popEnter, @AnimatorRes @AnimRes int popExit)
复制代码
注意:
除了setCustomAnimations方法,Android在5.0中还扩展了转场动画,可以使用如下方法实现:
/**
* 新Fragment进入时的动画
* @param transition
*/
public void setEnterTransition(@Nullable Object transition);
/**
* 新Fragment退出时的动画
* @param transition
*/
public void setReturnTransition(@Nullable Object transition);
/**
* 新Fragment进入时当前Fragment的退出动画, 需在当前Fragment对象中设置
* @param transition
*/
public void setExitTransition(@Nullable Object transition);
/**
* 新Fragment退出时原Fragment的进入动画,需在原Fragment对象中设置
* @param transition
*/
public void setReenterTransition(@Nullable Object transition);
复制代码
其中的参数,Android提供了几种实现:
示例:
FragmentManager fm = mContext.getSupportFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
Fragment fragment = ContentFragment.newInstance(fm.getFragments().size());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
fragment.setEnterTransition(new Slide(Gravity.RIGHT));
fragment.setReturnTransition(new Slide(Gravity.RIGHT));
}
transaction.add(R.id.container_layout, fragment);
transaction.addToBackStack(null);
transaction.commit();
复制代码
这是一个颇有争议性的话题,有人认为一个APP只须要一个Activity便可,也有人认为Fragment只须要使用在需复用的页面便可。关于这个问题,可从如下几方面来探讨:
基于这三点,咱们可在单个业务模块中使用单Activity+多Fragment的模式来实现,如登陆模块,只须要一个LoginActivity, 其余的功能如注册,找回密码等都使用Fragment实现。一方面,Fragment比Activity更轻量,另外一方面,使模块化在组件层面有了明显的区分。
这只是Fragment的一个简单总结,可能还有不少细节未说起,欢迎补充。