對于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生命周期

這張圖是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的時候,它的生命周期就會執行這幾個方法。

生命周期Log

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(),需要:

    1. 宿主Activity要實現一個空的onActivityResult(),里面調用super.onActivityResult()
    2. 調用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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容