背景
Fragment
已經成為Android
開發介面設計中不可或缺的一部分,同時也發揮著越來越重要的角色,雖然Fragment
已經能出色的專案開發,但是在使用過程中也暴露了越來越多的問題,雖然google
也一直在及時的修復,但是還是有很多坑,所以決定記錄Fragment
使用過程中的使用問題,避免小夥伴們重複踩坑。
在瞭解踩坑之前,我們需要先了解Fragment
的使用要點和使用方法
Fragment
介紹
作為 view
介面的一部分,Fragment
的存在必須依附於 FragmentActivit
使用,並且與 FragmentActivit
一樣,擁有自己的獨立的生命週期,同時處理使用者的互動動作。同一個 FragmentActivit
可以有一個或多個 Fragment
作為介面內容,同樣Fragment
也可以擁有多個子Fragment
,並且可以動態新增、刪除 Fragment
,讓UI
的重複利用率和易修改性得以提升,同樣可以用來解決部分螢幕適配問題。
另一方面,support v4 包中也提供了 Fragment,相容 Android 3.0 之前的系統,使用相容包需要注意兩點:
-
宿主
Activity
必須繼承自FragmentActivity
; -
使用
getSupportFragmentManager()
方法獲取FragmentManager
物件;
生命週期
Fragment
同樣是具備了獨立的生命週期,但是和Activity
的生命週期還有不一樣的地方,如圖:原圖地址
Fragment
初始化
Fragment
預設有兩種初始化的方法,一種new
另一種是嵌入xml
-
new
FirstFragment firstFragment=new FirstFragment();複製程式碼
-
xml
<fragment android:layout_width="match_parent" android:layout_height="match_parent" class="com.wzgiceman.fragmentpit.Fragment.FirstFragment"/>複製程式碼
上面兩種方法都可以初始得到一個Fragment
物件,但是前者比後者的有點在於,前者更加的靈活,所以推薦使用第一種方式。
Activity
和Fragment
傳參
預設建立Fragment
系統已經給我們初始了傳參的程式碼
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param param1 Parameter 1.
* @param param2 Parameter 2.
* @return A new instance of fragment FirstFragment.
*/
// TODO: Rename and change types and number of parameters
public static FirstFragment newInstance(String param1, String param2) {
FirstFragment fragment = new FirstFragment();
Bundle args = new Bundle();
args.putString(ARG_PARAM1, param1);
args.putString(ARG_PARAM2, param2);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mParam1 = getArguments().getString(ARG_PARAM1);
mParam2 = getArguments().getString(ARG_PARAM2);
}
}複製程式碼
這無疑是最好的選擇
回撥
Fragment
類提供有startActivityForResult()
方法用於 Activity
間的頁面跳轉和資料回傳,其實內部也是呼叫 Activity
的對應方法。但是在頁面返回時需要注意 Fragment 沒有提供 setResult()
方法,可以通過宿主 Activity
實現。
FragmentManager
和FragmentTransaction
使用
FragmentManager
在Activity
中使用Fragment
可以使用getSupportFragmentManager
獲取一個FragmentManager
物件,但是在Fragment
中顯示子Fragment
需要呼叫Fragment
的getChildFragmentManager()
原始碼如下:
public final FragmentManager getChildFragmentManager() {
throw new RuntimeException("Stub!");
}複製程式碼
FragmentTransaction
Fragment
的動態新增、刪除等操作都需要藉助於 FragmentTransaction
類來完成,比如上面提到的 commit()
操作,下面是幾種常用的方法:
-
add() 系列:新增 Fragment 到 Activity 介面中;
-
remove():移除 Activity 中的指定 Fragment;
-
replace() 系列:通過內部呼叫 remove() 和 add() 完成 Fragment 的修改;
-
hide() 和 show():隱藏和顯示 Activity 中的 Fragment;
-
addToBackStack():新增當前事務到回退棧中,即當按下返回鍵時,介面迴歸到當前事物狀態;
-
commit():提交事務,所有通過上述方法對 Fragment 的改動都必須通過呼叫 commit() 方法完成提交
replace()
和hide()
區別
replace()
和hide()
都可以動態的在Activity
中顯示多個Fragment
,並且可以來回靈活的切換,但是它們有很大的區別,replace()
方法不會保留 Fragment
的狀態,也就是說諸如 EditText
內容輸入等使用者操作在 remove()
時會消失;但是hide()
卻不會,能完整的保留使用者的處理資訊。
addToBackStack()
退棧
當使用者按下返回鍵時,如果回退棧中儲存有之前的事務,會先執行事務回退,然後再執行Activity
的finish()
方法 。
簡單使用
通過FragmentManager
和FragmentTransaction
結合使用,我們可以將第一種初始化的Fragment
動態的顯示到介面中,這裡使用replace()
演示:
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.fl_fragment, FirstFragment.newInstance("A","B"));
ft.commit();複製程式碼
踩坑
在瞭解了Fragment
的基礎使用後,可以開始使用過程中的踩坑了
getActivity()
引用問題
在Fragment
中常常需要使用到content
物件,比如網路載入現在一個progress
等等,這時候可能你遇到過getActivity()
返回null
,或者平時執行完好的程式碼,在“記憶體重啟”之後,呼叫getActivity()的地方卻返回null
,報了空指標異常。
大多數情況下的原因:你在呼叫了getActivity()
時,當前的Fragment已經onDetach()
了宿主Activity
。
比如:你在pop
了Fragment
之後,該Fragment
的非同步任務仍然在執行,並且在執行完成後呼叫了getActivity()
方法,這樣就會空指標;
解決辦法
-
用
getContext()
替代getActivity()
-
定義全域性變數,在
Fragment
的onAttach(Activity activity)準備廢棄
或者onAttach(Context context)
方法中初始化Context context; @Override public void onAttach(Context context) { super.onAttach(context); this.context=context; }複製程式碼
顯然第一種方法更加靈活方便了。
高耦合
當子Fragment
需要呼叫宿主Acitivity
的方法時,比如子Fragment
需要傳送一個廣播,但是Fragment
沒有改方法,所以需要藉助宿主Activity
去傳送,這時候常常需要強制轉換content
物件,然後呼叫宿主Acitivity
發方傳送廣播,這種直接使用的方式違背了高聚低耦的設計原則;
解決辦法
通過介面抽象的方法,通過介面去呼叫宿主Activity的方法。
- 定義介面
/**
* 傳送廣播
* Created by WZG on 2016/12/31.
*/
public interface SendBListener {
void send();
}複製程式碼
- 實現介面
public class FirstFragment extends Fragment {
SendBListener listener;
public void setListener(SendBListener listener) {
this.listener = listener;
}
@OnClick(value = R.id.tv)
void onTvClick(View view) {
listener.send();
}
}複製程式碼
- 呼叫
public class MainActivity extends AppCompatActivity implements SendBListener{
@BindView(R.id.fl_fragment)
FrameLayout mFlFragment;
@Override
public void send() {
sendBroadcast(new Intent("xxxxxx"));
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FirstFragment firstFragment=new FirstFragment();
firstFragment.setListener(this);
}
}複製程式碼
重疊
由於採用建立物件的方式去初始化Fragment
物件,當宿主Activity
在介面銷燬或者介面重新執行onCreate()
方法時,就有可能再一次的執行Fragment
的建立初始,而之前已經存在的 Fragment 例項也會銷燬再次建立,這不就與 Activity 中 onCreate() 方法裡面第二次建立的 Fragment 同時顯示從而發生 UI 重疊的問題。
如果宿主介面Acitivity
可以橫豎屏切換,導致的生命週期重新重新整理也同理可導致介面的重疊問題。
解決辦法
- 推薦:利用
savedInstanceState
判斷
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
FirstFragment firstFragment;
if (savedInstanceState==null) {
firstFragment=new FirstFragment();
ft.add(R.id.fl_fragment, firstFragment, "FirstFragment");
}else {
firstFragment = (FirstFragment) fm.findFragmentByTag("FirstFragment");
}
}複製程式碼
- 在
Activity
提供的onAttachFragment()
方法中處理
@Override
public void onAttachFragment(Fragment fragment) {
super.onAttachFragment(fragment);
if (fragment instanceof FirstFragment){
firstFragment = (FirstFragment) fragment;
}
}複製程式碼
- 建立
Fragment
時判斷
Fragment fragment = getSupportFragmentManager().findFragmentByTag("FirstFragment");
if (fragment==null) {
firstFragment =new FirstFragment();
ft.add(R.id.fl_fragment, firstFragment, "FirstFragment");
}else {
firstFragment = (FirstFragment) fragment;
}複製程式碼
Fragment
轉場動畫
如果你想給下一個Fragment設定進棧動畫和出棧動畫,setCustomAnimations(enter, exit)
只能設定進棧動畫,第二個引數並不是設定出棧動畫;
請使用setCustomAnimations(enter, exit, popEnter, popExit)
,這個方法的第1個引數對應進棧動畫,第4個引數對應出棧動畫,所以是setCustomAnimations(進, exit, popEnter, 出))
Fragment
狀態監聽
很多時候,我們需要在多Fragment
中重新整理介面,當然由於Fragment
有自己獨立的生命週期但是也依賴宿主Activity
存在,所以在重新整理介面的時候需要注意如:
當宿主Activity
A
進入B
中,又衝B
返回到A
,這時候宿主A
執行onResume()
方法,當然這時候的Fragment
也會執行onResume()
當宿主Activity
A
中的Fragment
全部初始完成顯示過,在切換Fragment
的時候不會再一次觸發onResume()
方法,但是卻可以觸發Fragment的onHiddenChanged(boolean hidden)
方法
所以當我們需要實時重新整理Fragment
介面的時候,需要同時結合onResume()
和onHiddenChanged(boolean hidden)
方法去重新整理當前顯示Fragment
而避免重新整理hide()
的Fragment
使用
Override
public void onResume() {
super.onResume();
//當前是否是現實狀態
if (isVisible()){
//重新整理介面
updateUI();
}
}
@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
//方法重複發起重新整理介面
if (isVisible() && isResumed()){
updateUI();
}
}複製程式碼