最近我在 Droidcon Paris 上進行了 一個技術相關的演講 ,我在此次演講中給你們展現了 Square 使用 Fragment 進行開發時遇到的種種問題,以及其餘 Android 開發者是怎麼避免在項目中使用 Fragment 的。 php
在 2011 年那會,因爲下面的緣由咱們決定使用 Fragment: html
在那會,雖然咱們很想讓應用能在平板設備上被使用,但咱們確實沒能爲平板提供平臺支持。而 Fragment 能幫助咱們完成這項願望,創建響應式 UI 界面。 java
Fragment 是視圖控制器,它們可以將一大塊耦合嚴重的業務邏輯模塊解耦,並使得解耦後的業務邏輯可以被測試。 android
Fragment 的 API 可以進行回退棧管理(例如,它能反射某個 Activity 內 Activity 棧的具體操做) git
由於 Fragment 處於視圖層的頂層,而爲 View 設置動畫並不麻煩,使得 Fragment 爲設置頁面切換的過渡效果提供了更好的支持。 程序員
Google 建議咱們使用 Fragment,而咱們做爲開發者都想讓本身的代碼符合標準。 github
在 2011年以後,咱們在爲 Square 進行開發的過程當中發現了比使用 Fragment 更好的方法。 架構
在 Android 中,Context 就像一個 上帝對象 ,由於在 Context 類中涵蓋了太多 Android 系統的信息和相關的操做,使得 Context 在 Android 系統中至關於一個全知全能的上帝,而 Activity 就是爲 Context 添加了生命週期的子類。不過讓上帝具備生命週期仍是有些諷刺的。雖然 Fragment 不是上帝對象,但 Fragment 爲了可以完成 Activity 中能完成的各類操做,使 Fragment 自身的生命週期變得異常複雜。 app
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 須要建立、綁定和配置 View,它們包含了許多與 View 關聯的結點,這就意味着 View 類代碼中的業務邏輯並無真正地被解耦,正是這個緣由使得咱們要爲 Fragment 實現測試單元將會變得很困難。
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 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 教給咱們許多代碼架構的思想:
獨立的 Activity 接口:實際上咱們並不須要爲每個頁面建立一個 Activity,咱們大能夠將應用切分紅許多解耦的視圖組件,按照咱們的實際需求把它們組裝成咱們想要的界面。這樣作也能簡化生命週期和動畫設置,由於咱們還能將視圖組件切分爲 view 組件和控制器組件。
回退棧不是 Activity 的特有概念,也就意味着你能在 Activity 內部實現回退棧。
不須要添加新的 API,咱們須要的只是 Activity,View 和 LayoutInflater。
咱們不妨先來看看一個 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 的代碼
首先,咱們會引入一個叫做「容器」的概念,「容器」的做用是幫助咱們展現一項內容並處理後退操做
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 在應用中很是有用,但咱們但願將業務邏輯從 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 已經寫過博客了,我就不在此贅述啦。
別理你的 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 與 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 也能知足實際開發的需求。