[Fragment] lazy loading implementation

Keywords: Mobile Fragment

I. why lazy loading

Lazy loading means loading when needed.

1.1. Why does Fragment need lazy loading?

Generally, we will start some data loading operations in onCreate() or onCreateView(), such as loading from local or server.

In most cases, this will not cause any problems, but when you use ViewPager + Fragment, the problem will come. At this time, you should consider whether you need to implement lazy loading.

1.2 viewpager + fragment pit

ViewPager has a caching mechanism in order to have a good user experience when sliding, that is, to prevent jamming.

By default, ViewPager creates two fragments next to the current Fragment in advance.

For example, if you are currently displaying Fragment No. 3, the fragments No. 2 and No. 4 have been created. That is to say, the three fragments have already executed the life cycle function between onattach() - > onresume().

Originally, the onResume() of Fragment indicates that the current Fragment is visible and interactive. However, due to the caching mechanism of ViewPager, it has lost its meaning. That is to say, we only open the "3" Fragment, but in fact, the data of "2" and "4" fragments have been loaded.

If the operation of loading data is time-consuming or similar pictures occupy a lot of memory, then you should consider whether to implement lazy loading. That is, when I open which Fragment, it will load the data.

II. Implementation of lazy loading

2.1. Is setUserVisibleHint(boolean isVisibleToUser) feasible?

Many people recommend loading data in this function. isVisibleToUser indicates whether the current Fragment is visible.

If you just need lazy data loading, this is OK, but if you still have the following requirements, then this method will not work:

  • If you need to perform some control operations when Fragment is visible, such as display loading control
  • If you also need to do something when the Fragment is from "visible" to "invisible", for example, cancel loading the control display.

Here again, setUserVisibleHint() may be called outside the Fragment life cycle, that is, it may be called before the view is created, or it may be called after destroyView, so if some control operations are involved, a null exception may be reported, because the control has not been initialized or has been destroyed.

2.2 further improvement

A new callback function onFragmentVisibleChange(boolean isVisible) has been customized. The effect can be achieved as follows:

  • There are only two situations in which this function can be triggered:

    • One is that Fragment is triggered from "invisible - > visible" and passed in isVisible = true;
    • One is when Fragment is triggered from "visible - > invisible" and passed in isVisible = false
  • Control operation can be performed in this function without null exception.

  • Only the displayed Fragment and the left Fragment trigger the callback function, so that we can perform some operations when the visible state changes, because there will be no redundant false trigger.

In addition, because of the ViewPager caching mechanism, the subject owner reuses the view to prevent onCreateView() from creating the view repeatedly. In fact, the view is set as a member variable. When creating the view, judge whether it is null first.

Because when the Fragment is recycled and created in ViewPager, if the Fragment has already been created, only the oncreateview() - > ondestroyview() life function will be called, onCreate() and onDestroy will not be triggered, so the initialization and assignment of variables can be performed in onCreate(), so that repeated operations can be avoided.

The specific code is as follows:

/**
 *
 * Viewpager + Fragment In this case, the life cycle of the fragment is lost due to the caching mechanism of the Viewpager.
 * This abstract class defines a new callback method, which will be triggered when the visible state of fragment changes. For details, see the following
 *
 * @see #onFragmentVisibleChange(boolean)
 */
public abstract class ViewPagerFragment extends Fragment {

    /**
     * rootView Whether to initialize flag to prevent callback function from triggering when rootView is empty
     */
    private boolean hasCreateView;
    
    /**
     * Flag of whether the current Fragment is in visible state to prevent the trigger of callback function due to the caching mechanism of ViewPager
     */
    private boolean isFragmentVisible;
    
    /**
     * onCreateView()The view returned in is decorated as protected, so when a subclass inherits the class, the variable must be initialized in onCreateView.
     */
    protected View rootView;

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        Log.d(getTAG(), "setUserVisibleHint() -> isVisibleToUser: " + isVisibleToUser);
        if (rootView == null) {
            return;
        }
        hasCreateView = true;
        if (isVisibleToUser) {
            onFragmentVisibleChange(true);
            isFragmentVisible = true;
            return;
        }
        if (isFragmentVisible) {
            onFragmentVisibleChange(false);
            isFragmentVisible = false;
        }
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        initVariable();
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        if (!hasCreateView && getUserVisibleHint()) {
            onFragmentVisibleChange(true);
            isFragmentVisible = true;
        }
    }

    private void initVariable() {
        hasCreateView = false;
        isFragmentVisible = false;
    }

    /**************************************************************
     *  Custom callback method, subclass can be rewritten as required
     *************************************************************/

    /**
     * This method will be called back when the visible state of the current fragment changes
     * If the current fragment is loaded for the first time, the method will be called back after onCreateView. In other cases, the call back time is the same as {@ link ා (setuservisiblehint (Boolean)}.
     * In this callback method, you can do some loading data operations, even control operations, because with the view reuse mechanism of fragment, you don't need to worry about null exceptions in control operations.
     *
     * @param isVisible true  Invisible - > visible
     *                  false Visible - > invisible
     */
    protected void onFragmentVisibleChange(boolean isVisible) {
        Log.w(getTAG(), "onFragmentVisibleChange -> isVisible: " + isVisible);
    }
}


Usage:

Create the Fragment class you need, inherit ViewPagerFragment, initialize rootView in onCreateView(), rewrite onFragmentVisibleChange(), and perform the operations you need here, such as data loading, control display, etc.

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        if (rootView == null) {
            rootView = inflater.inflate(R.layout.fragment_android, container, false);
        
        }
        return rootView;
    } 
    
    @Override
    protected void onFragmentVisibleChange(boolean isVisible) {
        super.onFragmentVisibleChange(isVisible);
        if (isVisible) {
        //   do things when fragment is visible    
        //    if (ListUtils.isEmpty(mDataList) && !isRefreshing()) {
        //        setRefresh(true);
        //        loadServiceData(false);
            } else {
        //        setRefresh(false);
            }
        }
    }


3. Load once only

3.1 support lazy data loading and only load once

Sometimes, when we open a Fragment page, we want it to load data only when it is visible, that is, we don't start loading data in the background. Moreover, we also want to load data only when we first open the Fragment. If we reopen the Fragment later, we don't need to load data repeatedly.

Specifically, when using Fragment with ViewPager, due to the caching mechanism of ViewPager, when opening a Fragment, several fragments beside it have been created. If we are in Fragment onCreat() or onCreateView() to interact with the server and download the interface data, then these fragments that have been created will all come out. Now the background downloads the data. So we usually need to judge whether the current Fragment is visible in setUserVisibleHint(), and then download the data when it is visible. But there is still a problem, that is, every time it is visible, we will download the data repeatedly. We hope that only when it is visible for the first time, we need to do some judgment. This is to encapsulate a base class to do these things. See the following for the specific code.

3.2. Provide callback when Fragment is visible or invisible, and support you to perform some ui operations here, such as show / hide load box

Even if we make a lot of judgments in setUserVisibleHint(), we can load when we are visible and only load when we are visible for the first time, we may encounter other problems.

For example, when I download data, I need to operate ui directly and display the data, but sometimes I report the exception of ui control null. This is because setUserVisibleHint() is likely to be invoked before onCreateView() is created, and the data loading time is very short. This may lead to null exception. Then we need to make some judgements to ensure data. After downloading, the ui control has been created.

In addition to the requirement of lazy loading and only loading once, we may also need to display the data loading progress every time Fragment is opened or closed. Right, when we open a Fragment, if the data hasn't been downloaded, we should give a prompt of download progress or loading box. If we open a new Fragment page at this time, and then return again, if the data hasn't been loaded, then we should continue to give the prompt, right? This requires a callback method triggered when the Fragment is visible or invisible, and the method must be triggered after the view is created, so as to support the ui operation.

3.3. Support the reuse of view to prevent the problem of creating view repeatedly when using with ViewPager

3.4 example code

/*
 *
 * Fragment Base class, encapsulating the implementation of lazy loading
 *
 * 1,Viewpager + Fragment In this case, the life cycle of the fragment is lost due to the caching mechanism of the Viewpager
 The concrete significance
 * This abstract class defines a new callback method, which will be triggered when the visible state of fragment changes.
 And the method that will be called back when Fragment is first visible
 *
 * @see #onFragmentVisibleChange(boolean)
 * @see #onFragmentFirstVisible()
 */
public abstract class BaseFragment extends Fragment {

    private static final String TAG = BaseFragment.class.getSimpleName();

    private boolean isFragmentVisible;
    private boolean isReuseView;
    private boolean isFirstVisible;
    private View rootView;


    //setUserVisibleHint() is called once when Fragment is created.
       //Incoming isVisibleToUser = false
    //If the current Fragment is visible, setUserVisibleHint() will be called again.
       //Incoming isVisibleToUser = true
    //If the Fragment is visible - > invisible, setUserVisibleHint() will also be called.
       //Incoming isVisibleToUser = false
    //Summary: setUserVisibleHint() is called back when the visible state of Fragment changes.
        //It will also be called back when new Fragment().
    //If we need to do something when fragments are visible or invisible,
      //If you use this method directly, there will be redundant callbacks, so you need to re encapsulate a
    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        //setUserVisibleHint() may be called outside the fragment's life cycle
        if (rootView == null) {
            return;
        }
        if (isFirstVisible && isVisibleToUser) {
            onFragmentFirstVisible();
            isFirstVisible = false;
        }
        if (isVisibleToUser) {
            onFragmentVisibleChange(true);
            isFragmentVisible = true;
            return;
        }
        if (isFragmentVisible) {
            isFragmentVisible = false;
            onFragmentVisibleChange(false);
        }
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        initVariable();
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        //If setUserVisibleHint() is called before rootView is created, then
        //Call back onFragmentVisibleChange(true) after rootView is created
        //Ensure that the callback of onFragmentVisibleChange() occurs after the creation of rootView to support ui operations
        if (rootView == null) {
            rootView = view;
            if (getUserVisibleHint()) {
                if (isFirstVisible) {
                    onFragmentFirstVisible();
                    isFirstVisible = false;
                }
                onFragmentVisibleChange(true);
                isFragmentVisible = true;
            }
        }
        super.onViewCreated(isReuseView ? rootView : view, savedInstanceState);
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        initVariable();
    }

    private void initVariable() {
        isFirstVisible = true;
        isFragmentVisible = false;
        rootView = null;
        isReuseView = true;
    }

    /**
     * Set whether to use view reuse. It is enabled by default.
     * view Reuse means that ViewPager will call oncreateview() - > ondestroyview() continuously when destroying and rebuilding fragments. 
     * In this way, the view may be created repeatedly, resulting in multiple identical fragments displayed on the interface.
     * view The reuse of is actually to save the view created for the first time, and then return the view created for the first time when onCreateView() is used.
     *
     * @param isReuse
     */
    protected void reuseView(boolean isReuse) {
        isReuseView = isReuse;
    }

    /**
     * Remove the redundant callback scenario of setUserVisibleHint() to ensure that the callback can only be performed when the visible state of the fragment changes.
     * The callback time is after the view is created, so ui operation is supported to solve the problem that ui operation may report null exception in setUserVisibleHint().
     *
     * You can display and hide some ui in this callback method, such as the display and hide of load box.
     *
     * @param isVisible true  Invisible - > visible
     *                  false Visible - > invisible
     */
    protected void onFragmentVisibleChange(boolean isVisible) {

    }

    /**
     * When the fragment is first visible, the callback can load the data here to ensure that the data will be loaded only when the fragment is first opened.
     * This prevents data from being loaded repeatedly each time it enters
     * This method is called before onFragmentVisibleChange(), so when you first open it, you can use a global variable to indicate the status of data download.
     * Then set the status to download status in this method, and then execute the download task.
     * Finally, in onFragmentVisibleChange(), the display and hiding of the ui control is controlled according to the data download status.
     */
    protected void onFragmentFirstVisible() {

    }

    protected boolean isFragmentVisible() {
        return isFragmentVisible;
    }
}

It's easy to use. Create a new Fragment class that you need to inherit from the BaseFragment, then rewrite two callback methods, and perform corresponding operations in the callback method according to your needs, such as downloading data. For example:

public class CategoryFragment extends BaseFragment {
    private static final String TAG = CategoryFragment.class.getSimpleName();

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_category, container, false);
        initView(view);
        return view;
    }

    @Override
    protected void onFragmentVisibleChange(boolean isVisible) {
        if (isVisible) {
            //Update the interface data. If the data is still downloading, the loading box will be displayed.
            notifyDataSetChanged();
            if (mRefreshState == STATE_REFRESHING) {
                mRefreshListener.onRefreshing();
            }
        } else {
            //Close load box
            mRefreshListener.onRefreshFinish();
        }
    }

    @Override
    protected void onFragmentFirstVisible() {
        //Go to the server to download data
        mRefreshState = STATE_REFRESHING;
        mCategoryController.loadBaseData();
    }
}


matters needing attention:

  • If you want to reuse the layout of fragment s successfully, you need to rewrite the destroyItem() method in the adapter of viewpager to remove the super, that is, do not destroy the view.

  • If there is a problem of blank interface caused by switching back or non adjacent tabs, the solution is to copy the destroyItem() method in the adapter of reusing layout + ViewPager in onCreateView and remove the super.

Posted by ghornet on Mon, 21 Oct 2019 00:53:39 -0700