fragment的生命周期图

关于fragments你所不知道的

复杂的生命周期

Android中,Context是一个上帝对象(god object),而Activity是具有附加生命周期的context。具有生命周期的上帝对象?有点讽刺的意味。Fragments不是上帝对象,但它们为了弥补这一点,实现了及其复杂的生命周期。

Steve Pomeroy为Fragments复杂的生命周期制作了一张图表看起来并不可爱:

上面Fragments的生命周期使得开发者很难弄清楚在每个回调处要做什么,这些回调是同步的还是异步的?顺序如何?

难以调试

当你的app出现bug,你使用调试器并一步一步执行代码以便了解到底发生了什么,这通常能很好地工作,直到你遇到了FragmentManagerImpl:它是地雷。

下面这段代码很难跟踪和调试,这使得很难正确的修复app中的bug:

switch (f.mState) {
    case Fragment.INITIALIZING:
        if (f.mSavedFragmentState != null) {
            f.mSavedViewState = f.mSavedFragmentState.getSparseParcelableArray(
                    FragmentManagerImpl.VIEW_STATE_TAG);
            f.mTarget = getFragment(f.mSavedFragmentState,
                    FragmentManagerImpl.TARGET_STATE_TAG);
            if (f.mTarget != null) {
                f.mTargetRequestCode = f.mSavedFragmentState.getInt(
                        FragmentManagerImpl.TARGET_REQUEST_CODE_STATE_TAG, 0);
            }
            f.mUserVisibleHint = f.mSavedFragmentState.getBoolean(
                    FragmentManagerImpl.USER_VISIBLE_HINT_TAG, true);
            if (!f.mUserVisibleHint) {
                f.mDeferStart = true;
                if (newState > Fragment.STOPPED) {
                    newState = Fragment.STOPPED;
                }
            }
        }
// ...
}
如果你曾经遇到屏幕旋转时旧的unattached的fragment重新创建,那么你应该知道我在谈论什么(不要让我从嵌套fragments讲起)。

正如Coding Horror所说,根据法律要求我需要附上这个动画的链接

经过多年深入的分析,我得到的结论是WTFs/min = 2^fragment的个数。

View controllers?没这么快

由于fragments创建,绑定和配置views,它们包含了大量的视图相关的代码。这实际上意味着业务逻辑没有和视图代码解耦-这使得很难针对fragments编写单元测试。

Fragment事务

Fragment事务使得你可以执行一系列fragment操作,不幸的是,提交事务是异步的,而且是附加在主线程handler队列尾部的。当你的app接收到多个点击事件或者配置发生变化时,将处于不可知的状态。

class BackStackRecord extends FragmentTransaction {
    int commitInternal(boolean allowStateLoss) {
        if (mCommitted)
            throw new IllegalStateException("commit already called");
        mCommitted = true;
        if (mAddToBackStack) {
            mIndex = mManager.allocBackStackIndex(this);
        } else {
            mIndex = -1;
        }
        mManager.enqueueAction(this, allowStateLoss);
        return mIndex;
    }
}

Fragment创建魔法

Fragment实例可以由你或者fragment manager创建。下面代码似乎很合理:

DialogFragment dialogFragment = new DialogFragment() {
  @Override public Dialog onCreateDialog(Bundle savedInstanceState) { ... }
};
dialogFragment.show(fragmentManager, tag);
然而,当恢复activity实例的状态时,fragment manager可能会尝试通过反射机制重新创建这个fragment类的实例。由于这是一个匿名内部类,它的构造函数有一个隐藏的参数,持有外部类的引用。
1
2
3
4
`android.support.v4.app.Fragment$InstantiationException:`
`    ``Unable to instantiate fragment com.squareup.MyActivity$1:`
`    ``make sure class name exists, is public, and has an empty`
`    ``constructor that is public`

Fragments的经验教训

尽管存在缺点,fragments教给我们宝贵的教训,让我们在编写app的时候可以重用:

  • 单Activity界面:没有必要为每个界面使用一个activity。我们可以分割我们的app为解耦的组件然后根据需要进行组合。这使得动画和生命周期变得简单。我们可以把组件代码分割成视图代码和控制器代码。
  • 返回栈不是activity特性的概念;我们可以在一个activity中实现返回栈。
  • 没有必要使用新的API;我们所需要的一切都是早就存在的:activities,views和layout inflaters。

响应式UI:fragments vs 自定义views

Fragments

让我们看一个fragment的简单例子,一个列表和详情UI。

HeadlinesFragment是一个简单的列表:

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
`public class HeadlinesFragment extends ListFragment {`
`  ``OnHeadlineSelectedListener mCallback;`
`  ``public interface OnHeadlineSelectedListener {`
`    ``void onArticleSelected(int position);`
`  ``}`
`  ``@Override`
`  ``public void onCreate(Bundle savedInstanceState) {`
`    ``super``.onCreate(savedInstanceState);`
`    ``setListAdapter(`
`        ``new` `ArrayAdapter<String>(getActivity(),`
`            ``R.layout.fragment_list,`
`            ``Ipsum.Headlines));`
`  ``}`
`  ``@Override`
`  ``public void onAttach(Activity activity) {`
`    ``super``.onAttach(activity);`
`    ``mCallback = (OnHeadlineSelectedListener) activity;`
`  ``}`
`  ``@Override`
`  ``public void onListItemClick(ListView l, View v, int position, long id) {`
`    ``mCallback.onArticleSelected(position);`
`    ``getListView().setItemChecked(position, ``true``);`
`  ``}`
`}`
接下来比较有趣:ListFragmentActivity到底需要处理相同界面上的细节还是不需要呢?
public class ListFragmentActivity extends Activity
    implements HeadlinesFragment.OnHeadlineSelectedListener {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.news_articles);
    if (findViewById(R.id.fragment_container) != null) {
      if (savedInstanceState != null) {
        return;
      }
      HeadlinesFragment firstFragment = new HeadlinesFragment();
      firstFragment.setArguments(getIntent().getExtras());
      getFragmentManager()
          .beginTransaction()
          .add(R.id.fragment_container, firstFragment)
          .commit();
    }
  }
  public void onArticleSelected(int position) {
    ArticleFragment articleFrag =
        (ArticleFragment) getFragmentManager()
            .findFragmentById(R.id.article_fragment);
    if (articleFrag != null) {
      articleFrag.updateArticleView(position);
    } else {
      ArticleFragment newFragment = new ArticleFragment();
      Bundle args = new Bundle();
      args.putInt(ArticleFragment.ARG_POSITION, position);
      newFragment.setArguments(args);
      getFragmentManager()
          .beginTransaction()
          .replace(R.id.fragment_container, newFragment)
          .addToBackStack(null)
          .commit();
    }
  }
}

自定义views

让我们只使用views来重新实现上面代码的相似版本。

首先,我们定义Container的概念,它可以显示一个item,也可以处理返回键。

1
2
3
4
5
`public interface Container {`
`  ``void showItem(String item);`
`  ``boolean onBackPressed();`
`}`
Activity假设总会存在一个container,并把工作委托给它。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
`public class MainActivity extends Activity {`
`  ``private Container container;`
`  ``@Override protected void onCreate(Bundle savedInstanceState) {`
`    ``super``.onCreate(savedInstanceState);`
`    ``setContentView(R.layout.main_activity);`
`    ``container = (Container) findViewById(R.id.container);`
`  ``}`
`  ``public Container getContainer() {`
`    ``return` `container;`
`  ``}`
`  ``@Override public void onBackPressed() {`
`    ``boolean handled = container.onBackPressed();`
`    ``if` `(!handled) {`
`      ``finish();`
`    ``}`
`  ``}`
`}`
列表的代码也类似如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
`public class ItemListView extends ListView {`
`  ``public ItemListView(Context context, AttributeSet attrs) {`
`    ``super``(context, attrs);`
`  ``}`
`  ``@Override protected void onFinishInflate() {`
`    ``super``.onFinishInflate();`
`    ``final MyListAdapter adapter = ``new` `MyListAdapter();`
`    ``setAdapter(adapter);`
`    ``setOnItemClickListener(``new` `OnItemClickListener() {`
`      ``@Override public void onItemClick(AdapterView<?> parent, View view,`
`            ``int position, long id) {`
`        ``String item = adapter.getItem(position);`
`        ``MainActivity activity = (MainActivity) getContext();`
`        ``Container container = activity.getContainer();`
`        ``container.showItem(item);`
`      ``}`
`    ``});`
`  ``}`
`}`
接着任务是:基于资源限定符加载不同的XML布局文件。

res/layout/main_activity.xml:

1
2
3
4
5
6
7
8
9
10
11
`<com.squareup.view.SinglePaneContainer`
`    ``xmlns:android=``"[http://schemas.android.com/apk/res/android](http://schemas.android.com/apk/res/android)"`
`    ``android:layout_width=``"match_parent"`
`    ``android:layout_height=``"match_parent"`
`    ``android:id=``"@+id/container"`
`    ``>`
`  ``<com.squareup.view.ItemListView`
`      ``android:layout_width=``"match_parent"`
`      ``android:layout_height=``"match_parent"`
`      ``/>`
`</com.squareup.view.SinglePaneContainer>`
res/layout-land/main_activity.xml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
`<com.squareup.view.DualPaneContainer`
`    ``xmlns:android=``"[http://schemas.android.com/apk/res/android](http://schemas.android.com/apk/res/android)"`
`    ``android:layout_width=``"match_parent"`
`    ``android:layout_height=``"match_parent"`
`    ``android:orientation=``"horizontal"`
`    ``android:id=``"@+id/container"`
`    ``>`
`  ``<com.squareup.view.ItemListView`
`      ``android:layout_width=``"0dp"`
`      ``android:layout_height=``"match_parent"`
`      ``android:layout_weight=``"0.2"`
`      ``/>`
`  ``<include layout=``"@layout/detail"`
`      ``android:layout_width=``"0dp"`
`      ``android:layout_height=``"match_parent"`
`      ``android:layout_weight=``"0.8"`
`      ``/>`
`</com.squareup.view.DualPaneContainer>`
下面是这些containers的简单实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
`public class DualPaneContainer extends LinearLayout implements Container {`
`  ``private MyDetailView detailView;`
`  ``public DualPaneContainer(Context context, AttributeSet attrs) {`
`    ``super``(context, attrs);`
`  ``}`
`  ``@Override protected void onFinishInflate() {`
`    ``super``.onFinishInflate();`
`    ``detailView = (MyDetailView) getChildAt(1);`
`  ``}`
`  ``public boolean onBackPressed() {`
`    ``return` `false``;`
`  ``}`
`  ``@Override public void showItem(String item) {`
`    ``detailView.setItem(item);`
`  ``}`
`}`
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 SinglePaneContainer extends FrameLayout implements Container {`
`  ``private ItemListView listView;`
`  ``public SinglePaneContainer(Context context, AttributeSet attrs) {`
`    ``super``(context, attrs);`
`  ``}`
`  ``@Override protected void onFinishInflate() {`
`    ``super``.onFinishInflate();`
`    ``listView = (ItemListView) getChildAt(0);`
`  ``}`
`  ``public boolean onBackPressed() {`
`    ``if` `(!listViewAttached()) {`
`      ``removeViewAt(0);`
`      ``addView(listView);`
`      ``return` `true``;`
`    ``}`
`    ``return` `false``;`
`  ``}`
`  ``@Override public void showItem(String item) {`
`    ``if` `(listViewAttached()) {`
`      ``removeViewAt(0);`
`      ``View.inflate(getContext(), R.layout.detail, ``this``);`
`    ``}`
`    ``MyDetailView detailView = (MyDetailView) getChildAt(0);`
`    ``detailView.setItem(item);`
`  ``}`
`  ``private boolean listViewAttached() {`
`    ``return` `listView.getParent() != ``null``;`
`  ``}`
`}`
抽象出这些container并以这种方式来构建app并不难-我们不仅不需要fragments,而且代码将是易于理解的。

Views & presenters

使用自定义views是很棒的,但我们想把业务逻辑分离到专门的controllers中。我们把这些controller称为presenters。这样一来,代码将更加可读,测试更加容易。上面例子中的MyDetailView如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
`public class MyDetailView extends LinearLayout {`
`  ``TextView textView;`
`  ``DetailPresenter presenter;`
`  ``public MyDetailView(Context context, AttributeSet attrs) {`
`    ``super``(context, attrs);`
`    ``presenter = ``new` `DetailPresenter();`
`  ``}`
`  ``@Override protected void onFinishInflate() {`
`    ``super``.onFinishInflate();`
`    ``presenter.setView(``this``);`
`    ``textView = (TextView) findViewById(R.id.text);`
`    ``findViewById(R.id.button).setOnClickListener(``new` `OnClickListener() {`
`      ``@Override public void onClick(View v) {`
`        ``presenter.buttonClicked();`
`      ``}`
`    ``});`
`  ``}`
`  ``public void setItem(String item) {`
`    ``textView.setText(item);`
`  ``}`
`}`
让我们看一下从Square Register中抽取的代码,编辑账号信息的界面如下:

presenter在高层级操作view:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
`class EditDiscountPresenter {`
`  ``// ...`
`  ``public void saveDiscount() {`
`    ``EditDiscountView view = getView();`
`    ``String name = view.getName();`
`    ``if` `(isBlank(name)) {`
`      ``view.showNameRequiredWarning();`
`      ``return``;`
`    ``}`
`    ``if` `(isNewDiscount()) {`
`      ``createNewDiscountAsync(name, view.getAmount(), view.isPercentage());`
`    ``} ``else` `{`
`      ``updateNewDiscountAsync(discountId, name, view.getAmount(),`
`        ``view.isPercentage());`
`    ``}`
`    ``close();`
`  ``}`
`}`
为这个presenter编写测试是轻而易举的事:
1
2
3
4
5
6
7
`@Test public void cannot_save_discount_with_empty_name() {`
`  ``startEditingLoadedPercentageDiscount();`
`  ``when(view.getName()).thenReturn(``""``);`
`  ``presenter.saveDiscount();`
`  ``verify(view).showNameRequiredWarning();`
`  ``assertThat(isSavingInBackground()).isFalse();`
`}`

返回栈管理

管理返回栈不需要异步事务,我们发布了一个小的函数库Flow来实现这个功能。Ray Ryan写了一篇很赞的博文介绍Flow。

我已经深陷在fragment的泥沼中,我如何逃离呢?

把fragments做成空壳,把view相关的代码写到自定义view类中,把业务逻辑代码写到presenter中,由presenter和自 定义views进行交互。这样一来,你的fragment几乎就是空的了,只需要在其中inflate自定义views,并把views和 presenters关联起来。

1
2
3
4
5
6
`public class DetailFragment extends Fragment {`
`  ``@Override public View onCreateView(LayoutInflater inflater,`
`    ``ViewGroup container, Bundle savedInstanceState) {`
`    ``return` `inflater.inflate(R.layout.my_detail_view, container, ``false``);`
`  ``}`
`}`
到这里,你可以消除fragment了。

从fragments模式移植过来并不容易,但我们做到了-感谢Dimitris KoutsogiorgasRay Ryan的杰出工作。

Dagger&Mortar如何呢?

Dagger&Mortar和fragments是正交的,它们可以和fragments一起工作,也可以脱离fragments而工作。

Dagger帮助我们把app模块化成一个解耦的组件图。他处理所有的绑定,使得可以很容易的提取依赖并编写自相关对象。

Mortar工作于Dagger之上,它具有两大优点:

  • 它为被注入组件提供简单的生命周期回调。这使你可以编写在屏幕旋转时不会被销毁的presenters单例,而且可以保存状态到bundle中从而在进程死亡中存活下来。
  • 它为你管理Dagger子图,并帮你把它绑定到activity的生命周期中。这让你有效的实现范围的概念:一个views生成的时候,它的presenter和依赖会作为子图创建;当views销毁的时候,你可以很容易的销毁这个范围,并让垃圾回收起作用。

结论

我们曾经大量的使用fragments,但最终改变了我们的想法:

  • 我们很多疑难的crashes都和fragment生命周期相关;
  • 我们只需要views来构建响应式的UI,一个返回栈和屏幕转场功能。