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

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

文中的fragments应该是fragment,在英文中为复数,但是中文没有这个习惯。

原文如下:

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

最近我在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找到了更好的选择。

关于fragments你所不知道的

复杂的生命周期

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

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

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

难以调试

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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接收到多个点击事件或者配置发生变化时,将处于不可知的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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创建。下面代码似乎很合理:

1
2
3
4
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到底需要处理相同界面上的细节还是不需要呢?

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

Dagger&Mortar如何呢?

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

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

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

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

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

结论

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

  • 我们很多疑难的crashes都和fragment生命周期相关;

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值