In the comments on the ViewPager2 article last week, people were generally concerned about the problem of lazy Fragment loading. In fact, there is no lack of excellent articles on the Internet about the problem of lazy Fragment loading. However, lazy loading is not handled gracefully due to the Fragment lifecycle. Clearly, Google is aware of the problem. Therefore, the Androidx library is deeply optimized to control the lifecycle state of fragments, making it easier for us to control the lifecycle of fragments and to deal with lazy loading. However, we need to understand what Google optimizes for fragments. So let’s take this opportunity to explore it together! (Lazy loading is better called lazy loading, so I’ll just call it lazy loading.)

Fragment lazy loading in the past life

Although this article is an exploration of the new features of fragments, I feel that there has to be a cause and effect in writing this article. And for those of you who don’t know what lazy loading is. Let’s take a look at lazy loading first and revisit the old method of Fragment lazy loading.

1. Why do FRAGMENTS need lazy loading?

First, let’s get one thing straight. The “delay” in “Fragment lazy loading” does not mean lazy loading the Fragment, but lazy loading the data in the Fragment. The use of fragments is usually combined with a ViewPager. We also mentioned the preloading problem of ViewPager in the article “Learn! Learn More about ViewPager2”. By default, ViewPager preloads at least one page on either side of the current page to ensure smooth ViewPager performance. Let’s assume that there are network requests in all fragments of the ViewPager. When we open this page, the ViewPager will preload the data even if the Fragment is not visible. If the user doesn’t swipe at all, the ViewPager exits the app or switches to another page. Don’t network requests in this invisible Fragment waste both traffic and performance on the phone and the server?

Now some of you have a question. Can’t you load data while the Fragment is displayed? Good question! Let’s take a look at the lifecycle of a Fragment before solving this problemI think you’re all familiar with this picture. When the Fragment is preloaded, the lifecycle of the Fragment is executed from onAttach to onResume. Obviously, there is no way to control the lazy loading of fragments through their lifecycle. So what to do? So let’s move on.

2. How to handle lazy loading of fragments?

From the analysis in the previous section, we know that it is impossible to handle lazy loading in the lifecycle of a Fragment. So if you want to handle lazy loading of fragments, you need to think differently. Thankfully, the Fragment gives us a setUserVisibleHint(isVisibleToUser: This method takes a Boolean parameter of type isVisibleToUser, which indicates whether the current Fragment is visible to the user. Therefore, for lazy loading of fragments we can use this method to expand. Since you are using setUserVisibleHint(isVisibleToUser: Boolean) you should know when this method is called. Let’s write an example of a ViewPager nested Fragment to print logs:

Note: In the log above, position:0 indicates the current Fragment, and position:1 indicates the preloaded Fragment. The same as the following.

You can see that this method was called before the onAttach of the Fragment. Therefore, for lazy loading we can use the setUserVisibleHint(isVisibleToUser: Boolean) method and onViewCreated(View: view, savedInstanceState: Bundle?) method. Add flag bits to control whether data is loaded. Let’s look at the code:

abstract class BaseLazyFragment : Fragment() {
    /** * Whether the current Fragment state is visible */
    private var isVisibleToUser: Boolean = false
    /** * Whether a View */ has been created
    private var isViewCreated: Boolean = false
    /** whether to load data for the first time */
    private var isFirstLoad = true

    override fun setUserVisibleHint(isVisibleToUser: Boolean) {
        super.setUserVisibleHint(isVisibleToUser)
        this.isVisibleToUser = isVisibleToUser
        onLazyLoad()
    }

    private fun onLazyLoad(a) {
        if (isVisibleToUser && isViewCreated && isFirstLoad) {
            isFirstLoad = false
            lazyLoad()
        }
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?). {
        super.onViewCreated(view, savedInstanceState)
        isViewCreated = true
        onLazyLoad()
    }

    protected abstract fun lazyLoad(a)

}
Copy the code

We implemented lazy loading by adding three flag bits to the Fragment. Let’s go to the TestFragment and try this:

class TestFragment : BaseLazyFragment() {

    private var position: Int = 0

    override fun setUserVisibleHint(isVisibleToUser: Boolean) {
        super.setUserVisibleHint(isVisibleToUser)
        valbundle = arguments position = bundle!! .getInt(KEY_POSITION) }override fun onCreateView(inflater: LayoutInflater, container: ViewGroup? , savedInstanceState:Bundle?).: View? {
        valcardView = CardView(inflater, container) cardView.bind(Card.fromBundle(arguments!!) ,position)return cardView.view
    }

    companion object {

        private const val KEY_POSITION = "position"

        fun getInstance(card: Card, position: Int): TestFragment {
            val fragment = TestFragment()
            val bundle = card.toBundle()
            bundle.putInt(KEY_POSITION, position)
            fragment.arguments = bundle
            return fragment
        }
    }

    override fun lazyLoad(a) {
        showToast("Fragment$position is loading data")}private fun showToast(content: String) {
        Toast.makeText(context, content, Toast.LENGTH_SHORT).show()
    }

}

Copy the code

Let’s look at the effect:Well! Loading data is performed only when the Fragment is fully displayed. This lazy loading scheme was widely used before Androidx version 1.1.0. With Androidx 1.1.0, Google has optimized the handling of fragments to provide a new solution for lazy loading.

2, Fragment setMaxLifecycle inquiry

In the last section we said that because the ViewPager preload mechanism and the Fragment lifecycle cannot be controlled, we had to use setUserVisibleHint(isVisibleToUser: Boolean) and onViewCreated(View: Boolean) View, savedInstanceState: Bundle?) Method and adding three flag bits to handle lazy loading. Obviously, such code is not elegant.

The setUserVisibleHint(isVisibleToUser: Boolean) method we used in the first section has been flagged as deprecated after we migrated the Android project to Android X and upgraded the Android X version to 1.1.0!

     / * * *... Omit other comments *@deprecated Use {@link FragmentTransaction#setMaxLifecycle(Fragment, Lifecycle.State)}
     * instead.
     */
    @Deprecated
    public void setUserVisibleHint(boolean isVisibleToUser) {
        if(! mUserVisibleHint && isVisibleToUser && mState < STARTED && mFragmentManager ! =null && isAdded() && mIsCreated) {
            mFragmentManager.performPendingDeferredStart(this); } mUserVisibleHint = isVisibleToUser; mDeferStart = mState < STARTED && ! isVisibleToUser;if(mSavedFragmentState ! =null) {
            // Ensure that if the user visible hint is set before the Fragment has
            // restored its state that we don't lose the new valuemSavedUserVisibleHint = isVisibleToUser; }}Copy the code

The setUserVisibleHint method is replaced by the FragmentTransaction#setMaxLifecycle(Fragment, Lifecycle.state) method. SetMaxLifecycle is a new method added to Androidx 1.1.0. SetMaxLifecycle by name means to set a maximum lifetime. Since this method is in a FragmentTransaction, we know that it should be set to a maximum lifetime for the Fragment. Let’s look at the source code for setMaxLifecycle:

 /** * Set a ceiling for the state of an active fragment in this FragmentManager. If fragment is * already above the received state, it will be forced down to the correct state. * * <p>The fragment provided must currently be added to the FragmentManager  to have it's * Lifecycle state capped, or previously added as part of this transaction. The * {@link Lifecycle.State} passed in must at least be {@link Lifecycle.State#CREATED}, otherwise
     * an {@link IllegalArgumentException} will be thrown.</p>
     *
     * @param fragment the fragment to have it's state capped.
     * @param state the ceiling state for the fragment.
     * @return the same FragmentTransaction instance
     */
    @NonNull
    public FragmentTransaction setMaxLifecycle(@NonNull Fragment fragment,
            @NonNull Lifecycle.State state) {
        addOp(new Op(OP_SET_MAX_LIFECYCLE, fragment, state));
        return this;
    }
Copy the code

This method accepts a Fragment parameter and a status parameter for Lifecycle. Lifecycle is an important library in jetpack. It has the ability to perceive the Lifecycle of activities and fragments. I believe that many students should know something about Lifecycle. Five Lifecycle states are defined in Lifecycle’s State, 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; }}Copy the code

The life cycle state received in setMaxLifecycle must not be less than CREATED, otherwise an IllegalArgumentException will be thrown. An exception as shown in the figure below will be thrown when an argument is passed as DESTROYED or INITIALIZED:

Therefore, apart from the two life cycles, only the CREATED, STARTED, and RESUMED life cycle states are available. Next, we will study the effects of these three parameters one by one.

1. Do not set setMaxLifecycle

Let’s look at the state of adding a Fragment without setting setMaxLifecycle to compare this with what happens later. To do this, add a Fragment to the Activity.

	fragment = TestLifecycleFragment.getInstance(Card.DECK[0].0)
        val fragmentTransaction = supportFragmentManager.beginTransaction()
        fragmentTransaction.add(R.id.ll_fragment, fragment)
        fragmentTransaction.commit()
Copy the code

Start the Activity and print the log of the Fragment lifecycle as follows:You can see that the Fragment lifecycle goes from onAttach to onResume. The Fragment is successfully displayed in the Activity

2. SetMaxLifecycle and CREATED

Next, we set maxLifecycle to CREATED:

        fragment = TestLifecycleFragment.getInstance(Card.DECK[0].0)
        val fragmentTransaction = supportFragmentManager.beginTransaction()
        fragmentTransaction.add(R.id.ll_fragment, fragment)
        fragmentTransaction.setMaxLifecycle(fragment, Lifecycle.State.CREATED)
        fragmentTransaction.commit()
Copy the code

Take a look at the log output:You can see that the life cycle of the Fragment is just up to onCreate and no further. The current Fragment is not loaded in the Activity.

If the Fragment is already onResume, what happens if you set a CREATED maximum life cycle for the Fragment? Let’s check the log:

OnPause ->onStop->onDestoryView: onPause->onStop->onDestoryView So we fall back to the onCreate state.

3. SetMaxLifecycle and STARTED

Next, we set maxLifecycle to STARTED:

        fragment = TestLifecycleFragment.getInstance(Card.DECK[0].0)
        val fragmentTransaction = supportFragmentManager.beginTransaction()
        fragmentTransaction.add(R.id.ll_fragment, fragment)
        fragmentTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED)
        fragmentTransaction.commit()
Copy the code

The log output is as follows:You can see that the Fragment lifecycle goes to onStart, and the current Fragment is successfully displayed in the Activity.

Also, suppose that the Fragment has already executed the onResume method and set its maximum lifetime to STARTED? Look at the log:As you can see, the Fragment executes the onPause method after setting the maximum lifecycle STARTED, which means that the lifecycle returns to onStart.

4. SetMaxLifecycle and RESUMED

Finally, we set maxLifecycle to RESUMED:

        fragment = TestLifecycleFragment.getInstance(Card.DECK[0].0)
        val fragmentTransaction = supportFragmentManager.beginTransaction()
        fragmentTransaction.add(R.id.ll_fragment, fragment)
        fragmentTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED)
        fragmentTransaction.commit()
Copy the code

You can see the same effect as in the first case, the lifecycle of the Fragment goes to onResume.

For fragments that have executed onResume, how about setting the maximum life cycle to RESUMED? Because the Fragment is RESUMED, no code is executed.

At this point we can draw a conclusion:

The setMaxLifecycle method allows you to accurately control the life cycle of a Fragment. If the life cycle of a Fragment is less than the maximum life cycle, the Fragment will run to the maximum life cycle. Otherwise, the Fragment will run to the maximum life cycle. If the Fragment’s life cycle state is greater than the maximum life cycle, the Fragment will fall back to the maximum life cycle.

With this conclusion, you can control the lifecycle of the Fragment in ViewPager, which makes lazy loading easier.

3, Fragment lazy load this life

1, lazy loading new scheme for ViewPager

In the previous section, we know that setMaxLifecycle can be used to set the maximum lifecycle of fragments, which can enable lazy loading of fragments in ViewPager. Of course, about the life cycle state handling operation without our own implementation, in Androidx 1.1.0 version of FragmentStatePagerAdapter has helped us to achieve, only need to use when in the corresponding parameter.

FragmentStatePagerAdapter structure method takes 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;
    }
Copy the code

The first FragmentManager argument is needless to say, and the second argument is an enumeration of type Behavior with the following optional values:

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({BEHAVIOR_SET_USER_VISIBLE_HINT, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT})
    private @interface Behavior { }
Copy the code

When the behavior is BEHAVIOR_SET_USER_VISIBLE_HINT and the Fragment changes, the setUserVisibleHint method will be called, which means that the parameter is actually compatible with the old code. The BEHAVIOR_SET_USER_VISIBLE_HINT parameter is disabled. So the only optional parameter left is BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT.

When the behaviors is BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT mean that only the currently displayed fragments will be executed to onResume, and all the other fragments of life cycle will only perform the onStart.

How does this function work? We followed the BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT step to find the setPrimaryItem method, which sets the Item currently displayed on the ViewPager. The source code is as follows:

public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        Fragment fragment = (Fragment)object;
        if(fragment ! = mCurrentPrimaryItem) {if(mCurrentPrimaryItem ! =null) {
                mCurrentPrimaryItem.setMenuVisibility(false);
                if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
                    if (mCurTransaction == null) {
                        mCurTransaction = mFragmentManager.beginTransaction();
                    }
                    mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED);
                } else {
                    mCurrentPrimaryItem.setUserVisibleHint(false);
                }
            }
            fragment.setMenuVisibility(true);
            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; }}Copy the code

This code is pretty straightforward, with mCurrentPrimaryItem being the item currently displayed and Fragment being the item to be displayed next. You can see that when mBehavior is BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT, the maximum lifecycle of mCurrentPrimaryItem is set to STARTED. The maximum life cycle of fragments is set to RESUMED. When mBehavior is BEHAVIOR_SET_USER_VISIBLE_HINT, the setUserVisibleHint method is still called, and this case is not discussed. Because the BEHAVIOR_SET_USER_VISIBLE_HINT is also disabled. Let’s look at the behavior of the BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT:

MCurrentPrimaryItem is the Fragment currently displayed, so the Fragment must have already executed onResume with the maximum lifetime set to STARTED. Then mCurrentPrimaryItem must perform onPause back to the STARTED state. The fragment’s current life cycle state is onStart. If the maximum life cycle state of RESUME is set for a fragment, the fragment must run the onResume method to enter the RESUMED state.

With that in mind, isn’t it easy to control lazy loading? At this point, we only need a flag to indicate whether the data is loaded for the first time. Therefore, lazy loading can be implemented as follows:

abstract class TestLifecycleFragment : Fragment() {
    private var isFirstLoad = true

    override fun onResume(a) {
        super.onResume()
        if (isFirstLoad) {
        	isFirstLoad = false
            loadData()
        }
    }

    abstract fun loadData(a)
}
Copy the code

Lazy loading for ViewPager2

ViewPager2: ViewPager2: ViewPager2: ViewPager2: ViewPager2: ViewPager2: ViewPager2: ViewPager2: ViewPager2: ViewPager2: ViewPager2: ViewPager2: ViewPager2: ViewPager2

The default value of offScreenPageLimit for ViewPager2 is OFFSCREEN_PAGE_LIMIT_DEFAULT, and Re is used when setOffscreenPageLimit is OFFSCREEN_PAGE_LIMIT_DEFAULT Caching mechanism of cyclerView. By default, only the Fragment currently displayed will be loaded, rather than preloading at least one item as in ViewPager. When switching to the next item, the current Fragment executes onPause, and the next Fragment executes onCreate all the way to onResume. When you slide back to the first page again, the current page also executes onPuase, and the first page executes onResume.

That is, in ViewPager2, preloading is turned off by default. It makes no sense to talk about lazy loading without the preloading mechanism. So what’s more about the lazy loading of ViewPager2? Just put the network request in onStart. With the popularity of ViewPager2, the concept of lazy loading will fade away.

2020/1/4 added:

What happens if I set offScreenPageLimit(1) for ViewPager2? Let’s look at the log:

ViewPager2 preloads a Fragment, and the lifecycle of the preloaded Fragment only runs to onStart. SetMaxLifecycle (Fragment, STARTED) must be set in the FragmentStateAdapter, so we can guess that setMaxLifecycle(fragment, STARTED) must be set. As a result, the lazy loading problem is handled in the same way as the new ViewPager lazy loading scheme, just by adding a Boolean value.

Third, summary

This article explores the lazy loading of fragments and the control of the maximum life cycle state of fragments in Androidx version 1.1.0, so as to explore a new method of lazy loading of fragments. For ViewPager2, it doesn’t preload by default so that means we don’t have to deal with lazy loading of ViewPager2. Well, this is the end of an article that took me two weekends to write. If you learned something from high school, please give me a thumbs up.

This article covers source code

Good library recommendation

I recommend BannerViewPager. This is a ViewPager based on the implementation of the powerful infinite round cast library. Banner and indicator styles for Tencent Video, QQ Music, Kugou Music, Alipay, Tmall, Taobao, Youku Video, Himalaya, netease Cloud Music, Bilibili and other apps can be implemented through BannerViewPager. Welcome to BannerViewPager on Github!