The past life and present life of fragment lazy loading

foreword

Fragment lazy loading is a common problem in android development. There are also many articles about lazy loading on the Internet. The reason I wrote this blog is because with the emergence of AndroidX and ViewPager2, lazy loading has different implementation methods, so this article introduces lazy loading in the context of time, so that readers have a comprehensive understanding of lazy loading. Later, I collectively refer to lazy loading as delayed loading .

Why do lazy loading of Fragment?

First of all, we have to figure out a problem. The "delay" in "Fragment lazy loading" does not refer to lazy loading of Fragment, but lazy loading of data in Fragment. For the use of Fragment, we usually combine ViewPager. ViewPager will preload at least one page on the left and right sides of the current page by default to ensure the smoothness of ViewPager. We assume that there are network requests in all Fragments of ViewPager. When we open this page, due to the preloading of ViewPager, even if other Fragments are not visible, it will make a network request to load data. And if the user quits the application or switches to other pages without sliding the ViewPager at all. So isn't the network request in this invisible Fragment a waste of both traffic and the performance of the mobile phone and server?
Then some students have problems at this time. Can't you load the data when the Fragment is displayed? Good question! Before answering, let's take a look at the life cycle of Fragment.
insert image description here
I think everyone should be very familiar with this picture. When a Fragment is preloaded, the Fragment's life cycle will be executed from onAttach to onResume. Obviously we can't control the lazy loading of Fragment through the life cycle of Fragment. Then what should be done? Let's look down.

How to delay loading

1.setUserVisibleHint

We already know that it is unrealistic to control the lazy loading of Fragment through the life cycle of Fragment, but fortunately, Fragment has a callback method setUserVisibleHint(boolean isVisibleToUser) to judge whether the fragment is visible. This method has a boolean parameter of isVisibleToUser, which means whether the current Fragment is visible to the user. This method is only called when using ViewPager, so it is only applicable to ViewPager+Fragment In other cases, you can use the onHiddenChanged method, which will be introduced later . Let's take a look at when setUserVisibleHint will be called (one ViewPager loads two Fragments).
insert image description here
You can see that setUserVisibleHint is called before onAttach. For the timing of setUserVisibleHint, you can read this article: From the perspective of source code, understand the magic of Fragment's setUserVisibleHint method , and the loaded fragment will not execute any life cycle methods when switching.

Because setUserVisibleHint is called before onAttach, we can't load the data before the Fragment is loaded. Therefore, for lazy loading, we can add flags in the setUserVisibleHint(isVisibleToUser: Boolean) method and onViewCreated(view: View, savedInstanceState: Bundle?) to control whether to load data. Take a look at the code:

public abstract class BaseFragment extends Fragment {
    
    
    /**
     * 当前Fragment状态是否可见
     */
    private boolean isVisibleToUser = false;
    /**
     * 是否已创建View
     */
    private boolean isViewCreated = false;
    /**
     * 是否第一次加载数据
     */
    private boolean isFirstLoad = true;

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
    
    
        super.setUserVisibleHint(isVisibleToUser);
        this.isVisibleToUser = isVisibleToUser;
        onLazyLoad();
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    
    
        super.onViewCreated(view, savedInstanceState);
        isViewCreated = true;
        onLazyLoad();
    }

    private void onLazyLoad() {
    
    
        if (isVisibleToUser && isViewCreated && isFirstLoad) {
    
    
            isFirstLoad = false;
            lazyLoad();
        }
    }

    public abstract void lazyLoad();
}

I will not analyze the code. When using it, you only need to inherit the Fragment from this BaseFragment, and then implement the lazyLoad method and load data in this method to achieve delayed loading.

2.onHiddenChanged

We said earlier that setUserVisibleHint is only called when ViewPager is used, so how to implement delayed loading if you control the display and hiding of fragments through the add hide show method of FragmentTransaction? The answer is to use the onHiddenChanged(boolean hidden) method. Judging from the name, this method is similar to setUserVisibleHint. His official API notes are as follows:

This function will be called when the hidden state of the Fragment changes. If the current Fragment is hidden, the value of hidden is true, otherwise it is false. The most important is the value of hidden, which can be obtained by calling the isHidden() function.

The onHiddenChanged method is called by FragmentTransaction, and the onHiddenChanged method will not be used when the display is loaded for the first time and destroyed.

public abstract class BaseFragment extends Fragment {
    
    
    /**
     * 当前Fragment状态是否可见
     */
    private boolean isVisibleToUser = false;
    /**
     * 是否已创建View
     */
    private boolean isViewCreated = false;
    /**
     * 是否第一次加载数据
     */
    private boolean isFirstLoad = true;

    @Override
    public void onHiddenChanged(boolean hidden) {
    
    
        super.onHiddenChanged(hidden);
        this.isVisibleToUser = !hidden;
        onLazyLoad();
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    
    
        super.onViewCreated(view, savedInstanceState);
        this.isVisibleToUser = isHidden();
        isViewCreated = true;
        onLazyLoad();
    }

    private void onLazyLoad() {
    
    
        if (isVisibleToUser && isViewCreated && isFirstLoad) {
    
    
            isFirstLoad = false;
            lazyLoad();
        }
    }

    public abstract void lazyLoad();
}

3.setMaxLifecycle

In Androidx 1.1.0 version, Google has optimized Fragment, making lazy loading also have a new solution. When we use the setUserVisibleHint method in Fragment, we will find that this method has been abandoned:
insert image description here
insert image description here
through the comment of setUserVisibleHint, we can know that setUserVisibleHint is replaced by the setMaxLifecycle method of FragmentTransaction. setMaxLifecycle is a new method added in Androidx 1.1.0. From the name, setMaxLifecycle means setting a maximum life cycle, because this method is in FragmentTransaction, so we can know that a maximum life cycle should be set for Fragment. Let's look at the source code of setMaxLifecycle:

    @NonNull
    public FragmentTransaction setMaxLifecycle(@NonNull Fragment fragment,
            @NonNull Lifecycle.State state) {
    
    
        addOp(new Op(OP_SET_MAX_LIFECYCLE, fragment, state));
        return this;
    }

This method receives a Fragment parameter and a Lifecycle status parameter. Lifecycle is a very important library in jetpack. It has the ability to perceive the life cycle of Activity and Fragment. I believe that many students should know a little about Lifecycle. Five life cycle states are defined in the State of Lifecycle, as follows:

   public enum State {
    
    
        /**
         * Destroyed state for a LifecycleOwner. After this event, this Lifecycle will not dispatch
         * any more events. For instance, for an {@link android.app.Activity}, this state is reached
         * <b>right before</b> Activity's {@link android.app.Activity#onDestroy() onDestroy} call.
         */
        DESTROYED,

        /**
         * Initialized state for a LifecycleOwner. For an {@link android.app.Activity}, this is
         * the state when it is constructed but has not received
         * {@link android.app.Activity#onCreate(android.os.Bundle) onCreate} yet.
         */
        INITIALIZED,

        /**
         * Created state for a LifecycleOwner. For an {@link android.app.Activity}, this state
         * is reached in two cases:
         * <ul>
         *     <li>after {@link android.app.Activity#onCreate(android.os.Bundle) onCreate} call;
         *     <li><b>right before</b> {@link android.app.Activity#onStop() onStop} call.
         * </ul>
         */
        CREATED,

        /**
         * Started state for a LifecycleOwner. For an {@link android.app.Activity}, this state
         * is reached in two cases:
         * <ul>
         *     <li>after {@link android.app.Activity#onStart() onStart} call;
         *     <li><b>right before</b> {@link android.app.Activity#onPause() onPause} call.
         * </ul>
         */
        STARTED,

        /**
         * Resumed state for a LifecycleOwner. For an {@link android.app.Activity}, this state
         * is reached after {@link android.app.Activity#onResume() onResume} is called.
         */
        RESUMED;

        /**
         * Compares if this State is greater or equal to the given {@code state}.
         *
         * @param state State to compare with
         * @return true if this State is greater or equal to the given {@code state}
         */
        public boolean isAtLeast(@NonNull State state) {
    
    
            return compareTo(state) >= 0;
        }
    }

From low to high, they are DESTROYED, INITIALIZED, CREATED, STARTED, RESUMED; and the life cycle status received in setMaxLifecycle must not be lower than CREATED, otherwise an IllegalArgumentException will be thrown. Therefore, except for the two life cycles of DESTROYED and INITIALIZED, only the parameters of the three life cycle states of CREATED, STARTED, and RESUMED are available.

  • CREATED is the created state. In a narrow sense, the life cycle method goes to onCreate. If the current fragment state is greater than CREATED, the fragment life cycle method will go to onDestoryView. If it is less than CREATED, it will go to onCreate; so there are two situations for CREATED;
  • Similarly, there are two situations in the STARTED state. If the current fragment state is greater than STARTED, the fragment life cycle method will go to onPause, and if it is less than CREATED, it will go to onStart;
  • The state represented by RESUMED is quite special, it only represents the onResume state, whether it is large or small or small to large, it will eventually stay in the onResume state;

Then we will study the effects of these three parameters one by one.

1. Do not set setMaxLifecycle

Let's first look at the state of adding a Fragment when setMaxLifecycle is not set, so as to compare with the latter situation. First we add a Fragment to the Activity, the code is as follows:

        FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
        fragmentTransaction.add(R.id.frameLayout, BaseFragment.newInstance());
        fragmentTransaction.commit();

Start the Activity, and we print out the log of the Fragment life cycle as follows:
insert image description here
You can see that the Fragment life cycle has been executed from onAttach to onResume. And the Fragment is successfully displayed in the Activity.

2.setMaxLifecycle为CREATED

Next, we set maxLifecycle to CREATED:

        FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
        Fragment fragment = BaseFragment.newInstance();
        fragmentTransaction.add(R.id.frameLayout, fragment);
        fragmentTransaction.setMaxLifecycle(fragment, Lifecycle.State.CREATED);
        fragmentTransaction.commit();

insert image description here
It can be seen that the life cycle of the Fragment is only executed until onCreate, and it is not executed any further. And the current Fragment is not loaded in the Activity.

So now the question is, assuming that the Fragment has been executed to onResume, what will happen if you set a CREATED maximum life cycle for the Fragment at this time? Let's verify it through the log:

        fragmentManager = getSupportFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragment = BaseFragment.newInstance();
        fragmentTransaction.add(R.id.frameLayout, fragment);
        fragmentTransaction.commit();
        findViewById(R.id.textView).setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                fragmentManager.beginTransaction().setMaxLifecycle(fragment, Lifecycle.State.CREATED).commit();
            }
        });

insert image description here
From the log, you can see that the Fragment that has executed onResume will execute onPause->onStop->onDestoryView after setting its maximum life cycle to CREATED. I have struggled here for a long time, and I don’t understand why it is onDestoryView. In fact, if we set it to CREATED and then set it to STARTED, this change can be understood: in fact, executing onDestoryView is equivalent to returning to CREATED. When setting STARTED, it happens to be executed from onCreateView after onCreate. If we set it to CREATED from the beginning and then set it to STARTED, then we will not go through the onResume stage but stop at onStart
insert image description here
.

3.setMaxLifecycle is STARTED

Next, we set maxLifecycle to STARTED:

        fragmentManager = getSupportFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragment = BaseFragment.newInstance();
        fragmentTransaction.add(R.id.frameLayout, fragment);
        fragmentTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
        fragmentTransaction.commit();

insert image description here
It can be seen that the life cycle of Fragment is executed to onStart, and the current fragment is successfully displayed in the Activity.
Similarly, what if the Fragment has been executed to the onResume method and then set the maximum life cycle to STARTED? Look at the log:

        fragmentManager = getSupportFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragment = BaseFragment.newInstance();
        fragmentTransaction.add(R.id.frameLayout, fragment);
        fragmentTransaction.commit();
        findViewById(R.id.textView).setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                fragmentManager.beginTransaction().setMaxLifecycle(fragment, Lifecycle.State.STARTED).commit();
            }
        });

insert image description here
It can be seen that Fragment executes the onPause method after setting the maximum life cycle STARTED.

4. setMaxLifecycle to RESUMED

Finally, we set maxLifecycle to RESUMED:

        fragmentManager = getSupportFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragment = BaseFragment.newInstance();
        fragmentTransaction.add(R.id.frameLayout, fragment);
        fragmentTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED).commit();
        fragmentTransaction.commit();

insert image description here
You can see the same effect as the first case at this time, and the life cycle of Fragment is executed to onResume.
And for the Fragment that has executed onResume, what happens if you set the maximum life cycle to RESUMED? Because the current Fragment is already in the RESUMED state, no code will be executed any more.

With the above conclusions, the life cycle of Fragment can be controlled in ViewPager, so as to realize the lazy loading function more conveniently.

5. Concrete implementation

Through the analysis in the previous section, we know that the maximum life cycle of Fragment can be set through setMaxLifecycle, so that the lazy loading of Fragment in ViewPager can be realized. Of course, we don’t need to implement the operation of life cycle state processing by ourselves. The FragmentStatePagerAdapter in Androidx 1.1.0 version has already implemented it for us, and we only need to pass in the corresponding parameters when using it.
The constructor of FragmentStatePagerAdapter receives two parameters, as follows:

    /**
     * Constructor for {@link FragmentStatePagerAdapter}.
     *
     * If {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} is passed in, then only the current
     * Fragment is in the {@link Lifecycle.State#RESUMED} state, while all other fragments are
     * capped at {@link Lifecycle.State#STARTED}. If {@link #BEHAVIOR_SET_USER_VISIBLE_HINT} is
     * passed, all fragments are in the {@link Lifecycle.State#RESUMED} state and there will be
     * callbacks to {@link Fragment#setUserVisibleHint(boolean)}.
     *
     * @param fm fragment manager that will interact with this adapter
     * @param behavior determines if only current fragments are in a resumed state
     */
    public FragmentStatePagerAdapter(@NonNull FragmentManager fm,
            @Behavior int behavior) {
    
    
        mFragmentManager = fm;
        mBehavior = behavior;
    }

Needless to say, the first FragmentManager parameter, the second parameter is an enumeration type Behavior parameter, and its optional values ​​are as follows:

    @Deprecated
    public static final int BEHAVIOR_SET_USER_VISIBLE_HINT = 0;

    public static final int BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT = 1;

When the behavior is BEHAVIOR_SET_USER_VISIBLE_HINT, it is the same as the first solution. When the Fragment changes, the setUserVisibleHint method will be called, that is, this parameter is actually for compatibility with the old code. And the BEHAVIOR_SET_USER_VISIBLE_HINT parameter has been deprecated.

When the behavior is BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT, it means that only the currently displayed Fragment will be executed until onResume, while the life cycle of other Fragments will only be executed until onStart, and when switching fragments, the displayed Fragment will be executed from onResume to onPause, and the fragment to be displayed will be executed from onStart to onResume. In this case, you only need to write the loaded code in onResume and add an initial loading judgment to achieve delayed loading.

public abstract class BaseFragment extends Fragment {
    
    

    private boolean isFirstLoad = true;
    
    @Override
    public void onResume() {
    
    
        super.onResume();
        if (isFirstLoad) {
    
    
            isFirstLoad = false;
        }
    }

    public abstract void lazyLoad();
}

How does FragmentStatePagerAdapter help us realize this function through setMaxLifecycle? Look at the source code:

    public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
    
    
        //将要切换的fragment
        Fragment fragment = (Fragment) object;
        //设置正在显示的fragment
        // 如果是BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT就使用setMaxLifecycle
        // 如果是BEHAVIOR_SET_USER_VISIBLE_HINT使用setUserVisibleHint
        if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
    
    
            if (mCurTransaction == null) {
    
    
                mCurTransaction = mFragmentManager.beginTransaction();
            }
            mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED);
        } else {
    
    
            mCurrentPrimaryItem.setUserVisibleHint(false);
        }
        //设置将要切换的fragment
        // 如果是BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT就使用setMaxLifecycle
        // 如果是BEHAVIOR_SET_USER_VISIBLE_HINT使用setUserVisibleHint
        if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
    
    
            if (mCurTransaction == null) {
    
    
                mCurTransaction = mFragmentManager.beginTransaction();
            }
            mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED);
        } else {
    
    
            fragment.setUserVisibleHint(true);
        }

        mCurrentPrimaryItem = fragment;
    }

This method is the code called when viewPager controls the switching of fragments. In order to make the code easier to understand, I made adjustments. mCurrentPrimaryItem is the item currently being displayed, and fragment is the item to be displayed next. It can be seen that when mBehavior is BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT, the maximum life cycle of mCurrentPrimaryItem is set to STARTED, and the maximum life cycle of fragment is set to RESUMED. When the mBehavior is BEHAVIOR_SET_USER_VISIBLE_HINT, the setUserVisibleHint method will still be called. This situation will not be discussed anymore, because BEHAVIOR_SET_USER_VISIBLE_HINT has also been discarded.

Then let's analyze the situation when BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT:
mCurrentPrimaryItem is the currently displayed Fragment, so the Fragment must have executed the onResume method, and at this time the maximum life cycle STARTED is set for it, then mCurrentPrimaryItem must execute onPause to return to the STARTED state. The current life cycle state of the fragment is onStart. When the maximum life cycle state of RESUME is set for it, the fragment will inevitably execute the onResume method to enter the RESUMED state.

4.ViewPager2

The default value of offScreenPageLimit of ViewPager2 is OFFSCREEN_PAGE_LIMIT_DEFAULT. When setOffscreenPageLimit is OFFSCREEN_PAGE_LIMIT_DEFAULT, the caching mechanism of RecyclerView will be used. By default, only the currently displayed Fragment will be loaded, and at least one item will not be preloaded like ViewPager. When switching to the next item, the current Fragment will execute the onPause method, and the next Fragment will execute from onCreate to onResume. When sliding back to the first page again, the current page will also execute onPuase, and the first page will execute onResume.

That is to say, in ViewPager2, the preloading mechanism is turned off by default. It doesn't make any sense to talk about lazy loading without the preloading mechanism. So there is no need to say more about the delayed loading of ViewPager2, right? Just put the network request in onStart, or write the loaded code in onResume as above and add an initial loading judgment.

Summarize

The past and present life of delayed loading is finished here. . . . . . . .
Reference:
Fragment new function, setMaxLifecycle Learn about
the new implementation of Fragment lazy loading under Androidx

Guess you like

Origin blog.csdn.net/shanshui911587154/article/details/106762770