1. 程式人生 > >Fragment懶載入(支援巢狀) 友盟統計Fragment時長最佳實踐

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()

方法判斷Fragment是否在螢幕之中。
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方法中即可。