1. 程式人生 > >ViewPage Fragment 懶載入

ViewPage Fragment 懶載入

最近遇到這麼一個問題,ViewPage+Fragment的傳統佈局,ABC三個Fragment,其中BC都是內嵌一個RecyclerView,非同步載入item,經常遇到一個現象就是點選b的時候,a,c也請求載入了,log如下:

08-08 17:57:39.601 26333-26333/com.nuctech.tr.trapp I/SetupHardwareParameterFragment: onAttach: 
08-08 17:57:39.601 26333-26333/com.nuctech.tr.trapp I/SetupHardwareParameterFragment: onCreateView: 
08-08 17:57:39.601 26333-26333/com.nuctech.tr.trapp I/SetupHardwareParameterFragment: onViewCreated: 
08-08 17:57:39.601 26333-26333/com.nuctech.tr.trapp I/SetupHardwareParameterFragment: onActivityCreated: 
08-08 17:57:39.601 26333-26333/com.nuctech.tr.trapp I/SetupHardwareParameterFragment: onResume: 
08-08 18:00:06.201 28740-28740/com.nuctech.tr.trapp I/SetupAlgorithmparameterFragment: onAttach: 
08-08 18:00:06.201 28740-28740/com.nuctech.tr.trapp I/SetupAlgorithmparameterFragment: onCreateView: 
08-08 18:00:06.201 28740-28740/com.nuctech.tr.trapp I/SetupAlgorithmparameterFragment: onViewCreated: 
08-08 18:00:06.201 28740-28740/com.nuctech.tr.trapp I/SetupAlgorithmparameterFragment: onActivityCreated: 
08-08 18:00:06.201 28740-28740/com.nuctech.tr.trapp W/FragmentManager: moveToState: Fragment state for SetupAlgorithmparameterFragment{20f1bd68 #4 id=0x7f0f012e android:switcher:2131689774:2} not updated inline; expected state 3 found 2
08-08 18:00:06.201 28740-28740/com.nuctech.tr.trapp I/SetupAlgorithmparameterFragment: onResume: 
A不貼了,這樣的後果就是點選B的時候AC生命週期都走完了,再點選C的時候,生命週期沒有變化。。。

1.於是乎我放在C生命週期裡的請求在點選B的時候就執行完了,對於移動端的效能還是有影響的。

2.更可氣的是我的RecyclerView有一定機率不出現資料(後面有解決辦法)。

於是乎懶載入就得提幾句了。

懶載入的技術關鍵點是什麼?

根據定義:

所謂懶載入,即Fragment 的 UI 對使用者可見時才載入資料。

需要判斷何時 Fragment 的 UI 才對使用者可見

如何判斷 Fragment 的 UI 是否對使用者可見?

Fragment 提供了一個方法 public void setUserVisibleHint(boolean isVisibleToUser)

,API 的註釋如下

Set a hint to the system about whether this fragment's UI is currently visible to the user. This hint defaults to true and is persistent across fragment instance state save and restore.

所以,只需要判斷引數 isVisibleToUser 是否為 True 即可知道該 Fragment 的 UI 是否對使用者可見。

setUserVisibleHint 在什麼時候呼叫?

對於單個 Fragment,setUserVisibleHint 是不會被呼叫的,只有該 Fragment 在 ViewPager 裡才會被呼叫。所以,我寫了一個 ViewPager + Fragment 的 Demo,列印了一下 Log。

D/Owen_TestFragment: setUserVisibleHint: isVisibleToUser = false
D/Owen_TestFragment: onAttach
D/Owen_TestFragment: onCreate
D/Owen_TestFragment: setUserVisibleHint: isVisibleToUser = true
D/Owen_TestFragment: onCreateView
D/Owen_TestFragment: onActivityCreated
D/Owen_TestFragment: onStart
D/Owen_TestFragment: onResume
D/Owen_TestFragment: onPause
D/Owen_TestFragment: onPause 
D/Owen_TestFragment: onStop
D/Owen_TestFragment: onDestroyView
D/Owen_TestFragment: onDestroy
D/Owen_TestFragment: onDetach

可以看到 setUserVisibleHint 的執行順序是

setUserVisibleHint(false) -> onAttach -> onCreate -> setUserVisibleHint(true)  -> onCreateView -> onActivityCreated ->.... -> onDetach

程式碼

為了方便,封裝一個基類 LazyLoadFragment,提供一個 loadData() 方法供呼叫去載入資料

public abstract class LazyLoadFragment extends Fragment {

    /**
     * 控制元件是否初始化完成
     */
    private boolean isViewCreated;
    /**
     * 資料是否已載入完畢
     */
    private boolean isLoadDataCompleted;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(getLayout(), container, false);
        initViews(view);
        isViewCreated = true;
        return view;
    }

    public abstract int getLayout();
    public abstract void initViews(View view);

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        if (isVisibleToUser && isViewCreated && !isLoadDataCompleted) {
            loadData();
        }
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        if (getUserVisibleHint()) {
            loadData();
        }
    }

    public void loadData() {
        isLoadDataCompleted = true;
    }
}

等等,為什麼 loadData() 會在兩個地方執行?在 setUserVisibleHint 方法裡執行我還能理解,為什麼 onActivityCreated 也要執行呢?

因為,ViewPager 預設顯示第一頁,第一頁肯定要先載入資料啊,而且 setUserVisibleHint 的執行順序又是在 onCreatView 之前,同時 onCreatView 需要初始化介面和修改 isViewCreated 的值。所以就需要在 onActivityCreated 裡執行一次咯。

等等

文章寫到這裡,我聽到了一個不同的聲音

ViewPager 不是有 setOffscreenPageLimit(int limit) 方法嗎?我呼叫 viewPager.setOffscreenPageLimit(0) 不就行了嗎?

我想說:思路是對的,但是這樣做沒效果。為什麼?看一下 setOffscreenPageLimit 的方法實現就知道了

private static final int DEFAULT_OFFSCREEN_PAGES = 1;

public void setOffscreenPageLimit(int limit) {
    if (limit < DEFAULT_OFFSCREEN_PAGES) {
        Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " +
                DEFAULT_OFFSCREEN_PAGES);
        limit = DEFAULT_OFFSCREEN_PAGES;
    }
    if (limit != mOffscreenPageLimit) {
        mOffscreenPageLimit = limit;
        populate();
    }
}

limit 預設為 1 ,就算傳一個 0 也無濟於事啊。

總結

  • 懶載入的技術關鍵點
  • setUserVisibleHint 的執行順序
  • 為什麼 ViewPager.setOffscreenPageLimit(0) 無效?


至於我RecylerView的有時不出資料,滑動一下才出來我就讓他載入的時候劃一下被。。。上其中一個fragment的所有原始碼。
package com.nuctech.tr.trapp.fragments;

import android.content.Context;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.nuctech.tr.trapp.R;
import com.nuctech.tr.trapp.adapter.recycleview.DividerItemDecoration;
import com.nuctech.tr.trapp.plugin.recycler_plugin.adapter.LoadMoreAdapter.OnLoadMoreListener;
import com.nuctech.tr.trapp.plugin.recycler_plugin.utils.RecyclerPlugin;
import com.nuctech.tr.trapp.present.SetupDeviceparaPresenter;
import com.nuctech.tr.trapp.view.IParaView;

import java.lang.ref.WeakReference;

import butterknife.Bind;
import butterknife.ButterKnife;


/**
 * A simple {@link Fragment} subclass.
 * Activities that contain this fragment must implement the
 * {@link SetupHardwareParameterFragment.OnFragmentInteractionListener} interface
 * to handle interaction events.
 * Use the {@link SetupHardwareParameterFragment#newInstance} factory method to
 * create an instance of this fragment.
 */
public class SetupHardwareParameterFragment extends Fragment implements OnLoadMoreListener, IParaView {


    private static final String TAG = "SetupHardwareParameterFragment";
    @Bind(R.id.id_recyclerview)
    RecyclerView idRecyclerview;
    //懶載入
    /**
     * 控制元件是否完成初始化
     */
    private boolean isViewCreated;
    /**
     * 資料是否已經載入完畢
     */
    private boolean IsLoadDataCompleted;

    public SetupDeviceparaPresenter getSetupDeviceparaPresenter() {
        return setupDeviceparaPresenter;
    }

    public void setSetupDeviceparaPresenter(SetupDeviceparaPresenter setupDeviceparaPresenter) {
        this.setupDeviceparaPresenter = setupDeviceparaPresenter;
    }

    private SetupDeviceparaPresenter setupDeviceparaPresenter;

    private Context mContext;
    private RecyclerPlugin plugin;

    public SetupHardwareParameterFragment() {
        // Required empty public constructor
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        Log.i(TAG, "onAttach: ");
        this.mContext = context;
        if (setupDeviceparaPresenter == null)
            setupDeviceparaPresenter = new SetupDeviceparaPresenter(mContext);
        setupDeviceparaPresenter.attach((IParaView) this);
    }

    @Override
    public void onDetach() {
        super.onDetach();
        Log.i(TAG, "onDetach: ");
        this.mContext = null;
        setupDeviceparaPresenter.dettach();
    }


    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        Log.i(TAG, "onCreateView: ");
        // Inflate the layout for this fragment
        View view = inflater.inflate(R.layout.fragment_setup_hardware_parameter, container, false);
        ButterKnife.bind(this, view);
        idRecyclerview.addItemDecoration(new DividerItemDecoration(mContext, DividerItemDecoration.VERTICAL_LIST));
        LinearLayoutManager manager = new LinearLayoutManager(mContext, LinearLayoutManager.VERTICAL, false);
        idRecyclerview.setLayoutManager(manager);
        idRecyclerview.setItemAnimator(new DefaultItemAnimator());

        plugin = new RecyclerPlugin(mContext, idRecyclerview, setupDeviceparaPresenter.getDeviceParaAdapter());
        plugin.createAddMore(getActivity().getLayoutInflater(), false, this);
        idRecyclerview.setAdapter(plugin.getLastAdapter());
        isViewCreated = true;
        return view;
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        Log.i(TAG, "onViewCreated: ");
//        setupDeviceparaPresenter.bindData();
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        Log.i(TAG, "onActivityCreated: ");
        if (getUserVisibleHint())
            setupDeviceparaPresenter.bindData();
    }

    @Override
    public void onResume() {
        super.onResume();
        Log.i(TAG, "onResume: ");

    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        Log.i(TAG, "onDestroyView: ");
        ButterKnife.unbind(this);

    }

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        if (isVisibleToUser && isViewCreated && !IsLoadDataCompleted) {
            setupDeviceparaPresenter.bindData();
            Log.i(TAG, "setUserVisibleHint: ");
        }
    }

    @Override
    public void onLoadMoreRequested() {

    }


    @Override
    public void updateData() {
        ((AppCompatActivity) mContext).runOnUiThread(new MyRunnable(this));
        IsLoadDataCompleted = true;

    }

    public static class MyRunnable implements Runnable {

        WeakReference<SetupHardwareParameterFragment> weakReference;

        public MyRunnable(SetupHardwareParameterFragment setupAlgorithmparaPresenter) {
            weakReference = new WeakReference<SetupHardwareParameterFragment>(setupAlgorithmparaPresenter);
        }

        @Override
        public void run() {
            if (weakReference.get() != null) {
                weakReference.get().idRecyclerview.getAdapter().notifyDataSetChanged();
                if (weakReference.get().idRecyclerview.getAdapter().getItemCount() > 0)
                    weakReference.get().idRecyclerview.scrollToPosition(0);
            }

        }
    }
}

下班~