• 原文链接 : Advocating Against Android Fragments
  • 原文作者 : Pierre-Yves Ricau
  • 译文出自 : 开发技术前线 www.devtf.cn
  • 译者 : chaossss
  • 校对者: Belial
  • 状态 : 完毕

近期我在 Droidcon Paris 上进行了一个技术相关的演讲,我在这次演讲中给大家展示了 Square 使用 Fragment 进行开发时遇到的种种问题,以及其他 Android 开发人员是怎么避免在项目中使用 Fragment 的。

在 2011 年那会,由于以下的原因我们决定使用 Fragment:

  • 在那会,尽管我们非常想让应用能在平板设备上被使用。但我们确实没能为平板提供平台支持。

    而 Fragment 能帮助我们完毕这项愿望,建立响应式 UI 界面。

  • Fragment 是视图控制器。它们可以将一大块耦合严重的业务逻辑模块解耦。并使得解耦后的业务逻辑可以被測试。

  • Fragment 的 API 可以进行回退栈管理(比如,它能反射某个 Activity 内 Activity 栈的详细操作)

  • 由于 Fragment 处于视图层的顶层,而为 View 设置动画并不麻烦。使得 Fragment 为设置页面切换的过渡效果提供了更好的支持。

  • Google 建议我们使用 Fragment,而我们作为开发人员都想让自己的代码符合标准。

在 2011年之后。我们在为 Square 进行开发的过程中发现了比使用 Fragment 更好的方法。

关于 Fragment 你不知道的事

The lolcycle

在 Android 中。Context 就像一个上帝对象。由于在 Context 类中涵盖了太多 Android 系统的信息和相关的操作,使得 Context 在 Android 系统中相当于一个全知全能的上帝,而 Activity 就是为 Context 加入了生命周期的子类。

只是让上帝具有生命周期还是有些讽刺的。尽管 Fragment 不是上帝对象,但 Fragment 为了可以完毕 Activity 中能完毕的各种操作。使 Fragment 自身的生命周期变得异常复杂。

Steve Pomeroy 做了一张 Fragment 的完整生命周期图。我相信任谁看到这张图都不会好受:

这张图由 Steve Pomeroy 完毕,图中移除了 Activity 的生命周期,分享这张图须要获得 CC BY-SA 4.0 许可。

整个 Fragment 的生命周期让你非常头疼要如何使用这些回调方法,它们是同步调用的呢,还是仅仅是一次性所有调用呢,还是其他情况……?

难于调试

当你的应用出现 Bug,你得用调试工具一步一步地运行代码才干知道究竟发生了什么,虽说普通情况下这样做 Bug 都能解决。但假设你在调试的时候发现 Bug 和 FragmentManagerImpl 类存在某种联系,那么我可要好好恭喜你即将中大奖了。

由于要跟踪 FragmentManagerImpl 类内代码的运行顺序,并进行调试是非常困难的。这也使得修复应用中相关的 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;}}}
// ...
}

假设你以前须要解决应用旋转后产生一个与旋转前 UI 同样(方向发生变化)的独立的 Fragment 的需求。我想你应该懂我在说什么。(别给我提嵌套使用的 Fragment!

我想以下这张图非常好地诠释了这类代码给程序猿带来的伤害(由于版权问题我得放出这张图的出处哈:this cartoon):

在多年的深度分析中我得出结论:操蛋程度/调试耗费的时间 = 2^m,m 为 Fragment 的个数。

Fragment 是视图控制器?想太多

由于 Fragment 须要创建、绑定和配置 View。它们包括了很多与 View 关联的结点,这就意味着 View 类代码中的业务逻辑并没有真正地被解耦,正是这个原因使得我们要为 Fragment 实现測试单元将会变得非常困难。

Fragment transactions

Fragment 的 transaction 同意你运行一系列的 Fragment 操作,但不幸的是,提交 transaction 是异步操作,而且在 UI 线程的 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 emptyconstructor that is public

Fragment 教给我们的思想

尽管 Fragment 有着上面提到的缺点,但也是 Fragment 教给我们很多代码架构的思想:

  • 独立的 Activity 接口:实际上我们并不须要为每个页面创建一个 Activity。我们大可以将应用切分成很多解耦的视图组件,依照我们的实际需求把它们组装成我们想要的界面。

    这样做也能简化生命周期和动画设置。由于我们还能将视图组件切分为 view 组件和控制器组件。

  • 回退栈不是 Activity 的特有概念,也就意味着你能在 Activity 内部实现回退栈。

  • 不须要加入新的 API。我们须要的仅仅是 Activity。View 和 LayoutInflater。

响应式 UI:Fragment VS Custom View

Fragment

我们最好还是先来看看一个 Fragment 的范例。界面中显示了一个 list。

HeadlinesFragment 就是显示 List 的简单 Fragment:

public class HeadlinesFragment extends ListFragment {OnHeadlineSelectedListener mCallback;public interface OnHeadlineSelectedListener {void onArticleSelected(int position);}@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setListAdapter(new ArrayAdapter<String>(getActivity(),R.layout.fragment_list,Ipsum.Headlines));}@Overridepublic void onAttach(Activity activity) {super.onAttach(activity);mCallback = (OnHeadlineSelectedListener) activity;}@Overridepublic void onListItemClick(ListView l, View v, int position, long id) {mCallback.onArticleSelected(position);getListView().setItemChecked(position, true);}
}

如今有趣的事情来了:ListFragmentActivity 必须控制 list 是否处于同一个页面中。

public class ListFragmentActivity extends Activityimplements HeadlinesFragment.OnHeadlineSelectedListener {@Overridepublic 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();}}
}

自己定义 View

我们最好还是又一次实现一个简化版的仅仅使用了 View 的代码

首先,我们会引入一个叫作“容器”的概念。“容器”的作用是帮助我们展示一项内容并处理后退操作

public interface Container {void showItem(String item);boolean onBackPressed();
}

Acitivity 将假设始终存在容器。而且差点儿不会将业务交给容器处理。

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

要显示的 List 也仅仅是个平庸的 List。

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>

以下是这些容器类的简单实现:

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

不难想象:将容器类抽象,并用这样的的方式开发 App,不但不须要 Fragment,还能架构出easy理解的代码。

View 和 Presenter

自己定义 View 在应用中非常实用,但我们希望将业务逻辑从 View 中剥离。转交给特定的控制器处理,也就是接下来我们所说的 Presenter,引入 Presenter 能提高代码的可读性和可測试性。假设你不信的话,最好还是看看重构后的 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 注冊界面中编辑账户的页面吧!

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,就行达到目的。有关 Flow 的介绍 Ray Ryan 已经写过博客了。我就不在此赘述啦。

我把 UI 相关的代码全都写在 Fragment 里了咋办呀,在线等,急!!!

别理你的 Fragment。你就一点一点地把 View 相关的代码移到自己定义 View 里。然后把涉及到的业务逻辑交给可以与 View 进行交互的 Presenter。然后你就会发现 Fragment 沦为空壳,仅仅有一些初始化自己定义 View 和连接 View 和 Presenter 的操作:

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 了。

抛弃 Fragment 确实得花非常大的功夫,但我们已经做到了,感谢 Dimitris Koutsogiorgas 和 Ray Ryan 的伟大贡献!

Dagger 和 Mortar 是什么?

Dagger & Mortar 与 Fragment 成正交关系,换句话说。两者间各自的变化不会影响对方,使用 Dagger & Mortar 既可以用 Fragment,也可以不用 Fragment。

Dagger 能帮你将应用模块化为一张由解耦组件构成的图,它考虑了所有类间的连接关系并简化了抽取依赖的操作,并实现一个与此相关的单例对象。

Mortar 在 Dagger 的顶层进行操作。主要优势有例如以下两点:

  • Mortar 为被注入组件提供简单的生命周期回调,使你能实现不会因旋转被销毁的单例 Presenter,只是须要注意的是,Mortar 将当前界面元素的状态储存在 Bundle 中。使数据不会随进程的结束而被清除。

  • Mortar 为你管理 Dagger 的子图,并帮你将它们与 Activity 的生命周期关联在一起,这样的功能让你能有效地实现“域”:当一个 View 被加入进来,它的 Presenter 和依赖都会作为子图被创建;当 View 被移除,你能轻易地销毁“域”。并让垃圾回收机制去完毕它的工作。

结论

我们曾为 Fragment 的诞生满心欢喜。幻想着 Fragment 能为我们带来种种便利,然而这一切只是是场虚空大梦,我们最后发现骑着白马的 Fragment 既不是王子也不是唐僧,仅仅只是是人品爆发捡了仅仅白马的乞丐罢了:

  • 我们遇到的大多数难以解决的 Bug 都与 Fragment 的生命周期有关。

  • 我们仅仅须要 View 创建响应式 UI,实现回退栈以及屏幕事件的处理,不用 Fragment 也能满足实际开发的需求。

Square:从今天開始抛弃Fragment吧!相关推荐

  1. Square:从今天开始抛弃Fragment吧!

    Square:从今天开始抛弃Fragment吧! 原文链接 : Advocating Against Android Fragments 原文作者 : Pierre-Yves Ricau 译文出自 : ...

  2. android openGL ES2 一切从绘制纹理開始

    纹理.在openGL中,能够理解为载入到显卡显存中的图片.Android设备在2.2開始支持openGL ES2.0.从前都是ES1.0 和 ES1.1的版本号.简单来说,openGL ES是为了嵌入 ...

  3. 開始Unity3D的学习之旅

    前言:这个系列的文章纯属对自己学习的整理,非高手之作.但确实的记载了我作为一个没接触过3D游戏编程的大学生的心路历程.争取每周整理一次吧.之所以会開始学Unity3D,最基本的原因是由于在快放暑假的时 ...

  4. (轉貼) 友達光電第五屆【A+種子暑期實習計畫】開始辦理報名 (News)

    友達光電第五屆[A+種子暑期實習計畫]開始辦理報名 友達光電以絕佳的團隊執行力,帶領台灣光電產業進入世界級的領域! 還在就學的你/妳,想成為世界級光電產業的A+種子嗎? 把握最後的暑假加入友達的A+種 ...

  5. wxWidgets刚開始学习的人导引(3)——wxWidgets应用程序初体验

    wxWidgets刚開始学习的人导引全文件夹   PDF版及附件下载 1 前言 2 下载.安装wxWidgets 3 wxWidgets应用程序初体验 4 wxWidgets学习资料及利用方法指导 5 ...

  6. 今天開始學習silverlight了

    今天開始學習silverlight了,雖然集團里面很多的計算機安裝的系統是winpro2000,但老大說先不管,那只有學了,請大家多幫忙,發此貼留記號 转载于:https://www.cnblogs. ...

  7. # 从零開始搭建Hadoop2.7.1的分布式集群

    Hadoop 2.7.1 (2015-7-6更新),Hadoop的环境配置不是特别的复杂,可是确实有非常多细节须要注意.不然会造成很多配置错误的情况.尽量保证一次配置正确防止重复改动. 网上教程有非常 ...

  8. ExtJs自学教程(1):一切从API開始

    题 记 该系列文章不側重全方位的去介绍ExtJs的使用,仅仅是側重于解决ExtJs问题的思考方法.写的人不用长篇大论,学的人则可以自立更生.l   学习的人仅仅要有一些CSS的javascript的基 ...

  9. 2016年7月微软MVP申请開始了!

    2016年7月微软MVP申请開始了! CSDN与微软合作,长期为用户提供申请"微软最有价值专家"的平台.希望有兴趣.资历的朋友以及正在朝这个方向努力的朋友能够积极參与. 2016年 ...

最新文章

  1. python cx_oracle 有超时的设置吗_python cx_Oracle的基础使用方法(连接和增删改查)
  2. 嵌入式CNN检测网络--LCDet
  3. jQuery遇到问题的小记
  4. canal mysql多实例_canal搭建实例
  5. java求100以内的a2 b2=c2,Java语言程序设计Ⅱ-中国大学mooc-试题题目及答案
  6. PHP的PHPStorm的使用姿势
  7. 产品认知:说说产品经理的底层思维——用户思维
  8. java tomcat重启linux_Linux下tomcat重启
  9. HTML+CSS+JS实现 ❤️卡通湖面上日出动画特效❤️
  10. 【clickhouse】clickhouse 解析器
  11. AIX安装中文语言包
  12. PostgresSQL模式
  13. Go语言的素数对象编程实现及其使用
  14. matlab实现将彩色图像转换成灰色图像的方法
  15. 洛谷 P4234 LCT + 排序 + 枚举
  16. SQL分析在2020年度第一季度的购买人数,销售金额,客单价,客单件人均购买频次(时间函数、分组汇总、常用指标计算)
  17. Python-练习 42. Is-A, Has-A, 对象和类
  18. 【FPGA Verilog】如何捕获信号Posedge和Negedge?学习记录
  19. 使用 FFmpeg(bilibili视频blv合成mp4)
  20. 计算机硬件知识比赛策划,计算机硬件知识讲座活动策划案.doc

热门文章

  1. 一、 软件危机和软件工程
  2. undefined相关
  3. MybatisPlus基本查询
  4. PyTorch搭建预训练AlexNet、DenseNet、ResNet、VGG实现猫狗图片分类
  5. 正则表达式,终极使用!3个工具,搞定一切
  6. 细思极恐啊,哈哈哈哈哈哈
  7. 2015年07月16日
  8. 词霸天下---词根252【-emper- = -imper- 命 令】
  9. MIT6.828 lab1 exercise 23
  10. 以太坊:导入预售钱包,更新、备份、恢复账号