Fragment-踩坑

wzgiceman發表於2019-03-04

背景

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初始化

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物件,但是前者比後者的有點在於,前者更加的靈活,所以推薦使用第一種方式。


ActivityFragment傳參

預設建立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 實現。


FragmentManagerFragmentTransaction使用

FragmentManager

Activity中使用Fragment可以使用getSupportFragmentManager獲取一個FragmentManager物件,但是在Fragment中顯示子Fragment需要呼叫FragmentgetChildFragmentManager()

原始碼如下:

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()退棧
當使用者按下返回鍵時,如果回退棧中儲存有之前的事務,會先執行事務回退,然後再執行Activityfinish()方法 。

簡單使用

通過FragmentManagerFragmentTransaction結合使用,我們可以將第一種初始化的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
比如:你在popFragment之後,該Fragment的非同步任務仍然在執行,並且在執行完成後呼叫了getActivity()方法,這樣就會空指標;

解決辦法

  • getContext()替代getActivity()

  • 定義全域性變數,在FragmentonAttach(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();
        }
    }複製程式碼

交流

QQ交流群,談談夢想,聊聊人生!