對於Fragment的一些理解
前言
Fragment想必大家不陌生吧,在日常開發中,對於Fragment的使用也很頻繁,現在主流的APP中,基本的架構也都是一個主頁,然後每個Tab項用Fragment做佈局,不同選項做切換,使用起來也方便。但是否對它有足夠的認識嗎,谷歌推薦用Fragment來代替Activity,但又沒有明確說為什麼要用Fragment來代替Activity,這裡就引發爭議了,那到底是要不要用,是否使用Fragment完全替換Activity真的比常規開發模式更好嗎?如果要用的話,那需要了解為何要使用Fragment,Fragment是什麼,它的生命週期如何,如何使用,通訊又是怎樣,有什麼缺點嗎?帶著這些問題,我們一一去解讀。
目錄
- Fragment為何要用
- Fragment是什麼
- Fragment生命週期
- Fragment怎麼用
- Fragment通訊
- Fragment是否很完美
- 小結
- 參考地址
Fragment為何要用
Fragment是Android 3.0 (Honeycomb)被引入的。主要目的是為了給大螢幕(如平板電腦)上更加動態和靈活的UI設計提供支援。由於平板電腦的螢幕比手機的螢幕大很多,因此可用於組合和交換的UI元件的空間更大,利用Fragment實現此類設計的時,就無需管理對檢視層次結構的複雜更改。
通過將 Activity 佈局分成片段,您可以在執行時修改 Activity 的外觀,並在由 Activity 管理的返回棧中保留這些更改。如果僅僅只有Activity佈局,那是不夠的,不僅在手機上有一套佈局,同時在平板上還需要設計一套佈局,那樣維護起來也麻煩,程式碼上也有一定的冗餘,對於APP包的大小也有一定的壓力。Fragment的優勢是佈局在不同裝置上的適配。
比如:
從圖中我們可以看到,在平板中,一個Activity A包含了兩個Fragment,分別是Fragment A和Fragment B,但在手機中呢,就需要兩個Activity,分別是Activity A包含Fragment A和Activity B包含Fragment B。同時每個Fragment都具有自己的一套生命週期回撥方法,並各自處理自己的使用者輸入事件。 因此,在平板中使用一個Activity 就可以了,左側是列表,右邊是內容詳情。
除此之外,使用Fragment還有這麼幾個方面優勢:
- 程式碼複用。特別適用於模組化的開發,因為一個Fragment可以被多個Activity巢狀,有個共同的業務模組就可以複用了,是模組化UI的良好元件。
- Activity用來管理Fragment。Fragment的生命週期是寄託到Activity中,Fragment可以被Attach新增和Detach釋放。
- 可控性。Fragment可以像普通物件那樣自由的建立和控制,傳遞引數更加容易和方便,也不用處理系統相關的事情,顯示方式、替換、不管是整體還是部分,都可以做到相應的更改。
- Fragments是view controllers,它們包含可測試的,解耦的業務邏輯塊,由於Fragments是構建在views之上的,而views很容易實現動畫效果,因此Fragments在螢幕切換時具有更好的控制。
Fragment是什麼
說了半天的Fragment,也看到這麼多次Fragment這個名詞出現,那麼Fragment到底是什麼東東呢?定義又是如何?
Fragment也可以叫為“片段”,但我覺得“片段”中文叫法有點生硬,還是保持叫Fragment比較好,它可以表示Activity中的行為或使用者介面部分。我們可以在一個Activity中用多個Fragment組合來構建多窗格的UI,以及在多個Activity中重複使用某個Fragment。它有自己的生命週期,能接受自己的輸入,並且可以在 Activity 執行時新增或刪除Fragment(有點像在不同 Activity 中重複使用的“子 Activity”)。
簡單來說,Fragment其實可以理解為一個具有自己生命週期的控制元件,只不過這個控制元件又有點特殊,它有自己的處理輸入事件的能力,有自己的生命週期,又必須依賴於Activity,能互相通訊和託管。
Fragment生命週期
如圖:
這張圖是Fragment生命週期和Activity生命週期對比圖,可以看到兩者還是有很多相似的地方,比如都有onCreate(),onStart(),onPause(),onDestroy()等等,因為Fragment是被託管到Activity中的,所以多了兩個onAttach()和onDetach()。這裡講講與Activity生命週期不一樣的方法。
onAttach()
Fragment和Activity建立關聯的時候呼叫,被附加到Activity中去。
onCreate()
系統會在建立Fragment時呼叫此方法。可以初始化一段資原始檔等等。
onCreateView()
系統會在Fragment首次繪製其使用者介面時呼叫此方法。 要想為Fragment繪製 UI,從該方法中返回的 View 必須是Fragment佈局的根檢視。如果Fragment未提供 UI,您可以返回 null。
onViewCreated()
在Fragment被繪製後,呼叫此方法,可以初始化控制元件資源。
onActivityCreated()
當onCreate(),onCreateView(),onViewCreated()方法執行完後呼叫,也就是Activity被渲染繪製出來後。
onPause()
系統將此方法作為使用者離開Fragment的第一個訊號(但並不總是意味著此Fragment會被銷燬)進行呼叫。 通常可以在此方法內確認在當前使用者會話結束後仍然有效的任何更改(因為使用者可能不會返回)。
onDestroyView()
Fragment中的佈局被移除時呼叫。
onDetach()
Fragment和Activity解除關聯的時候呼叫。
但需要注一點是:除了onCreateView,其他的所有方法如果你重寫了,必須呼叫父類對於該方法的實現。
還有一般在啟動Fragment的時候,它的生命週期就會執行這幾個方法。
Fragment怎麼用
前面介紹了半天,不耐煩的人會說,這麼多廢話,也不見的到底是如何使用,畢竟我們是開發者,需要的使用方式,那麼現在就來說說用法如何吧。兩種方式:靜態用法和動態用法。
靜態用法
1、繼承Fragment,重寫onCreateView決定Fragemnt的佈局
2、在Activity中宣告此Fragment,就當和普通的View一樣
首先是佈局檔案:fragment1.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#00ff00" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This is fragment 1"
android:textColor="#000000"
android:textSize="25sp" />
</LinearLayout>
可以看到,這個佈局檔案非常簡單,只有一個LinearLayout,裡面加入了一個TextView。我們再新建一個fragment2.xml :
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffff00" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This is fragment 2"
android:textColor="#000000"
android:textSize="25sp" />
</LinearLayout>
然後新建一個類Fragment1,這個類是繼承自Fragment的:
public class Fragment1 extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment1, container, false);
}
}
可以看到,在onCreateView()方法中載入了fragment1.xml的佈局。同樣fragment2.xml也是一樣的做法,新建一個Fragment2類:
public class Fragment2 extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment2, container, false);
}
}
然後開啟或新建activity_main.xml作為主Activity的佈局檔案,在裡面加入兩個Fragment的引用,使用android:name字首來引用具體的Fragment:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false" >
<fragment
android:id="@+id/fragment1"
android:name="com.example.fragmentdemo.Fragment1"
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1" />
<fragment
android:id="@+id/fragment2"
android:name="com.example.fragmentdemo.Fragment2"
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>
最後新建MainActivity作為程式的主Activity,裡面的程式碼非常簡單,都是自動生成的:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
現在我們來執行一次程式,就會看到,一個Activity很融洽地包含了兩個Fragment,這兩個Fragment平分了整個螢幕,效果圖如下:
動態用法
上面僅僅是Fragment簡單用法,它真正強大部分是在動態地新增到Activity中,那麼動態用法又是如何呢?
還是在靜態用法程式碼的基礎上修改,開啟activity_main.xml,將其中對Fragment的引用都刪除,只保留最外層的LinearLayout,並給它新增一個id,因為我們要動態新增Fragment,不用在XML裡添加了,刪除後代碼如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false" >
</LinearLayout>
然後開啟MainActivity,修改其中的程式碼如下所示:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Display display = getWindowManager().getDefaultDisplay();
if (display.getWidth() > display.getHeight()) {
Fragment1 fragment1 = new Fragment1();
getFragmentManager().beginTransaction().replace(R.id.main_layout, fragment1).commit();
} else {
Fragment2 fragment2 = new Fragment2();
getFragmentManager().beginTransaction().replace(R.id.main_layout, fragment2).commit();
}
}
}
看到了沒,首先,我們要獲取螢幕的寬度和高度,然後進行判斷,如果螢幕寬度大於高度就新增fragment1,如果高度大於寬度就新增fragment2。動態新增Fragment主要分為4步:
1.獲取到FragmentManager,在Activity中可以直接通過getFragmentManager得到。
2.開啟一個事務,通過呼叫beginTransaction方法開啟。
3.向容器內加入Fragment,一般使用replace方法實現,需要傳入容器的id和Fragment的例項。
4.提交事務,呼叫commit方法提交。
現在執行一下程式,效果如下圖所示:
要想管理 Activity 中的片段,需要使用 FragmentManager。要想獲取它,需要 Activity 呼叫 getFragmentManager()。
使用 FragmentManager 執行的操作包括:
- 通過 findFragmentById()(對於在 Activity 佈局中提供 UI 的片段)或 findFragmentByTag()(對於提供或不提供 UI 的片段)獲取 Activity 中存在的片段
- 通過 popBackStack()將片段從返回棧中彈出
- 通過 addOnBackStackChangedListener() 註冊一個偵聽返回棧變化的偵聽器
也可以使用 FragmentManager 開啟一個 FragmentTransaction,通過它來執行某些事務,如新增和刪除片段。
Fragment通訊
儘管 Fragment 是作為獨立於 Activity的物件實現,並且可在多個 Activity 內使用,但Fragment 的給定例項會直接繫結到包含它的 Activity。具體地說,Fragment 可以通過 getActivity() 訪問 Activity例項,並輕鬆地執行在 Activity 佈局中查詢檢視等任務。如:
View listView = getActivity().findViewById(R.id.list);
同樣地,Activity 也可以使用 findFragmentById() 或 findFragmentByTag(),通過從 FragmentManager 獲取對 Fragment 的引用來呼叫Fragment中的方法。例如:
ExampleFragment fragment = (ExampleFragment) getFragmentManager().findFragmentById(R.id.example_fragment);
建立對 Activity 的事件回撥
在某些情況下,可能需要通過與 Activity 共享事件。執行此操作的一個好方法是,在Fragment 內定義一個回撥介面,並要求宿主 Activity 實現它。 當 Activity 通過該介面收到回撥時,可以根據需要與佈局中的其他Fragment共享這些資訊。
例如,如果一個新聞應用的 Activity 有兩個Fragment ,一個用於顯示文章列表(Fragment A),另一個用於顯示文章(Fragment B)—,那麼Fragment A必須在列表項被選定後告知 Activity,以便它告知Fragment B 顯示該文章。 在本例中,OnArticleSelectedListener 介面在片段 A 內宣告:
public static class FragmentA extends ListFragment {
public interface OnArticleSelectedListener {
public void onArticleSelected(Uri articleUri);
}
}
然後,該Fragment的宿主 Activity 會實現 OnArticleSelectedListener 介面並替代 onArticleSelected(),將來自Fragment A 的事件通知Fragment B。為確保宿主 Activity 實現此介面,Fragment A 的 onAttach() 回撥方法(系統在向 Activity 新增Fragment時呼叫的方法)會通過轉換傳遞到 onAttach() 中的 Activity 來例項化 OnArticleSelectedListener 的例項:
public static class FragmentA extends ListFragment {
OnArticleSelectedListener mListener;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mListener = (OnArticleSelectedListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + " must implement OnArticleSelectedListener");
}
}
}
如果 Activity 未實現介面,則片段會引發 ClassCastException。實現時,mListener 成員會保留對 Activity 的 OnArticleSelectedListener 實現的引用,以便Fragment A 可以通過呼叫 OnArticleSelectedListener 介面定義的方法與 Activity 共享事件。例如,如果Fragment A 是 ListFragment 的一個擴充套件,則使用者每次點選列表項時,系統都會呼叫Fragment中的 onListItemClick(),然後該方法會呼叫 onArticleSelected() 以與 Activity 共享事件:
public static class FragmentA extends ListFragment {
OnArticleSelectedListener mListener;
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
Uri noteUri = ContentUris.withAppendedId(ArticleColumns.CONTENT_URI, id);
mListener.onArticleSelected(noteUri);
}
}
Fragment是否很完美
因為Fragment是由FragmentManager來管理,每一個Activity有一個FragmentManager,管理著一個Fragment的棧,Activity是系統級別的,由系統來管理ActivityManager,棧也是系統範圍的。而Fragment則是每個Activity範圍內的,所以在使用Fragment的時候也有幾點要注意。
- 同一個Activity中,只能有一個ID或TAG標識的Fragment例項。
這很容易理解,同一個範圍內,有標識的例項肯定是要唯一才行(否則還要標識幹嘛)這個在佈局中經常犯錯,在佈局中寫Fragment最好不要加ID或者TAG,否則很容易出現不允許建立的錯誤。我的原則是如果放在佈局中,就不要加ID和TAG,如果需要ID和TAG就全用程式碼控制。建立新例項前先到FragmentManager中查詢一番,這也正是有標識的意義所在。 - 一個Activity中有一個Fragment池,例項不一定會被銷燬,可能會儲存在池中。
這個跟第一點差不多。就好比系統會快取Activity的例項一樣,FragmentManager也會快取Fragment例項,以方便和加速再次顯示。 - FragmentManager的作用範圍是整個Activity,所以,某一個佈局ID,不能重複被Fragment替換。
通常顯示Fragment有二種方式,一種是層疊到某個佈局上,或者把某個佈局上面的Fragment替換掉,但是這個佈局不能出現二次,比如佈局A中有ID為id的區域,要顯示為Fragment,此佈局A,只能在一個Activity中顯示一個,否則第二個id區域不能被Fragment成功替換。因為雖有二個ID佈局的例項,但ID是相同的,對FragmentManager來說是一樣的,它會認為只有一個,因為它看的是佈局的ID,而不是佈局的例項。 - Fragment的生命週期反應Activity的生命週期。
Fragment在顯示和退出時會走一遍完整的生命週期。此外,正在顯示時,就跟Activity的一樣,Activity被onPause,裡面的Fragment就onPause,以此類推,由此帶來的問題就是,比如你在onStart()裡面做了一些事情,那麼,當宿主Activity被擋住,又出現時(比如接了個電話),Fragment的onStart也會被高到,所以你要想到,這些生命週期不單單在顯示和退出時會走到。 - Fragment的可見性。
這個問題出現在有Fragment棧的時候,也就是說每個Fragment不知道自己是否真的對使用者可見。比如現在是Fragment A,又在其上面顯示了Fragment B,當B顯示後,A並不知道自己上面還有一個,也不知道自己對使用者不可見了,同樣再有一個C,B也不知。C退出後,B依然不知自己已在棧頂,對使用者可見,B退後,A也不知。也就是說Fragment顯示或者退出,棧裡的其他Fragment無法感知。這點就不如Activity,a被b蓋住後,a會走到onStop(),同樣c顯示後,b也能通過onStop()感知。Fragment可以從FragmentManager監聽BackStackState的變化,但它只告訴你Stack變了,不告訴你是多了,還是少,還有你處的位置。有一個解決方案就是,記錄頁面的Path深度,再跟Fragment所在的Stack深度來比較,如果一致,那麼這個Fragment就在棧頂。因為每個頁面的Path深度是固定的,而Stack深度是不變化的,所以這個能準確的判斷Fragment是否對使用者可見,當然,這個僅針對整個頁面有效,對於佈局中的一個區域是無效的。 - Fragment的事件傳遞。
對於層疊的Fragment,其實就相當於在一個FrameLayout裡面加上一堆的View,所以,如果處於頂層的Fragment沒處理點選事件,那麼事件就會向下層傳遞,直到事件被處理。比如有二個Fragment A和B,B在A上面,B只有TextView且沒處理事件,那麼點選B時,會發現A裡的View處理了事件。這個對於Activity也不會發生,因為事件不能跨窗體傳播,上面的Activity沒處理事件,也不會傳給下面的Activity,即使它可見。解決之法,就是讓上面的Fragment的根佈局吃掉事件,為每個根ViewGroup新增onClick=“true”。 - 與第三方Activity互動。與第三方互動,仍要採用Android的標準startActivityForResult()和onActivityResult()這二個方法來進行。但對於Fragment有些事情需要注意,Fragment也有這二個方法,但是為了能正確的讓Fragment收到onActivityResult(),需要:
- 宿主Activity要實現一個空的onActivityResult(),裡面呼叫super.onActivityResult()
- 呼叫Fragment#startActivityForResult()而不是用Activity的 當然,也可以直接使用Activity的startActivityForResult(),那樣的話,就只能在宿主Activity裡處理返回的結果了。
小結
在用法的程式碼部分參考郭神的部落格,感覺郭神在程式碼講解部分通俗易懂,看起來也方便。總之,在使用Fragment也有一些注意事項,不是那麼完美的,雖然谷歌推薦我們用Fragment來代替Activity來使用,我們也確實這做了,現在基本主流的APP也都是少量Activity+很多Fragment,但也需要避免有些坑慎入。
參考地址
1,https://developer.android.com/guide/components/fragments.html
2,http://blog.csdn.net/guolin_blog/article/details/8881711
3,http://toughcoder.net/blog/2014/10/22/effective-android-ui-architecture