我为什么主张反对使用Android Fragment

    最近我在Droidcon Paris举办了一场技术讲座,我讲述了Square公司在使用Android fragments时遇到的问题,以及其他人如何避免使用fragments。

    在2011年,基于以下原因我们决定在项目中使用fragments:

    • 在那个时候,我们还没有支持平板设备-但是我们知道最终将会支持的,Fragments有助于构建响应式UI;

    • Fragments是view controllers,它们包含可测试的,解耦的业务逻辑块;

    • Fragments API提供了返回堆栈管理功能(即把activity堆栈的行为映射到单独一个activity中);

    • 由于fragments是构建在views之上的,而views很容易实现动画效果,因此fragments在屏幕切换时具有更好的控制;

    • Google推荐使用fragments,而我们想要我们的代码标准化;

    自从2011年以来,我们为Square找到了更好的选择。

    复杂的生命周期

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

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

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

    难以调试

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

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

    如果你曾经遇到屏幕旋转时旧的unattached的fragment重新创建,那么你应该知道我在谈论什么(不要让我从嵌套fragments讲起)。

    正如Coding Horror所说,根据法律要求我需要附上这个。5. 我为什么主张反对使用Android Fragment  - 图1

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

    View controllers?没这么快

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

    Fragment事务

    1. int commitInternal(boolean allowStateLoss) {
    2. if (mCommitted)
    3. throw new IllegalStateException("commit already called");
    4. mCommitted = true;
    5. if (mAddToBackStack) {
    6. mIndex = mManager.allocBackStackIndex(this);
    7. } else {
    8. mIndex = -1;
    9. }
    10. mManager.enqueueAction(this, allowStateLoss);
    11. return mIndex;
    12. }
    13. }

    Fragment创建魔法

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

    1. DialogFragment dialogFragment = new DialogFragment() {
    2. @Override public Dialog onCreateDialog(Bundle savedInstanceState) { ... }
    3. };
    4. dialogFragment.show(fragmentManager, tag);

    然而,当恢复activity实例的状态时,fragment manager可能会尝试通过反射机制重新创建这个fragment类的实例。由于这是一个匿名内部类,它的构造函数有一个隐藏的参数,持有外部类的引用。

    1. android.support.v4.app.Fragment$InstantiationException:
    2. Unable to instantiate fragment com.squareup.MyActivity$1:
    3. make sure class name exists, is public, and has an empty
    4. constructor that is public

    Fragments的经验教训

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

    • 单Activity界面:没有必要为每个界面使用一个activity。我们可以分割我们的app为解耦的组件然后根据需要进行组合。这使得动画和生命周期变得简单。我们可以把组件代码分割成视图代码和控制器代码。

    • 返回栈不是activity特性的概念;我们可以在一个activity中实现返回栈。

    • 没有必要使用新的API;我们所需要的一切都是早就存在的:activities,views和layout inflaters。

    Fragments

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

    HeadlinesFragment是一个简单的列表:

    1. public class HeadlinesFragment extends ListFragment {
    2. OnHeadlineSelectedListener mCallback;
    3. public interface OnHeadlineSelectedListener {
    4. void onArticleSelected(int position);
    5. }
    6. @Override
    7. public void onCreate(Bundle savedInstanceState) {
    8. super.onCreate(savedInstanceState);
    9. setListAdapter(
    10. new ArrayAdapter<String>(getActivity(),
    11. R.layout.fragment_list,
    12. Ipsum.Headlines));
    13. }
    14. @Override
    15. public void onAttach(Activity activity) {
    16. super.onAttach(activity);
    17. mCallback = (OnHeadlineSelectedListener) activity;
    18. }
    19. @Override
    20. public void onListItemClick(ListView l, View v, int position, long id) {
    21. mCallback.onArticleSelected(position);
    22. getListView().setItemChecked(position, true);
    23. }
    24. }

    接下来比较有趣:ListFragmentActivity到底需要处理相同界面上的细节还是不需要呢?

    自定义views

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

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

    1. public interface Container {
    2. void showItem(String item);
    3. boolean onBackPressed();

    Activity假设总会存在一个container,并把工作委托给它。

    1. public class MainActivity extends Activity {
    2. private Container container;
    3. @Override protected void onCreate(Bundle savedInstanceState) {
    4. super.onCreate(savedInstanceState);
    5. setContentView(R.layout.main_activity);
    6. container = (Container) findViewById(R.id.container);
    7. }
    8. public Container getContainer() {
    9. return container;
    10. }
    11. @Override public void onBackPressed() {
    12. if (!handled) {
    13. finish();
    14. }
    15. }
    16. }

    列表的代码也类似如下:

    1. public class ItemListView extends ListView {
    2. public ItemListView(Context context, AttributeSet attrs) {
    3. super(context, attrs);
    4. }
    5. @Override protected void onFinishInflate() {
    6. super.onFinishInflate();
    7. final MyListAdapter adapter = new MyListAdapter();
    8. setAdapter(adapter);
    9. setOnItemClickListener(new OnItemClickListener() {
    10. @Override public void onItemClick(AdapterView<?> parent, View view,
    11. int position, long id) {
    12. String item = adapter.getItem(position);
    13. MainActivity activity = (MainActivity) getContext();
    14. Container container = activity.getContainer();
    15. container.showItem(item);
    16. }
    17. });
    18. }
    19. }

    接着任务是:基于资源限定符加载不同的XML布局文件。

    res/layout/main_activity.xml:

    1. <com.squareup.view.SinglePaneContainer
    2. xmlns:android="http://schemas.android.com/apk/res/android"
    3. android:layout_width="match_parent"
    4. android:layout_height="match_parent"
    5. android:id="@+id/container"
    6. >
    7. <com.squareup.view.ItemListView
    8. android:layout_width="match_parent"
    9. android:layout_height="match_parent"
    10. />
    11. </com.squareup.view.SinglePaneContainer>

    res/layout-land/main_activity.xml:

    下面是这些containers的简单实现:

    1. public class DualPaneContainer extends LinearLayout implements Container {
    2. private MyDetailView detailView;
    3. public DualPaneContainer(Context context, AttributeSet attrs) {
    4. super(context, attrs);
    5. }
    6. @Override protected void onFinishInflate() {
    7. super.onFinishInflate();
    8. detailView = (MyDetailView) getChildAt(1);
    9. }
    10. public boolean onBackPressed() {
    11. return false;
    12. }
    13. @Override public void showItem(String item) {
    14. detailView.setItem(item);
    15. }
    16. }
    1. public class SinglePaneContainer extends FrameLayout implements Container {
    2. private ItemListView listView;
    3. public SinglePaneContainer(Context context, AttributeSet attrs) {
    4. super(context, attrs);
    5. }
    6. @Override protected void onFinishInflate() {
    7. super.onFinishInflate();
    8. listView = (ItemListView) getChildAt(0);
    9. }
    10. if (!listViewAttached()) {
    11. removeViewAt(0);
    12. addView(listView);
    13. return true;
    14. }
    15. return false;
    16. }
    17. @Override public void showItem(String item) {
    18. if (listViewAttached()) {
    19. removeViewAt(0);
    20. View.inflate(getContext(), R.layout.detail, this);
    21. }
    22. MyDetailView detailView = (MyDetailView) getChildAt(0);
    23. detailView.setItem(item);
    24. }
    25. private boolean listViewAttached() {
    26. return listView.getParent() != null;
    27. }
    28. }

    Views & presenters

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

    1. public class MyDetailView extends LinearLayout {
    2. TextView textView;
    3. DetailPresenter presenter;
    4. public MyDetailView(Context context, AttributeSet attrs) {
    5. super(context, attrs);
    6. presenter = new DetailPresenter();
    7. }
    8. @Override protected void onFinishInflate() {
    9. super.onFinishInflate();
    10. presenter.setView(this);
    11. textView = (TextView) findViewById(R.id.text);
    12. findViewById(R.id.button).setOnClickListener(new OnClickListener() {
    13. @Override public void onClick(View v) {
    14. presenter.buttonClicked();
    15. }
    16. });
    17. }
    18. public void setItem(String item) {
    19. textView.setText(item);
    20. }
    21. }

    让我们看一下从Square Register中抽取的代码,编辑账号信息的界面如下:

    presenter在高层级操作view:

    1. class EditDiscountPresenter {
    2. // ...
    3. public void saveDiscount() {
    4. EditDiscountView view = getView();
    5. String name = view.getName();
    6. if (isBlank(name)) {
    7. view.showNameRequiredWarning();
    8. return;
    9. }
    10. if (isNewDiscount()) {
    11. createNewDiscountAsync(name, view.getAmount(), view.isPercentage());
    12. } else {
    13. updateNewDiscountAsync(discountId, name, view.getAmount(),
    14. view.isPercentage());
    15. }
    16. close();
    17. }
    18. }

    为这个presenter编写测试是轻而易举的事:

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

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

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

    1. public class DetailFragment extends Fragment {
    2. @Override public View onCreateView(LayoutInflater inflater,
    3. ViewGroup container, Bundle savedInstanceState) {
    4. return inflater.inflate(R.layout.my_detail_view, container, false);
    5. }

    到这里,你可以消除fragment了。

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

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

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

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

    • 它为被注入组件提供简单的生命周期回调。这使你可以编写在屏幕旋转时不会被销毁的presenters单例,而且可以保存状态到bundle中从而在进程死亡中存活下来。

    • 它为你管理Dagger子图,并帮你把它绑定到activity的生命周期中。这让你有效的实现范围的概念:一个views生成的时候,它的presenter和依赖会作为子图创建;当views销毁的时候,你可以很容易的销毁这个范围,并让垃圾回收起作用。

    结论

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

    • 我们只需要views来构建响应式的UI,一个返回栈和屏幕转场功能。