Android技能樹 — Fragment總體小結

青蛙要fly發表於2019-08-19

前言:

Android基礎知識

Android技能樹 — Fragment總體小結

Android技能樹 — 動畫小結

Android技能樹 — View小結

Android技能樹 — Activity小結

Android技能樹 — View事件體系小結

Android技能樹 — Android儲存路徑及IO操作小結

Android技能樹 — 多程式相關小結

Android技能樹 — Drawable小結

Android技能樹 — 螢幕適配小結


很久沒有寫文章了,沒其他原因,就是因為懶。

因為最近的APP開發,使用的是單Activity + 多Fragment的方式,不同於以前基本介面都是Activity的方式,所以Fragment用了很多,想到自己以前也寫了很多相關的基礎知識,Fragment卻從來沒有寫過,所以就打算補上一篇fragment的基礎總結。

老樣子,先上腦圖:

Android技能樹 — Fragment總體小結

Android技能樹 — Fragment總體小結

我們就按照腦圖的順序一樣樣來看Fragment的基礎知識。


正文:

1.Fragment的新增

Android技能樹 — Fragment總體小結

我們知道Fragment是一個"碎片(或者片段)",新增在Activity中。如果我現在問你,Activity要顯示一個按鈕Button,你會怎麼做?

1. 直接在Layout.xml中新增<Button/>

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button"
        />
</LinearLayout>
複製程式碼

2. 在程式碼中動態新增,比如我們新增到一個LinearLayout中:

Button button = new Button(mActivity);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.WRAP_CONTENT);
button.setLayoutParams(params);
container.addView(button);
複製程式碼

所以Fragment也很簡單,就把它當做一個簡單的View(但其實更像是“子 Activity”),然後新增方式也是一樣。

1. 直接在Layout.xml中新增:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <fragment
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>
複製程式碼

2. 直接在程式碼中新增:

Fragment one = new FragmentOne();//自定義的Fragment類
//要先獲取FragmentManager物件
FragmentManager fragmentManager = getSupportFragmentManager();
//開啟一個FragmentTransaction事務
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.add(R.id.framelayout_view,one).commit();
複製程式碼

其中新增到R.id.framelayout_view的這個idViewGourp可以是<FrameLayout/>,也可以是其他的比如<LinearLayout/>等。


2. Fragment的基本操作

Android技能樹 — Fragment總體小結

看到我們上面的動態程式碼新增的時候需要獲取FragmentTransactionFragmentManager

2.1 FragmentManager相關

1. getFragmentManager():

獲取Fragment父容器的管理器,但是現在該方法在Activity中已經被標記不推薦使用了。

 /**
     * Return the FragmentManager for interacting with fragments associated
     * with this activity.
     *
     * @deprecated Use {@link android.support.v4.app.FragmentActivity#getSupportFragmentManager()}
     */
    @Deprecated
    public FragmentManager getFragmentManager() {
        return mFragments.getFragmentManager();
    }
複製程式碼

2. getSupportFragmentManager():

v4包下的這個方法,與上一個效果一樣,不過是Android推薦使用的方法(畢竟可以相容Android所有版本)

3. getChildFragmentManager():

我們提過,Fragment更像是一個“子Activity”,那你說"子Activity"中能否再新增Fragment,答案當然是可以。那麼在Fragment內部中的Fragment的管理器,就需要使用getChildFragmentManager()來獲取了。

2.2 FragmentTransaction相關

Android技能樹 — Fragment總體小結

我們可以看到有新增刪除各種方法操作。

1. attach/detach方法:

  • detach(Fragment fragment) : 分離指定Fragment的UI檢視
  • attach(Fragment fragment) : 重新關聯一個Fragment(當這個Fragment的detach執行之後)
  1. 當Fragment被detach後,Fragment的生命週期執行完onDestroyView就終止了,這意味著Fragment的例項並沒有被銷燬,只是UI介面被移除了(注意和remove是有區別的)。
  2. 當Fragment被detach後,執行attach操作,會讓Fragment從onCreateView開始執行,一直執行到onResume。
  3. attach無法像add一樣單獨使用,單獨使用會拋異常。方法存在的意義是對detach後的Fragment進行介面恢復。

2.add/remove方法:

我想這二個是用的最多的了,add()和remove()是將fragment新增和移除. remove()比detach()要徹底一些, 如果不加入到回退棧中, remove()的時候, fragment的生命週期會一直走到onDetach();如果加入了回退棧,則會只執行到onDestoryView(),Fragment物件還是存在的。

add一個fragment,如果加到的是同一個id的話,有點像我們的Activity棧,啟動多個Activity時候,Activity一個個疊在上面,fragment也是類似,一個個fragment疊在上面。

3.replace方法:

replace = remove + add , 所以可以理解為先把相同id下的Fragment移除掉,然後再加入這個當前的fragment。

所以如果你覺得Fragment存在太多,影響效能,可以用replace來切換各個介面,就可以保證當前只有一個Fragment,但是因為每次切換後,Fragment都會重建,所以如果這個介面有網路請求相關的,你就會發現這個介面又重新去請求網路介面了,顯得很多此一舉。

4.hide/show方法:

就是字面意思,讓一個Fragment隱藏,讓一個Fragment顯示。你可以理解為Button設定了View.GONE和View.VISIBLE。常常配合有多個Fragment及有TAB等切換方式的時候,選中某個按鈕,然後根據相應的讓對應的Fragment顯示,其他Fragment隱藏。

5.commit/commitAllowingStateLoss:

我估計很多人認識這個commitAllowingStateLoss大部分是因為自己的程式碼有閃退異常:

java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
複製程式碼

在你離開當前Activity等情況下,系統會呼叫onSaveInstanceState()幫你儲存當前Activity的狀態、資料等,直到再回到該Activity之前(onResume()之前),你執行Fragment事務,就會丟擲該異常。然後網上有很多教程,叫你提交的時候使用commitAllowingStateLoss()方法,雖然說不會丟擲錯誤,但是如果在Activity已經儲存狀態完之後提交了它,到時候Ativity意外崩潰,再恢復資料的時候就不會恢復在Activity儲存狀態之後提交的fragment的更新,造成狀態丟失了。

額外補充:
1.commit()方法並不立即執行transaction中包含的動作,而是把它加入到UI執行緒佇列中. 如果想要立即執行,可以在commit之後立即呼叫FragmentManager的executePendingTransactions()方法.

2. commit()方法必須在狀態儲存之前呼叫,否則會丟擲異常,如果覺得狀態丟失沒關係, 可以呼叫commitAllowingStateLoss(). 但是除非萬不得已, 一般不推薦用這個方法, 會掩蓋很多錯誤.

6. addToBackStack:

我們可以看到FragmentTransaction裡面有加入回退棧方法,但是沒有退出的方法:popBackStack。這是因為這個方法在FragmentManager裡面。

也就是如下圖:

Android技能樹 — Fragment總體小結

一般反應是,addToBackStack和popBackStack不是應該像上面的類似add和remove一樣,都一個層級的嗎??所以popBackStack不也應該是FragmentTransaction下的一個方法???

所以我們單從圖片所示就能知道,popBackStackFragmentTransaction是一個層級,所以popBackStack操作的其實也是《fragment事務》(FragmentTransaction),所以可以理解為addToBackStack把我們前面的FragmentTransaction事務(比如add,remove,replace等一系列操作)加入到了回退棧(!!!記住不是把fragment加入到了回退棧),而popBackStack是操作回退棧裡面的事務。

當然具體的原始碼過程分析,細講的話又是很多,都可以另外專門寫一篇文章,所以直接借鑑網上別人已經寫好的文章:

Fragment那點事①Fragment棧管理

額外補充:
1.加入回退棧:remove掉的fragment執行onDestoryView,並沒有執行onDestory,fragment例項物件還是存在,當回退時候,fragment從onCreateView處執行

2. 未加入回退棧:remove掉的fragment 執行 onDestoryView和onDestory,徹底銷燬移除


3.Fragment中獲取Context

Android技能樹 — Fragment總體小結

我們可以直接在fragment程式碼裡面直接使用getActivity()getContext()方法。

但是有時候獲取為空,所以一般我們使用的是:

Class xxxFragment extends Fragment {

    private Context mContext;
    
    //'高版本後,都是回撥這個方法'
    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        mContext = context;
    }
    
    //'API低於 23 的版本的時候,是會回撥這個方法'
    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        mContext = activity;
    }
}
複製程式碼

4.Fragment配合ViewPager

Android技能樹 — Fragment總體小結

ViewPager配合Fragment的時候,主要使用FragmentPagerAdapterFragmentStatePagerAdapter這二個Adapter。其實使用很簡單(一般的最最簡單的寫法):

public class FragmentAdapter extends FragmentPagerAdapter{
    private ArrayList<Fragment> list;

    //通過構造獲取fragment集合
    public Fragment_pager(FragmentManager fm,ArrayList<Fragment> list) {
        super(fm);
        this.list=list;
    }
    //設定具體position的fragment
    @Override
    public Fragment getItem(int position) {
        // TODO Auto-generated method stub
        return list.get(position);
    }
    //設定有多少個fragment
    @Override
    public int getCount() {
        // TODO Auto-generated method stub
        return list.size();
    }
}
複製程式碼

然後ViewPager.setAdapter(xxxx);

但是大家會奇怪為啥有二個Adapter:FragmentPagerAdapterFragmentStatePagerAdapter,他們的區別我們可以看具體的原始碼:

FragmentPagerAdapter原始碼:

public abstract class FragmentPagerAdapter extends PagerAdapter {


    //'初始化建立Item:'
    @NonNull
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
        if (this.mCurTransaction == null) {
            this.mCurTransaction = this.mFragmentManager.beginTransaction();
        }

        long itemId = this.getItemId(position);
        String name = makeFragmentName(container.getId(), itemId);
        Fragment fragment = this.mFragmentManager.findFragmentByTag(name);
        if (fragment != null) {
        
            //'後面使用fragment是通過FragmentTransaction.attach方式加進來的,'
            //'只是重新繪製了UI,fragment物件還在。'
            this.mCurTransaction.attach(fragment);
        } else {
        
            //'我們知道剛返回fragment使用的是getItem(position)方法'
            //'我們可以看到第一次使用fragment是通過FragmentTransaction.add方式加進來的'
            fragment = this.getItem(position);
            this.mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId));
        }

        if (fragment != this.mCurrentPrimaryItem) {
            fragment.setMenuVisibility(false);
            fragment.setUserVisibleHint(false);
        }

        return fragment;
    }
    
    
    //'銷燬item:'
    public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        if (this.mCurTransaction == null) {
            this.mCurTransaction = this.mFragmentManager.beginTransaction();
        }
        
        //'我們可以看到FragmentTransaction只是單純的detach了fragment,檢視不在了,但是fragment物件還在'
        this.mCurTransaction.detach((Fragment)object);
    }
    
}
複製程式碼

我們可以看到fragment並沒有真的銷燬,FragmentPageAdapter則適用於固定的,少量的Fragment情況,例如和TabLayout共同使用時。

FragmentStatePagerAdapter原始碼:

public abstract class FragmentStatePagerAdapter extends PagerAdapter {


    
    @NonNull
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
        Fragment fragment;
        if (this.mFragments.size() > position) {
            fragment = (Fragment)this.mFragments.get(position);
            if (fragment != null) {
                return fragment;
            }
        }

        if (this.mCurTransaction == null) {
            this.mCurTransaction = this.mFragmentManager.beginTransaction();
        }

        fragment = this.getItem(position);
        if (this.mSavedState.size() > position) {
            SavedState fss = (SavedState)this.mSavedState.get(position);
            if (fss != null) {
                fragment.setInitialSavedState(fss);
            }
        }

        while(this.mFragments.size() <= position) {
            this.mFragments.add((Object)null);
        }

        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
        this.mFragments.set(position, fragment);
        
        //'我們可以看到fragment都是add進來的'
        this.mCurTransaction.add(container.getId(), fragment);
        return fragment;
    }
    
    
    public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        Fragment fragment = (Fragment)object;
        if (this.mCurTransaction == null) {
            this.mCurTransaction = this.mFragmentManager.beginTransaction();
        }

        while(this.mSavedState.size() <= position) {
            this.mSavedState.add((Object)null);
        }

        this.mSavedState.set(position, fragment.isAdded() ? this.mFragmentManager.saveFragmentInstanceState(fragment) : null);
        this.mFragments.set(position, (Object)null);
        
        
        //'可以看到都是通過remove的方式移除了'
        this.mCurTransaction.remove(fragment);
    }

    

}
複製程式碼

所以我們知道了FragmentStatePagerAdapter是真的會把fragment物件都銷燬,所以如果fragment數量很多的話,使用這個會更好,因為fragment存在太多,對應用效能造成很大影響,所以要remove掉fragment。


5.無UI的fragment:

Android技能樹 — Fragment總體小結

5.1 使用Fragment 保持需要恢復物件

呼叫setRetainInstance(true)方法可保留fragment,如下:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setRetainInstance(true);
    ...........
}
複製程式碼

比如旋轉螢幕,已保留的fragment不會隨著activity一起被銷燬(但會銷燬fragment的檢視); 相反,它會一直保留(程式不消亡的前提下),並在需要時原封不動地傳遞給新的Activity。

所以我們比如一些物件可以保持在fragment中,這時候Activity重新恢復後,其他物件可以從fragment中找回。

可以大概看下其他作者文章介紹:

Fragment呼叫setRetainInstance的原理

5.2 類似RxPermission用於處理回撥

RxPermission裡有一個Fragment用於分發許可權回撥。這個是什麼意思??

我們知道原生請求許可權:

//發出許可權請求:
int requestCode = 1;
requestPermissions(new String[]{Manifest.permission.READ_PHONE_STATE},requestCode);


//許可權處理結果回撥
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
複製程式碼

是不是覺得在要看複寫這個回撥方法很麻煩???而且沒有美感。

而RxPermission是這樣申請許可權的:

RxPermissions rxPermissions = new RxPermissions(this);

rxPermissions.requestEach(
        //請求的許可權
        Manifest.permission.CAMERA,
        Manifest.permission.READ_PHONE_STATE)
        .subscribe(new Consumer<Permission>() {
        @Override
        public void accept(@io.reactivex.annotations.NonNull Permission permission) throws Exception {
            //許可權通知回撥
        }
});

複製程式碼

感覺就是一步呵成的感覺,很棒。但是RxPermission只是對系統的原生許可權申請做了封裝而已,那系統的原本的回撥函式:onRequestPermissionsResult去哪裡了呢???

public class RxPermissionsFragment extends Fragment {
    
    .......
    .......
    .......
    
    //'申請許可權'
    @TargetApi(23)
    void requestPermissions(@NonNull String[] permissions) {
        this.requestPermissions(permissions, 42);
    }
    
    //'申請許可權後結果回撥'
    @TargetApi(23)
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == 42) {
            boolean[] shouldShowRequestPermissionRationale = new boolean[permissions.length];

            for(int i = 0; i < permissions.length; ++i) {
                shouldShowRequestPermissionRationale[i] = this.shouldShowRequestPermissionRationale(permissions[i]);
            }

            this.onRequestPermissionsResult(permissions, grantResults, shouldShowRequestPermissionRationale);
        }
    }
    
    //'回撥後的具體處理方法'
    void onRequestPermissionsResult(String[] permissions, int[] grantResults, boolean[] shouldShowRequestPermissionRationale) {
        int i = 0;

        for(int size = permissions.length; i < size; ++i) {
            this.log("onRequestPermissionsResult  " + permissions[i]);
            PublishSubject<Permission> subject = (PublishSubject)this.mSubjects.get(permissions[i]);
            if (subject == null) {
                Log.e(RxPermissions.TAG, "RxPermissions.onRequestPermissionsResult invoked but didn't find the corresponding permission request.");
                return;
            }

            this.mSubjects.remove(permissions[i]);
            boolean granted = grantResults[i] == 0;
            
            //'subject主動呼叫onNext方法傳送結果'
            subject.onNext(new Permission(permissions[i], granted, shouldShowRequestPermissionRationale[i]));
            subject.onComplete();
        }

    }

    
    ......
    ......
    ......  

 
}
複製程式碼

我們可以到這個fragment內部已經幫我們複寫了請求許可權的原生方法和許可權回撥通知的原生方法。然後再通過subject在結果處傳送通知即可。

這裡我不會細講整個RxPermission原始碼,我以前寫過的相關文章,大家可以具體看下:

專案需求討論 - 動態許可權申請分析及相關第三方庫原始碼分析

專案需求討論 — 手把手帶你寫RxPermission


6.建構函式和資料傳遞

Android技能樹 — Fragment總體小結

6.1 建構函式傳遞資料

我們知道fragment也就是普通的物件,可以通過new的方式,我們平常使用物件傳遞值都是可以直接在建構函式裡面定義引數值,直接賦值進去,那fragment是否可以這樣??答案是可以的,但是不推薦。

public class FragmentOne extends Fragment {
    
    //'在其他地方直接FragmentOne one = new FragmentOne("青蛙要fly");進行值傳遞'
    //'但是我們不推薦這樣'
    public FragmentOne(String value) {

    }
    
    
    //'而是通過bundle來傳遞,Fragment.setArguments(Bundle)賦值進去'
    public static FragmentOne newInstance(Bundle args) {
        FragmentOne fragment = new FragmentOne();
        if(args != null){
            fragment.setArguments(args);    
        }
        return fragment;
    }
    
}
複製程式碼

原因:我們可以知道Activity重新建立時,會重新構建它所管理的Fragment,原先的Fragment的欄位值將會全部丟失(因為當切換橫豎屏時,Fragment會呼叫自己的無參建構函式,那麼在建構函式傳參就會失效),但是通過 Fragment.setArguments(Bundle bundle)方法設定的bundle會保留下來,從而資料又可以恢復,所以儘量使用 Fragment.setArguments(Bundle bundle)方式來傳遞引數

6.2 其他資料傳遞方式

Activity 與 Fragment 資料傳遞:

Android技能樹 — Fragment總體小結

Fragment 與 Fragment 資料傳遞

Android技能樹 — Fragment總體小結

重點說下setTargetFragment,因為很多人都不知道。

我們的目標:FragmentA 啟動FragmentB ,然後FragmentB做完事情,返回結果給FragmentA


FragmentB.setTargetFragment(FragmentA);

然後在B中:
getTargetFragment().onActivityResult(getTargetRequestCode(), resultOK, i);


然後再FragmentA中:
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
}

複製程式碼

7. Fragment重建恢復資料

Android技能樹 — Fragment總體小結

推薦下面這篇文章:

[譯] 儲存/恢復 Activity 和 Fragment 狀態的最佳實踐

引用一段話:

完全分開處理Fragment狀態和view狀態 為了使你的程式碼變得乾淨和可擴充套件,你最好把Fragment狀態和View狀態分開處理。如果這裡有任何屬性是屬於View的,在View內部進行儲存和恢復.如果這裡有任何屬性是屬於Fragment的,在Fragment內部進行儲存和恢復。


8.常用監聽Fragment顯示方法

Android技能樹 — Fragment總體小結

這塊比較基礎,就不細講了。


9.監聽Fragment發生變化

Android技能樹 — Fragment總體小結

回退棧(back stack)狀態改變監聽:

getSupportFragmentManager().addOnBackStackChangedListener(new OnBackStackChangedListener() {
    @Override
    public void onBackStackChanged() {
    
    }
});
複製程式碼

註冊fragment的生命監聽:

List<Fragment> fragmentList = new ArrayList<>();

getActivity().getSupportFragmentManager().registerFragmentLifecycleCallbacks(new FragmentManager.FragmentLifecycleCallbacks() {
    @Override
    public void onFragmentViewCreated(@NonNull FragmentManager fm, @NonNull Fragment f, @NonNull View v, @Nullable Bundle savedInstanceState) {
        super.onFragmentViewCreated(fm, f, v, savedInstanceState);
        fragmentList.add(f);
    }

    @Override
    public void onFragmentViewDestroyed(@NonNull FragmentManager fm, @NonNull Fragment f) {
        super.onFragmentViewDestroyed(fm, f);
        fragmentList.remove(f);
    }
}, false);

//'這裡我們注意下最後的程式碼裡面的那個false,這個false的意思是:不遞迴碎片中的碎片了,就是碎片棧中的碎片'

複製程式碼

10. DialogFragment:

我們知道現在大家已經很少使用了Dialog類,而是使用了DialogFragment,其本質就是個Fragment。

其實這個本來也想多寫點,但是我估計這個基本安卓開發都使用過,所以就直接用網上其他作者的基礎介紹文章:

Android 必知必會 - DialogFragment 使用總結

同時具體的自定義DialogFragment我以前文章也有寫過:

專案需求討論-仿ios底部彈框實現及分析


結語:

很久沒寫文章了。一看居然都快半年了......後面準備慢慢的補起自己的部落格。有錯誤的地方歡迎大家指出

Android技能樹 — Fragment總體小結

相關文章