在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:
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 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是一个简单的列表:
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:layout_height="match_parent" android:layout_weight="0.8" /> </com.squareup.view.DualPaneContainer>
下面是这些containers的简单实现:
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); } }
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如下所示:
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之上,它具有两大优点:
它为被注入组件提供简单的生命周期回调。这使你可以编写在屏幕旋转时不会被销毁的presenters单例,而且可以保存状态到bundle中从而在进程死亡中存活下来。
它为你管理Dagger子图,并帮你把它绑定到activity的生命周期中。这让你有效的实现范围的概念:一个views生成的时候,它的presenter和依赖会作为子图创建;当views销毁的时候,你可以很容易的销毁这个范围,并让垃圾回收起作用。
结论
我们曾经大量的使用fragments,但最终改变了我们的想法:
我们很多疑难的crashes都和fragment生命周期相关;
我们只需要views来构建响应式的UI,一个返回栈和屏幕转场功能。
共同学习,写下你的评论
评论加载中...
作者其他优质文章