Fragment懶載入(支援巢狀) 友盟統計Fragment時長最佳實踐
前言
在專案中我們可能需要獲取Fragment可見或者不可見時的回撥。(回撥這個詞在這裡用的可能並不準確,這裡理解為當Fragment可見或者不可見時能夠觸發一些方法,下同)。
Fragmeng生命週期中有onResume
,onPause
,這兩個生命週期是跟隨Activity的。當呼叫getSupportFragmentManager().beginTransaction().hide(fragment)
時或者滑動ViewPager隱藏Fragment時,Fragment的這兩個生命週期都不會回撥。
那麼,如何得到Fragment可見時的回撥呢?如何得到Fragment不可見時的回撥呢?
思路
首先先定義一個BetterLifecycleFragment繼承自Fragment。我希望當Fragment可見狀態改變時,子類可以有對應的生命週期的回撥。如 onFragmentResume, onFragmentPause。如下:
public class BetterLifecycleFragment extends Fragment {
/**
* Fragment 可見時回撥
* @param isFirst 是否是第一次顯示
*/
protected void onFragmentResume(boolean isFirst){
}
/**
* Fragment 不可見時回撥
*/
protected void onFragmentPause(){
}
}
注意到這裡 onFragmentResume 裡有一個 isFirst引數,這個引數是為了方便懶載入。如果是第一次,則去請求資料。
統計Fragment時長也很簡單,把原來onResume和onPause中的方法搬到這兩個方法裡即可。
判斷Fragment可見的充分條件
我們先思考Fragment不可見都有幾種情況:
1. Activity 進入後臺,Fragment#onPause回撥。
2. ViewPager中被滑出螢幕之外的Fragment
我們可以通過 getUserVisibleHint()
3. Activity執行
getSupportFragmentManager().beginTransaction().hide(fragment)
的Fragment。 這時
Fragment#onHiddenChanged(boolean hidden)
方法會執行。
除去上面三種情況,Fragment對於使用者就是可見的了。
我們在 BetterLifecycleFragment中新增isFragmentVisible()
方法用來判斷Fragment是否可見。
public class BetterLifecycleFragment extends Fragment {
private boolean isResuming = false;
private boolean hidden = false;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
hidden = false;
}
@Override
public void onResume() {
super.onResume();
isResuming = true;
}
@Override
public void onPause() {
super.onPause();
isResuming = false;
}
@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
this.hidden = hidden;
}
/**
* Fragment是否可見
* @return
*/
public boolean isFragmentVisible() {
if (isResuming()
&& getUserVisibleHint()
&& !hidden) {
return true;
}
return false;
}
/**
* Fragment 是否在前臺。
* @return
*/
private boolean isResuming() {
return isResuming;
}
}
注意這裡我們並沒有使用系統的 isResumed()以及isHiddlen();
為什麼呢?不妨自己在onPause以及onHiddenChanged(boolean hidden)
裡面列印一下這兩個方法就知道了。
完整實現
上述已經說過,Fragment不可見有三種情況。同理Fragment可見也和這三種情況有關。
Fragment可見狀態改變的三種情況:
1. Fragment進入前臺或者進入後臺:
對應 onResume 和 onPause
2. ViewPager 滑動:
Fragment 的setUserVisibleHint(boolean isVisibleToUser)
觸發。(事實上這個方法根本不會改變Fragment是否可見,只是告訴我們Fragment是否在螢幕之中。)
3. FragmentManager
呼叫了show或者hide:
Fragment 的onHiddenChanged(boolean hidden)
觸發。
我們只需在Fragment中可能改變其可見狀態的方法中呼叫我們的 isFragmentVisible()
,然後和上次Fragment的可見狀態做個比較,即可得到Fragment可見以及不可見的回撥。
完整程式碼如下:
public class BetterLifecycleFragment extends Fragment {
private boolean isLastVisible = false;
private boolean hidden = false;
private boolean isFirst = true;
private boolean isResuming = false;
private boolean isViewDestroyed = false;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
isLastVisible = false;
hidden = false;
isFirst = true;
isViewDestroyed = false;
}
@Override
public void onResume() {
super.onResume();
isResuming = true;
tryToChangeVisibility(true);
}
@Override
public void onPause() {
super.onPause();
isResuming = false;
tryToChangeVisibility(false);
}
@Override
public void onDestroyView() {
super.onDestroyView();
isViewDestroyed = true;
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
setUserVisibleHintClient(isVisibleToUser);
}
private void setUserVisibleHintClient(boolean isVisibleToUser) {
tryToChangeVisibility(isVisibleToUser);
if (isAdded()) {
// 當Fragment不可見時,其子Fragment也是不可見的。因此要通知子Fragment當前可見狀態改變了。
List<Fragment> fragments = getChildFragmentManager().getFragments();
if (fragments != null) {
for (Fragment fragment : fragments) {
if (fragment instanceof BetterLifecycleFragment) {
((BetterLifecycleFragment) fragment).setUserVisibleHintClient(isVisibleToUser);
}
}
}
}
}
@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
onHiddenChangedClient(hidden);
}
public void onHiddenChangedClient(boolean hidden) {
this.hidden = hidden;
tryToChangeVisibility(!hidden);
if (isAdded()) {
List<Fragment> fragments = getChildFragmentManager().getFragments();
if (fragments != null) {
for (Fragment fragment : fragments) {
if (fragment instanceof BetterLifecycleFragment) {
((BetterLifecycleFragment) fragment).onHiddenChangedClient(hidden);
}
}
}
}
}
private void tryToChangeVisibility(boolean tryToShow) {
// 上次可見
if (isLastVisible) {
if (tryToShow) {
return;
}
if (!isFragmentVisible()) {
onFragmentPause();
isLastVisible = false;
}
// 上次不可見
} else {
boolean tryToHide = !tryToShow;
if (tryToHide) {
return;
}
if (isFragmentVisible()) {
onFragmentResume(isFirst, isViewDestroyed);
isLastVisible = true;
isFirst = false;
}
}
}
/**
* Fragment是否可見
*
* @return
*/
public boolean isFragmentVisible() {
if (isResuming()
&& getUserVisibleHint()
&& !hidden) {
return true;
}
return false;
}
/**
* Fragment 是否在前臺。
*
* @return
*/
private boolean isResuming() {
return isResuming;
}
/**
* Fragment 可見時回撥
*
* @param isFirst 是否是第一次顯示
* @param isViewDestroyed Fragment中的View是否被回收過。
* 存在這種情況:Fragment 的 View 被回收,但是Fragment例項仍在。
*/
protected void onFragmentResume(boolean isFirst, boolean isViewDestroyed) {
}
/**
* Fragment 不可見時回撥
*/
protected void onFragmentPause() {
}
}
值得注意:
1. 如果Fragment存在巢狀,那麼父Fragment的可見狀態將影響到子Fragment的可見狀態。所以父Fragment在可見狀態改變時要通知子Fragment可見狀態發生改變了。
2. 考慮一種情況,Fragment的View被回收,但是Fragment例項仍在,這種在ViewPager中是存在的。所以在onFragmentResume方法中我加入了一個isViewDestroyed表示Fragment的View是否被收回過。
懶載入實現如下:
@Override
protected void onFragmentResume(boolean isFirst, boolean isViewDestroy) {
if(isFirst){
loadData();
} else if(isViewDestroy){
// 為View設定資料。當然也可以在onCreateView或者onActivityCreate中設定資料。
setViews();
}
}
友盟統計Fragment時長只需把原來onResume和onPuase中的方法copy到onFragmentResume及onFragmentPause方法中即可。