轉載自 我爲什麼主張反對使用Android Fragment
文中的fragments應該是fragment,在英文中爲複數,但是中文沒有這個習慣。
原文如下:
原文鏈接:https://corner.squareup.com/2014/10/advocating-against-android-fragments.html
最近我在Droidcon Paris舉辦了一場技術講座,我講述了Square公司在使用Android fragments時遇到的問題,以及其他人如何避免使用fragments。
在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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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接收到多個點擊事件或者配置發生變化時,將處於不可知的狀態。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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創建。下面代碼似乎很合理:
1
2
3
4
|
DialogFragment dialogFragment = new DialogFragment() { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { ... } }; dialogFragment.show(fragmentManager, tag); |
然而,當恢復activity實例的狀態時,fragment manager可能會嘗試通過反射機制重新創建這個fragment類的實例。由於這是一個匿名內部類,它的構造函數有一個隱藏的參數,持有外部類的引用。
1
2
3
4
|
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是一個簡單的列表:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
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到底需要處理相同界面上的細節還是不需要呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
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,也可以處理返回鍵。
1
2
3
4
5
|
public interface Container { void showItem(String item); boolean onBackPressed(); } |
Activity假設總會存在一個container,並把工作委託給它。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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(); } } } |
列表的代碼也類似如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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:
1
2
3
4
5
6
7
8
9
10
11
|
<com.squareup.view.SinglePaneContainer 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
<com.squareup.view.DualPaneContainer 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的簡單實現:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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); } } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
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如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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編寫測試是輕而易舉的事:
1
2
3
4
5
6
7
|
@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關聯起來。
1
2
3
4
5
6
|
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,一個返回棧和屏幕轉場功能。