Fragment

摘要:

  • Fragment生命周期
  • Fragment与Activity通信
  • Fragment的使用陷阱

Fragment

特点

  • Fragment 解决Activity间的切换不流畅,轻量切换
  • 可以从startActivityForResult中接收到返回结果,但是View不能
  • 只能在 Activity 保存其状态(用户离开 Activity)之前使用 commit() 提交事务。如果您试图在该时间点后提交,则会引发异常。 这是因为如需恢复 Activity,则提交后的状态可能会丢失。 对于丢失提交无关紧要的情况,请使用commitAllowingStateLoss()。

生命周期

Fragment生命周期

其中onActivityCreated()已过时,现在用onViewCreate()

onAttach

onCreate

onCreateView

onViewCreated(API13的时候引入的)

onActivityCreated

onStart

onResume

与Activity通信

执行此操作的一个好方法是,在片段内定义一个回调接口,并要求宿主 Activity 实现它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static class FragmentA extends ListFragment {
...
// Container Activity must implement this interface
public interface OnArticleSelectedListener {
public void onArticleSelected(Uri articleUri);
}
...
}

public static class FragmentA extends ListFragment {
OnArticleSelectedListener mListener;
...
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mListener = (OnArticleSelectedListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString());
}
}
...
}

Fragment使用陷阱

重复创建Fragment

错误例子:

1
2
3
4
5
6
7
8
9
10
11
class YourActivity{
@Override
protected void onCreated(Bundle savedInstanceState){
super.onCreate(saveInstanceState);
//...
FragmentManager manager = getSupportFragmentManager();
FragmentTransaction transaction = manager.beginTransaction();
transaction.replace(R.id.container, new YourFragment());
transaction.commit();
}
}

这个例子中,Activity重建时,系统会自动帮我们恢复 Fragment (super.onCreate),接下来我们自己又创建了一个新的实例,然后把系统创建的那个 repalce掉。表面上程序运行正常,实际上我们自己创建的那个Fragment是不必要的。

正确做法:

1
2
3
4
5
6
7
FragmentManager manager = getSupportFragmentManager();
if(manager.findFragmentById(R.id.container) == null){
FragmentManager manager = getSupportFragmentManager();
FragmentTransaction transaction = manager.beginTransaction();
transaction.replace(R.id.container, new YourFragment());
transaction.commit();
}

此外,直接把Fragment写在xml中不会有这个问题,即便onCreate的时候总是setContentView。我们有理由推断,这种情况下用了类似的方法防止重复创建fragment,因为它要求我们给<fragment>加一个idtag,否则会有一个warning。

参数传递

错误例子:

对于普通类是正确的,对于fragment是错误的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class SomeFragment{
Foo mFoo;
Bar mBar;
void setFoo(Foo foo){ mFoo = foo; }
void setBar(Bar bar){ mBar = bar; }
}

//或者
//对于fragment还会有警告“fragment应该有无参的构造函数”
class SomeFragment{
Foo mFoo;
Bar mBar;
SomeFragment(Foo foo, Bar bar){
mFoo = foo;
mBar = bar;
}
}

上面的传参用法对于Fragment是错误的,因为系统在恢复Fragment的时候使用的是无参构造函数。所以当Fragment由系统创建的时候,mFoomBar都会是null

正确做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class YourFragment extends Fragment{
static YourFragment makeInstance(/* param0, param1, ... */){
YourFragment fragment = new YourFragment();
Bundle args = new Bundle();
//args.putXXX
fragment.setArguments(args);
}

@Override
public void onCreate(@Nullable Bundle savedInstanceState){
super.onCreate(savedInstanceState);
Bundle args = getArguments();
if(args != null){
//unpack arguments
}
}
}

通过这种方式,即便是系统恢复 Activity 时自动创建的 Fragment,也可以 get 到原来设置的参数(不需要在 onSaveInstanceState 的时候保存)。当然,有些数据可能不适合放在 Bundle 里,这个时候可以另外用一个 setRetainInstance(true) 的 fragment 来保存(或者用 ViewModel)。

与ViewPager交互,getActivity()为空问题

正常例子:(有坑)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class SomePagerAdapter extends FragmentPagerAdapter {

private static final Fragment[] mFragments = new Fragment[3];

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

@Override
public Fragment getItem(int i) {
if (mFragments[i] == null) {
mFragments[i] = createFragment(i);
}
return mFragments[i];
}

private Fragment createFragment(int index) {
switch (index) {
case 0:
return new Fragment1();
case 1:
return new Fragment2();
case 2:
return new Fragment3();
default:
throw new IllegalArgumentException();
}
}

@Override
public int getCount() {
return mFragments.length;
}
}

这一段代码的问题跟前面两个比起来隐晦得多,也不是所有这样写的代码都会出问题。某些情况下,比方说,用户触发了某个动作后,我们想获取当前的 fragment 并做一些操作:

1
2
3
4
void foo() {
Fragment fragment = mPagerAdapter.getItem(mViewPager.getCurrentItem());
fragment.bar();
}

我们再假设 bar() 方法里调用了 getActivity(),这样,线上就会出现当前 fragment 的 getActivity 竟然返回 null 的崩溃……

正确例子:

adapter 的最佳实践应该是, getItem 总返回一个新创建的 fragment(如果它叫 makeItem、createItem,就不会有这么多麻烦事了):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class SomePagerAdapter extends FragmentPagerAdapter {

private static final Fragment[] mFragments = new Fragment[3];

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

@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
Object object = super.instantiateItem(container, position);
mFragments[position] = (Fragment) object;
return object;
}

@Nullable
public Fragment getFragment(int position) {
return mFragments[position];
}

@Override
public Fragment getItem(int i) {
switch (i) {
case 0:
return new Fragment1();
case 1:
return new Fragment2();
case 2:
return new Fragment3();
default:
throw new IllegalArgumentException();
}
}

@Override
public int getCount() {
return mFragments.length;
}
}

作为一个妥协,我们把父类 instantiateItem 返回的 fragment 缓存了起来。

防止ViewPager+Fragment结合使用时的数据预加载

背景:

ViewPager+Fragment结合使用,有俩适配器可用FragmentStatePagerAdapterFragmentPagerAdapter

FragmentStatePagerAdapter:在内存中最多保留三个Fragment实例,即当有Fragment切换时,会走Fragment的生命周期onDestroyView()–>onDestroy(),创建会走onCreate()–>onCreateView()

FragmentPagerAdapter:内存中会保留所有Fragment实例,即当Fragment切换时,会走onDestroyView()–>onCreateView(),不走onDestroy()onCreate()

这两种都存在预加载Fragment的效果(若每个Fragment都要网络请求数据,等待网络请求结束的进度条会在所有fragment预加载结束再消失)

防止Fragment预加载做法:

  1. 更改Fragment中的setUserVisibleHint方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public void setUserVisibleHint(boolean isVisibleToUser){
    super.setUserVisibleHint(isVisibleToUser);
    }

    //更改成
    public void serUserVisibleHint(boolean isVisibleToUser){
    //isVisibleToUser--》true:Fragment用户可见
    //isVisible()--》判断Fragment的视图是否创建好
    if(isVisibleToUser && isVisible()){
    initData();
    }
    }
  2. 上面的写法,第一个显示的Fragment会因为isVisibleToUser=trueisVisible()=false而显示空白:

    1
    2
    3
    4
    5
    6
    7
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
    //...
    if(getUserVisibleHint()){
    initData();
    }
    return inflater.inflate(R.layout.fragment_blank, container, false);
    }

上面的写法有缺陷(每次切换都要重新请求数据消耗流量)升级版做法:

加个规定时间内切换Fragment则不重新请求数据的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private long refreshTime = 0;
public void setUserVisibleHint(boolean isVisibleToUser){
if(isVisibleToUser && isVisible()){
//只有当超过一定的时效后,Fragment可见时,才进行数据的重新请求
if(refreshTime == 0 ||(refreshTime != 0 && (System.currentTimeMillis() - refreshTime) > 30*60*1000)){
initData();
}
}
super.setUserVisibleHint(isVisibleToUser);
}

private void initData(){
refreshTime = System.currentTimeMillis();
}

public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
refreshTime = 0;
//...
}

上面的写法,可用进一步,比如把网络请求回来的json数据进行缓存。再次网络请求直接加载这个缓存来节省流量。

新版fragment判断可见

1
2
3
4
5
6
7
@Override
public void onResume() {
super.onResume();
if (!isHidden() && isResumed()){

}
}

代码

Activity 与 Fragment 交互

Fragment 给 Activity 传值

  • Fragment 可以直接调用 Activity 中非 private 的方法

Activity 给 Fragment 传值

*

动态替换fragment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 添加一个Fragment
* @param containerId
* @param fragment
* @param tag
*/
protected void addFragment(int containerId, Fragment fragment, String tag) {
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.add(containerId,fragment,tag);
fragmentTransaction.addToBackStack(tag);
fragmentTransaction.commit();
}

/**
* 替换Fragment
* @param containerId
* @param fragment
* @param tag
*/
protected void replaceFragment(int containerId,Fragment fragment,String tag) {
if(getSupportFragmentManager().findFragmentByTag(tag) == null) {
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(containerId,fragment,tag);
fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
fragmentTransaction.addToBackStack(tag);
fragmentTransaction.commit();
}else {
//弹出tag标记的那层以上的所有fragment
getSupportFragmentManager().popBackStack(tag,0);
}
}

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEvent(ChangeTextBookEvent event){
if (event != null){
Fragment secondFragment;
if (DataManager.getInstance().getChosedTextBook() == null || event.isShowChoosePage()){
secondFragment = ChooseGradeFragment.createInstance(UserRecyclerFragment.RANGE_RECOMMEND);
} else {
secondFragment = CourseFragment.createInstance(UserRecyclerFragment.RANGE_RECOMMEND);
}
fragments[1] = secondFragment;
FragmentTransaction ft = fragmentManager.beginTransaction();
ft.replace(getFragmentContainerResId(), secondFragment).commit();
}
}

快速切换fragment出现内存泄漏

Fragment中嵌套可切换的fragment【可参考361度SportFragment.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//“根”Fragment中

private FragmentManager fragmentManager;
private final ArrayList<Fragment> mFragments = new ArrayList<>();
protected int currentPosition = 0;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
//实例化fragments
mFragments.add(childFragment1);
mFragments.add(childFragment2);
//实例化fragmentManager
fragmentManager = context.getSupportFragmentManager();
//初始显示fragment
showFragment(currentPosition);
//点击切换fragment逻辑
tv1.setOnClickListener(v->showFragment(0));
tv2.setOnClickListener(v->showFragment(1));
}

private void showFragment(int index) {
if (fragmentManager != null && mFragments.size() > index) {
FragmentTransaction ft = fragmentManager.beginTransaction();
ft.hide(mFragments.get(currentPosition));
if (!mFragments.get(index).isAdded()){
ft.add(R.id.container_fragment, mFragments.get(index));
}
ft.show(mFragments.get(index)).commit();
this.currentPosition = index;
}
}

上面的代码在崩溃自动重启的时候会出现空白页面,需要把

1
fragmentManager = context.getSupportFragmentManager();

改成

1
fragmentManager = getChildFragmentManager();

Fragment问题

关于fragment的onHidden的问题

  1. 这个只有在hide,show的时候会触发 在add 的时候是不会触发的 首次加载的时候是不触发的onHiddenChanged

  2. activity 上有4个fragment 当前的fragment 为A activity finish掉的时候, B fragment不走生命周期的 除非是已经被调用.add的时候才会走生命周期

关于fragment的onHiddenChanged+onResume+问题

  1. viewpager+fragment 情况:

onHiddenOnChange()+onResume()的区别:

第一次可见的时候生命周期都是onHiddenOnChange–>onResume

其它都只走onHiddenOnChange。除非是activity切换了走了生命周期,这个时候会走onPause跟onResume但是不走onHiddenOnChange

  1. 不带有viewpager情况:

Activity has been destroyed

1
2
3
4
5
//在 activity 的 onCreate 中
mFragmentManager.beginTransaction()
.add(resId, new OneFragment())
//这里会报异常,activity has been destroyed
.commit();

原因:Fragmenttransaction也是在 onCreate 中初始化,也许是异步初始化。这就涉及到执行顺序问题。
解决:add(redId,Fragment)放到onCreate()后的生命周期onResume()

DilogFragment生命周期和和设置布局大小无效问题

FragmentManag.beginTransaction().add(Res,fragment).commit 报错 Activity has been destroyed

1
2
3
4
5
6
java.lang.RuntimeException: Unable to start activity ComponentInfo{}java.lang.IllegalStateException: Activity has been destroyed

Caused by: java.lang.IllegalStateException: Activity has been destroyed
at android.support.v4.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:1560)
at android.support.v4.app.BackStackRecord.commitInternal(BackStackRecord.java:696)
at android.support.v4.app.BackStackRecord.commit(BackStackRecord.java:662)
1
2
3
4
5
fragment = new OneFragment();
mFragmentManager.beginTransaction()
.add(com.handsomeyang.mango.R.id.root_relative_layout, fragment)
//这里会报异常,activity has been destroyed
.commit();

当我在Activity 的onCreate 中初始化fragment 并且用事务添加到Activity 的时候报错,说是Activity 已被销毁,然而怎么也不能理解,commit 源码点进去没找到,百度了好一会,在stackoverflow 上一个出现类似问题的评论中发现一句话: 说是Fragmentranscation 也是在onCrate中初始化的,也许是异步初始化,这样就涉及到代码执行顺序问题,最好把addFragment 往后提一提,我把这段同样的代码写在OnResume中就能顺利添加上fragment 并且不报错了。

getActivity() 有时候为null

fragment not attached to a context

解决:getString()替换成getActivity().getString()

而我用getActivity().getString()getActivity().getResource().getString()都不行,直接在这个页面写死文案。