Advertisement

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

阅读量:

最近我参与了 Droidcon Paris 的一个技术相关的演讲活动,并在会上分享了Square 使用 Fragment 进行开发时遇到的各种问题。 在本次演讲中, 我向大家展示了Square 在实际开发过程中使用Fragment 遇到的各种挑战。 此外, 还有其他Android 开发者是如何规避在项目中使用Fragment以提高开发效率的方法。

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

  • 那时候,
    尽管我们非常希望能够将应用在平板设备上得到使用,
    但我们未能成功地为此提供相应的平台支持。
    然而,
    Fragment能够有效地帮助我们实现这一目标,
    从而创建出一个响应式 UI 界面。

Fragment 属于视图控制器技术体系中的重要组成部分,在该系统中采用模块化设计方法来实现对一个高度依赖前后文交互的业务流程进行拆分。这种拆分不仅确保了各个独立功能模块之间的独立性,还通过相应的机制使得拆分后的各个独立功能模块得以分别测试。

Fragment 的 API 支持回退栈管理(如通过实例展示某个 Activity 内部的 Activity 栈的具体操作)

由于 Fragment 位于视图层次结构的顶端,在为 View 设置动画操作上无需复杂操作即可完成;这不仅简化了相关操作流程,并且有助于提升整体用户体验。

参考Google的指导方针, 我们倾向于采用Fragment, 同时, 我们这些开发者都期望编写出符合行业标准的代码。

自2011年以后,在Square开发过程中我们发现了相较于使用Fragment更为有效的方法

关于 Fragment 你不知道的事

The lolcycle

在Android系统中,Context就像一个万能工具,因为它囊括了丰富的信息与操作,仿佛无所不能.它为Android系统提供了全面的支持,类似于万能工具.然而,将这样一个"神"赋予生命属性却让它拥有生命周期,这种设定本身就带有一种讽刺意味.虽然Fragment并不是像Context那样万能的对象,但它为了能够完成类似于Activity的各种功能,不得不使自身的功能体系变得异常复杂.

由 Steve Pomeroy 制作了 Fragment 的完整生命周期流程图 ,我相信任何人看到这张图都会感到不满。

此幅图片由Steve Pomeroy创作,在删除了Activity生命周期的基础上发布,请确保发布此图片需遵循CC BY-SA 4.0授权

每个 Fragment 的生命周期让人感到棘手,该如何使用这些回调方法?它们是在同步调用的情况下进行处理的吗?或者是一次性全部处理的情况?还有其他情况吗……?

难于调试

当你的应用发生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必须创建、绑定以及配置View对象,并且这些View对象之间存在复杂的关联关系,在这种情况下可以看出,在View类代码中所封装的业务逻辑并未实现与其他组件的有效分离。正是由于这一原因,在为Fragment设计测试单元时会面临较大的挑战

Fragment transactions

该 Fragment 提供了一系列 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 创建,请参考以下代码 snippet 看是否有明显错误

Fragment 实例可通过 Fragment Manager 创建,请参考以下代码 snippet 看是否有明显错误

复制代码
    DialogFragment dialogFragment = new DialogFragment() {
      @Override public Dialog onCreateDialog(Bundle savedInstanceState) { ... }
    };
    dialogFragment.show(fragmentManager, tag);

然而,在存储 Activity 实例的状态时,在反射机制下生成新的实例可能是 Fragment Manager 的行为。由于它是一个匿名内部类,在其隐藏构造函数中接受一个外部实例作为参数。如果您熟悉相关知识或者阅读过这篇博文的话就会知道,“使用外部引用可能导致内存泄漏”的问题。

复制代码
    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 在之前的讨论中提到了一些不足之处,但它同时也为我们提供了构建代码架构的重要思路的基础:

  • 独立的 Activity 接口:实际上并不需要每个页面都单独创建一个 Activity, 我们可以将应用拆分成许多解耦开的视图组件, 按照实际需求进行组装以形成所需界面. 这样不仅能够简化生命周期管理以及动画设置, 因为这样我们还可以进一步划分各个视图组件为 view 和 controller 组件.

*回退栈不属于Activity这一领域;这也就表示你可以在Activity内部实施回退栈。

  • 不需要添加新的 API,我们需要的只是 Activity,View 和 LayoutInflater。

响应式 UI:Fragment VS Custom View

Fragment

建议您先访问其中一个 范例,界面上展示了一个列表。

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;同样能够构建出易于理解的代码

View 和 Presenter

在实际应用中,默认的View往往难以满足需求;为了更好地管理业务流程, 我们希望通过将核心业务逻辑分离出来, 由特定的Controller来处理相关逻辑; 这也就是我们常说的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能够显著提升效率并降低开发成本。Ray Ryan已在相关领域分享了详尽的技术解析与实践经验可进一步了解这一解决方案。

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

别去管理那里的业务逻辑吧!你可以逐步将与View相关的代码迁移到专门定制化的View中,并移交给能够与之进行交互的对象来处理应用层面的业务逻辑。这样下来你会发现那个Fragment几乎成了无足轻重的存在——它仅仅涉及初始化CustomizedView以及与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 and mortar exhibit orthogonal relationships with fragments. 换言之, 每个变化都不会影响另一个. 采用 Dagger and mortar 可以选择是否加入 Fragments.

Dagger 可以帮助您将应用程序模块化为一个由解耦组件构成的图表。该工具考虑到各类之间的关联关系,并优化了抽取依赖操作的过程;同时实现了与之相关的一个单例对象的具体管理逻辑。

Mortar 在 Dagger 层级系统中执行操作,其核心优势体现在两个方面:

Mortar 支持注入组件并提供其生命周期回调机制,在使用时您可以实现不会因旋转而被销毁的单例 presenter;需要注意的是, 该库将当前界面元素的状态存储于Bundle中以确保数据在进程结束时不被清除.

Mortar 负责管理 Dagger 的子图,并协助将这些子图与应用程序的生命 cycle 连接起来。该功能允许你轻松地实现"领域":每当一个 View 被添加时(即当你引入新的视图),其 corresponding Presenter 和依赖会被自动创建为新的子图;而当 View 被删除时(即视图不再被引用),你可以方便地销毁该领域(领域),让回收机制处理相关的资源。/

结论

曾以为这一发现令人欢欣无比。原本期待能为我们带来诸多便利的想法也随之破灭。然而这一切不过是场虚妄之梦——原来骑着白马的 fragment 并非王子亦非唐僧者而只是 simply a beggar with a horse whose astral body flirts with our astral realm.

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

主要依赖于 View 生成响应式界面,并且能够处理回滚栈和屏幕事件。与之相比,在不使用 Fragment 的情况下也能满足实际开发的需求。

全部评论 (0)

还没有任何评论哟~