我为什么主张反对使用Android Fragment
2018-12-30 20:45 阅读(316)

由于最近在整理Fragment相关的资料,所以把这篇文章翻出来,转到了简书。
虽然已经是几年前的文章了,但是有些思想还是值得学习

原文地址:
https://corner.squareup.com/2014/10/advocating-against-android-fragments.html

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

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

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

关于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类的实例。由于这是一个匿名内部类,它的构造函数有一个隐藏的参数,持有外部类的引用。

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

Fragments的经验教训

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

响应式UI:fragments vs 自定义views

Fragments

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

HeadlinesFragment是一个简单的列表:

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,也可以处理返回键。

public interface Container {
 void showItem(String item);

 boolean onBackPressed();
}

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

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();
   }
 }
}

列表的代码也类似如下:

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:

<com.squareup.view.SinglePaneContainer
   xmlns: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:

<com.squareup.view.DualPaneContainer
   xmlns: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:l
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);
 }
}
 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;
 }
}
yout_height="match_parent"
     android:layout_weight="0.8"
     />
</com.squareup.view.DualPaneContainer>

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

抽象出这些container并以这种方式来构建app并不难-我们不仅不需要fragments,而且代码将是易于理解的。

Views & presenters

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

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:

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编写测试是轻而易举的事:

@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关联起来。

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 Koutsogiorgas 和 Ray Ryan的杰出工作。

Dagger&Mortar如何呢?

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

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

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

结论

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