Fragment是Android開發中經常使用的組件之一,也是最容易出問題的組件,爲了更好地使用它,對此進行一個簡單的總結。java
說明: 因爲v4包中的Fragment具備更好的兼容性,且可隨時更新到指定版本,因此本文的討論僅限v4包中的Fragment。git
使用Fragment的方式有2種:github
在佈局文件中使用標籤,將其中的name屬性值設置爲須要加載的Fragment的全路徑;編程
當系統在建立Activity時,會實例化佈局中指定的Fragment,並調用它的onCreateView方法,以返回的View來替換元素。網絡
在建立Fragment的時候須要注意,Android不推薦使用自定義構造方法的方式來建立Fragment,可以使用官方推薦的方式來傳參:框架
不提倡的方式:異步
public ChatFragment(int id, String name) {
this.id = id;
this.name = name;
}
複製代碼
推薦的方式:ide
public static Fragment newInstance(int id, String name) {
Fragment fragment = new ChatFragment();
Bundle bundle = new Bundle();
bundle.putInt("id", id);
bundle.putString("name", name);
fragment.setArguments(bundle);
return fragment;
}
複製代碼
固然,也可以使用Fragment提供的靜態初始化方法來構造Fragment:模塊化
/**
* @param context 加載Fragment的Activity實例
* @param fname 須要加載的Fragment的全路徑名 [其本質是經過反射調用的]
* @param args 須要傳遞的參數
* @return
*/
public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) {
}
// 示例
Bundle bundle = new Bundle();
bundle.putInt("id", id);
bundle.putString("name", name);
Fragment.instantiate(this, "com.sxu.fragment.ChatFragment", bundle);
複製代碼
而後使用FragmentTransaction將Fragment提交給FragmentManager:佈局
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
transaction.add(R.id.container_layout, ContentFragment.newInstance(0);
transaction.commit();
複製代碼
相比Activity, Fragment的生命週期要複雜不少,使用官方的一張圖來展現:
下面對Fragment生週期中幾個核心的階段說明一下:
onAttach是Activity與Fragment關聯時被調用此,可在此方法中保存Activity實例解決getActivity爲空的問題。與之對應的是onDetach, 表示與Activity解除綁定;
與Activity同樣,Fragment也可使用onSaveInstanceState在退到後臺時保存頁面數據,但它沒有提供onRestoreInstanceState方法,因此可在onCreate中進行恢復操做;
加載Fragment View的地方,相似於Activity中的setContentView, 它返會Fragment加載的View。從圖上能夠看出, Fragment從回退棧中返回時,會今後方法開始調用,因此可將Fragment加載的View以成員變量的形式保存,當其爲空時進行再加載操做, 從而避免View的屢次加載。
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
if (mContentView == null) {
mContentView = inflater.inflate(R.layout.fragment_content_layout, container, false);
}
return mContentView;
}
複製代碼
FragmentView加載完成後onViewCreated會被調用,調用時機先於onActivityCreated,其中的參數View就表示Fragment加載的佈局,因此可在此方法中獲取佈局中的各個View。
與之對應的是onDestroyView, 它會銷燬Fragment中的View.
Activity的onCreate執行完成後被調用,Fragment中的邏輯一般在此方法中執行。
Fragment雖然有完整的生命週期,但仍然須要以Activity爲宿主來存在,因此它的生命週期與Activity生命週期有着直接的關係,如圖所示:
從圖上能夠看出,Fragment的生命週期和Activity基本保持一致。
與Activity不一樣的是,Fragment生命週期並老是在頁面可見性發生變化時變化。在如下場景中,Fragment的可見性發生變化時,不會調用生命週期的任何方法。
Fragment在顯示或隱藏時會回調onHiddenChanged, 參數hidden爲true表示Fragment被隱藏, 不然表示被顯示;
ViewPager中已加載的Fragment在切換時會調用setUserVisibleHint, 參數isVisibleToUser爲true表示Fragment被切換到當前頁.因爲ViewPager中的Fragment在首次加載時,也會調用setUserVisibleHint,致使出現監聽重複的問題,因此在setUserVisibleHint須要添加條件判斷:
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isResumed() && isVisibleToUser) {
// 頁面被顯示
}
}
複製代碼
因此,監聽Fragment頁面的顯示,除了監聽Fragment生命週期中的onPause/onResume方法,還須要監聽onHiddenChanged方法和setUserVisibleHint。
在統計Fragment頁面的顯示時長時,須要綜合考慮這幾個方法,具體見Android無埋點方案實踐項目——Tracker中對Fragment生命週期的監聽過程FragmentLifecycleListener。
懶加載(或者叫延遲加載),也就是延遲數據的請求過程。經常使用於ViewPager+Fragment模式中,不一樣的Fragment可能使用不一樣的接口,在頁面顯示的時候,可能會同時請求offscreenPageLimit 個接口,致使頁面出現卡頓。爲了解決這種問題,可延遲未顯示的Fragment的數據請求過程,即在Fragment顯示時,再進行網絡請求。Fragment中的setUserVisibleHint方法在ViewPager中的Fragment顯示時被調用,因此咱們可在其中實現數據的請求。
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser && !dataRequested) {
requestData();
}
}
複製代碼
關於這二者的區別,可從源碼來看:
@Nullable
public Context getContext() {
return mHost == null ? null : mHost.getContext();
}
@Nullable
final public FragmentActivity getActivity() {
return mHost == null ? null : (FragmentActivity) mHost.getActivity();
}
複製代碼
從它們的實現來看,都是直接返回mHost對象中的成員,mHost的類型爲FragmentHostCallback,它的構造方法以下:
public FragmentHostCallback(Context context, Handler handler, int windowAnimations) {
this(context instanceof Activity ? (Activity) context : null, context, handler,
windowAnimations);
}
FragmentHostCallback(FragmentActivity activity) {
this(activity, activity /*context*/, activity.mHandler, 0 /*windowAnimations*/);
}
FragmentHostCallback(Activity activity, Context context, Handler handler,
int windowAnimations) {
mActivity = activity;
mContext = context;
mHandler = handler;
mWindowAnimations = windowAnimations;
}
複製代碼
經過對源碼進行搜索,發現只是FragmentActivity中直接調用了FragmentHostCallback的構造方法:
class HostCallbacks extends FragmentHostCallback<FragmentActivity> {
public HostCallbacks() {
super(FragmentActivity.this /*fragmentActivity*/);
}
...
}
複製代碼
就目前源碼的實現來講,這二者沒有任何區別,引用的都是它所在的Activity的實例,可是它提供的公開的構造方法的實現卻說明: getContext()爲空的可能性能更大。 因此,在Fragment中獲取Context實例時最好使用getActivity()。
在使用Fragment的過程當中,getActivity()爲null的異常應該是最多見的。其根本緣由:Fragment與以前關聯的Activity失去了聯繫!
使用Fragment時,咱們的Activity繼承的都是FragmentActivity, FragmentActivity在被異常關閉時會保存已加載的Fragment,具體以下:
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
markFragmentsCreated();
// 保存Fragment
Parcelable p = mFragments.saveAllState();
if (p != null) {
outState.putParcelable(FRAGMENTS_TAG, p);
}
...
}
複製代碼
而後在其onCreate中對保存的Fragment進行了恢復:
protected void onCreate(@Nullable Bundle savedInstanceState) {
mFragments.attachHost(null /*parent*/);
super.onCreate(savedInstanceState);
...
if (savedInstanceState != null) {
Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
mFragments.restoreAllState(p, nc != null ? nc.fragments : null);
}
...
}
複製代碼
雖然對Fragment進行了恢復,並與其關聯了新的Activity實例,但Fragment以前關聯的Activity實例已被銷燬,若是這些Fragment中有一些延時任務,並使用了getActivity(), 就會出現空指針異常。
這是從根本上杜絕getActivity爲空的方案。平常開發中,使用的延時任務最多的狀況莫過於網絡請求和Handler。 對於網絡請求,封裝的時候最好考慮Activity/Fragment的生命週期, 在onDestroy和onDetach取消網絡請求,上傳/下載等大數據量的網絡請求,可引用ApplicationContext,放在後臺服務進行執行。對於Handler, 只須要在onDetach中清除任務便可:
@Override
public void onDetach() {
super.onDetach();
handler.removeCallbacksAndMessages(null);
}
複製代碼
經過保存的Activity實例替代getActivity。Activity雖然重建了,但以前的實例由於Fragment的持有而不會被內存清理,會形成短暫性的內存泄漏。
@Override
public void onAttach(Context context) {
super.onAttach(context);
this.mContext = context;
}
複製代碼
具體使用哪一種方法,看本身的需求,若是項目框架良好,團隊又有良好的編程規範,天然是推薦第一種。不然仍是使用第二種方案,雖然會形成短暫性的內存泄漏,倒也不會有什麼大的影響。
FragmentActivity默認狀況下在異常銷燬時會保存Fragment,並在onCreate中進行恢復,而在重建時又會建立新的Fragment,就會出現頁面重疊的問題。同時致使內存中出現n(n+1)/2個Fragment實例,這會大大增長內存消耗。這裏可採用如下兩種方案進行優化:
只在首次或沒有Fragment實例存在的時候才建立新的Fragment:
protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState);
FragmentManager fm = getSupportFragmentManager();
// 從新關聯保存的Fragment
if (savedInstanceState == null || fm.getFragments().size() == 0) {
FragmentTransaction transaction = fm.beginTransaction();
transaction.add(R.id.container_layout, ContentFragment.newInstance(fm.getFragments().size()));
transaction.commit();
}
...
複製代碼
}
Activity被異常關閉時,不要保存Fragment:
@Override public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) { FragmentManager fm = getSupportFragmentManager(); List fragmentList = fm.getFragments(); if (fragmentList.size() == 0) { super.onSaveInstanceState(outState, outPersistentState); } }
getSupportFragmentManager是V4包中管理Activity中的Fragment的管理器,而getChildFragmentManager是管理Fragment中的Fragment的管理器,也就是Fragment嵌套時應該使用getChildFragmentManager而不是getSupportFragmentManager。
動態建立Fragment時,須要事務的配合,事務添加完成後須要提交,FragmentTransaction中提供了多個提交方法:
commit(); // 異步提交
commitNow(); // 同步提交
commitAllowingStateLoss(); // 異步提交,容許狀態丟失
commitNowAllowingStateLoss(); // 同步提交,容許狀態丟失
複製代碼
commit()和commitNow()不容許在onSaveInstanceState後調用,不然會拋出java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState異常,由於onSaveInstanceState就是用來保存Fragment的狀態,onSaveInstanceState後面再次提交事務,與這些事務關聯的Fragment的狀態就會丟失,因此拋出了異常。在這種狀況下,若是肯定狀態丟失不會產生影響,可以使用commitAllowingStateLoss()或commitNowAllowingStateLoss()。
與Activity相似,Fragment也有回退棧的概念,使用addBackToStack方法便可將Fragment添加到回退棧中(注意:須要在commit以前調用),加入到回退棧中的Fragment在執行Actiivty的onBackPressed()方法時,會逐漸出棧。
執行replace操做時,若是添加到回退棧中,則被替換的Fragment的onDestroy()和onDetach()不會被調用,按返回鍵可返回到以前的Fragment,並從onCreateView()開始調用。若是不添加到回退棧中,則會調用被替換Fragment的onDestroy()和onDetach()方法。
==建議:== Activity中的第一個Fragment不要添加到回退棧中,不然返回時須要多執行一次onBackPressed()(具體UI表現:第一個Fragment出棧後出現空白頁面);
與其將Fragment與Activity比較,倒不如將其與View進行比較,畢竟它們都是不可單獨存在的元素,都須要Activity做爲宿主。Fragment與View很像,能夠動態建立,也能夠在佈局文件中定義,因此可視爲是加入了生命週期的View. 只不過它的管理須要藉助於FragmentManager和FragmentTransaction.
在使用Fragment過程當中,常常會遇到一些UI問題:
Fragment不像Activity,沒有主題的概念,若是其中加載的佈局沒有設置背景,默認就是透明的。若是Activity只加載了一個Fragment,看起來背景就是主題的背景,當添加多個Fragment的時候,就會發現頁面出現重疊,由於背景是透明的,此時須要爲Fragment中的View設置背景。
基於減小頁面重繪的原則,可以使用如下方案:
Fragment中的View默認狀況下是不可點擊的,因此不會攔截事件。一般須要將Fragment中的根佈局View的clickable屬性設置爲true,以屏蔽事件穿透。
有時候將當前頁面切換到後臺,而後恢復到前臺時會發現頁面內容丟失。出現這個問題,緣由主要出如今onSaveInstanceState方法,有時候可能不須要保存Fragment的狀態,因此在super.onSaveInstanceState以前清空了FragmentManager中的Fragment,當頁面被切換到前臺時,就會出現頁面爲空的問題。至於如何正確保存/恢復Fragment的狀態,前面的頁面重疊部分已提供瞭解決方案。
ViewPager+Fragment是一個經典組合,基本上每一個APP中都會使用它。但在使用過程當中有一些細節須要關注。下面以一個實例來講明一下:
fragmentList.add(new FirstFragment());
fragmentList.add(new SecondFragment());
fragmentList.add(new ThirdFragment());
viewPager.setAdapter(new MyFragmentPagerAdapter(getSupportFragmentManager()));
public static class MyFragmentPagerAdapter extends FragmentPagerAdapter {
public MyFragmentPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
return fragmentList.get(position);
}
@Override
public int getCount() {
return fragmentList.size();
}
}
複製代碼
這種寫法應該很熟悉,一般狀況下不會出現什麼問題,但當Activity重建時,就會發現,也不會有大的問題,只是多建立了幾個Fragment而已,下面看一下FragmentPagerAdapter的實現:
public Object instantiateItem(ViewGroup container, int position) {
final long itemId = getItemId(position);
String name = makeFragmentName(container.getId(), itemId);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
mCurTransaction.attach(fragment);
} else {
fragment = getItem(position);
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), itemId));
}
return fragment;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
...
mCurTransaction.detach((Fragment)object);
}
複製代碼
能夠看到Adapter在初始化Item的時候,會先查看該position是否已存在Fragment,若是存在就從新attach, 沒有的時候纔會調用getItem建立新的Fragment。而destroyItem的實現說明已加載的Fragment,若是不在mOffscreenPageLimit範圍內,也只是detach掉了,其Fragment實例仍然是存在的,也就是說,每一個position上的Fragment僅會建立一次(每一個position上的getItem只被調用一次),即使是Activity被重建。也就是說,FragmentPagerAdapter內置了Fragment從新關聯的功能。
再回到上面的問題,若是在onCreate中建立Fragment, 那麼每次Activity重建時,都會建立新的Fragment,然而這些新建立的Fragment並無什麼用,由於FragmentPagerAdapter關聯的仍是以前存在的Fragment。因此推薦將Fragment的構造寫在getItem中。
@Override
public Fragment getItem(int position) {
return ContentFragment.newInstance(position) ;
}
複製代碼
Fragment設置轉場動畫使用setCustomAnimations方法,它有2個重載方法,詳細介紹以下:
/**
* @param entry 新fragment進入的動畫,只對add和replace操做有效
* @param exit 當前fragment退出的動畫,只對replace和remove操做有效
* @return
*/
public FragmentTransaction setCustomAnimations(@AnimatorRes @AnimRes int entry, @AnimatorRes @AnimRes int exit);
/**
* @param enter 新頁面進入的動畫
* @param exit 當前頁面退出的動畫
* @param popEnter 當前頁面進入的動畫
* @param popExit 新頁面退出的動畫
* @return
*/
public FragmentTransaction setCustomAnimations(@AnimatorRes @AnimRes int enter, @AnimatorRes @AnimRes int exit,
@AnimatorRes @AnimRes int popEnter, @AnimatorRes @AnimRes int popExit)
複製代碼
注意:
除了setCustomAnimations方法,Android在5.0中還擴展了轉場動畫,可以使用如下方法實現:
/**
* 新Fragment進入時的動畫
* @param transition
*/
public void setEnterTransition(@Nullable Object transition);
/**
* 新Fragment退出時的動畫
* @param transition
*/
public void setReturnTransition(@Nullable Object transition);
/**
* 新Fragment進入時當前Fragment的退出動畫, 需在當前Fragment對象中設置
* @param transition
*/
public void setExitTransition(@Nullable Object transition);
/**
* 新Fragment退出時原Fragment的進入動畫,需在原Fragment對象中設置
* @param transition
*/
public void setReenterTransition(@Nullable Object transition);
複製代碼
其中的參數,Android提供了幾種實現:
示例:
FragmentManager fm = mContext.getSupportFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
Fragment fragment = ContentFragment.newInstance(fm.getFragments().size());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
fragment.setEnterTransition(new Slide(Gravity.RIGHT));
fragment.setReturnTransition(new Slide(Gravity.RIGHT));
}
transaction.add(R.id.container_layout, fragment);
transaction.addToBackStack(null);
transaction.commit();
複製代碼
這是一個頗有爭議性的話題,有人認爲一個APP只須要一個Activity便可,也有人認爲Fragment只須要使用在需複用的頁面便可。關於這個問題,可從如下幾方面來探討:
基於這三點,咱們可在單個業務模塊中使用單Activity+多Fragment的模式來實現,如登陸模塊,只須要一個LoginActivity, 其餘的功能如註冊,找回密碼等都使用Fragment實現。一方面,Fragment比Activity更輕量,另外一方面,使模塊化在組件層面有了明顯的區分。
這只是Fragment的一個簡單總結,可能還有不少細節未說起,歡迎補充。