近期我在 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 empty
constructor 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);
} @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 必须控制 list 是否处于同一个页面中。

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

自己定义 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 KoutsogiorgasRay 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. android openGL ES2 一切从绘制纹理開始

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

  2. 从0開始学习 GitHub 系列之「07.GitHub 常见的几种操作」

    之前写了一个 GitHub 系列,反响非常不错,突然发现居然还落下点东西没写,前段时间 GitHub 也改版了,借此机会补充下. 我们都说开源社区最大的魅力是人人多能够參与进去,发挥众人的力量,让一个 ...

  3. 兼容,原来在这里就已经開始--------Day34

    看了两天,算是将w3cschool的javascript部分浏览了一遍.在脑海中大约有了一点概念,也才真切体会到:一入江湖深似海.欲穷此路难上难啊,至少如今看起来是遥遥无期.太多不懂, 太多茫然,只是 ...

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

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

  5. [WebGL入门]二,開始WebGL之前,先了解一下canvas

    年2月)HTML5依旧处于草案阶段. HTML5支持网页端的多媒体功能和画布功能,追加了非常多全新的更合理的Tag标签.各个浏览器也都在逐渐的完好这些新的特性. Canvas对象表示一个 HTML画布 ...

  6. 開始Unity3D的学习之旅

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

  7. csdn博客又開始更新了

    csdn博客经过两年多的沉寂又開始更新了,这两年偶尔在http://www.cnblogs.com/JerryWang1991/ 写一些博文,写的也比較少,如今工作一年多了,又開始回到csdn上更新. ...

  8. 開始折腾cocos2d-x,使用批处理来创建项目

    開始抽出时间学习cocos2d-x了.尽管C和C++还都不咋地.只是在开发中学习记忆也许会更深吧. so决定从今天開始正式学习的用自己的空暇时间折腾它了.正好这个五一没什么事.昨天搭建了一下开发环境. ...

  9. 从零開始开发Android版2048 (一)初始化界面

    自学Android一个月多了,一直在工作之余零零散散地看一些东西.感觉经常使用的东西都有些了解了,可是一開始写代码总会出各种奇葩的问题.感觉还是代码写得太少.这样继续杂乱地学习下去进度也太慢了,并且学 ...

随机推荐

  1. BZOJ 1613: [Usaco2007 Jan]Running贝茜的晨练计划

    题目 1613: [Usaco2007 Jan]Running贝茜的晨练计划 Time Limit: 5 Sec  Memory Limit: 64 MB Description 奶牛们打算通过锻炼来 ...

  2. Linux基本配置和管理 1---- Linux网络基本配置

    1 IP编址 1 IP编址是一个双层的编址方案,一个IP编址标识一个主机(或一个网卡接口) 2 现在应用最为广泛的是ipv4,已经开始逐步香ipv6切换 3 ipv4地址为32位,ipv6为128位 ...

  3. Part Acquisition(spfa输出路径)

    Part Acquisition Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 4080   Accepted: 1742 ...

  4. c#中的委托使用(方法的调用, 和类的实话)

    方法的调用 delegate int test1(int a); class Program { static int num = 10; static void Main(string[] args ...

  5. D.6661 - Equal Sum Sets

    Equal Sum Sets Let us consider sets of positive integers less than or equal to n. Note that all elem ...

  6. HDU OJ 5326 Work( 2015多校联合训练第3场) 并查集

    题目连接:戳ME #include <iostream> #include <cstdio> #include <cstring> using namespace ...

  7. NHibernate变的简单

    前言 这篇文章出自于我尝试学习使用Nhiberbnate的挫败感.我发现好像Nhibernate全部的介绍材料不是很模糊就是太详细.我所需要的就是一个简单直接的教程,能让我尽快对NHibernate熟 ...

  8. JAVA平台上的网络爬虫脚本语言 CrawlScript

    JAVA平台上的网络爬虫脚本语言 CrawlScript 网络爬虫即自动获取网页信息的一种程序,有很多JAVA.C++的网络爬虫类库,但是在这些类库的基础上开发十分繁琐,需要大量的代码才可以完成一 个 ...

  9. eclipse处理长字符串拼接快捷方法类

    情景: 你在后台写sql文访问数据库时是不是要这样写 String sql="select a," +"b," +"c " +"f ...

  10. 通过sharedpreference两个程序共享数据

    一.整体工程图      二.SharePreferenceWriteActivity.java package org.ourunix.android.sharepreferencewrite; i ...