There are many ways to use fragments, but there is no unified API to handle the visibility and callback of fragments, resulting in different methods to determine the visibility of fragments in different usage scenarios. There are many articles on Fragment visibility on the Web, but most of them cover incomplete usage scenarios, and some of them are outdated, so I’ve combed through the current use scenarios of fragments to provide a unified API for dealing with Fragment visibility.

Common Usage Scenarios

Use it directly in an Activity

Declare the Fragment in an XML file or dynamically load the Fragment in your code via the Add or replace of FragmentTransaction. In both cases, you can determine the Fragment’s visibility simply by listening to the Fragment’s onResume and onPause methods.

override fun onResume(a) {
    super.onResume()
 
    determineFragmentVisible()
}

override fun onPause(a) {
    super.onPause()
    
    determineFragmentInvisible()
}
Copy the code

Use show and hide to control show and hide

Google has added a New FragmentContainerView in AndroidX. Fragment 1.2.0 to replace FlameLayout as a fragment container. You’ll use the FragmentContainerView as a container for the Fragment later.

The use of the old

After adding a Fragment to the FragmentManager via the Add of FragmentTransaction, the Fragment’s life cycle follows the bound Activity or parent Fragment to onResume. At this point, as long as the lifecycle of the attached Activity or parent Fragment does not change, Using the show and hide methods of the FragmentTransaction to display and hide fragments does not change the lifecycle of the Fragment. You need to listen to onHiddenChanged to determine whether the Fragment is visible.

Normally, the process of adding a Fragment to the FragmentManager takes place in the onCreate callback in the Activity. The first callback to onHiddenChanged is before the Fragment callback to onCreateView. If you need to perform UI operations when the Fragment is visible for the first time, errors will occur. To avoid errors, you need to determine the Fragment visibility using onResume and onPause.

override fun onHiddenChanged(hidden: Boolean) {
    super.onHiddenChanged(hidden)

    if (hidden) {
        determineFragmentInvisible()
    } else {
        determineFragmentVisible()
    }
}

override fun onResume(a) {
    super.onResume()
 
    determineFragmentVisible()
}

override fun onPause(a) {
    super.onPause()
    
    determineFragmentInvisible()
}
Copy the code

AndroidX usage

After hide is called, setMaxLifecycle(fragment, Lifecycle.state.started) is called, and the fragment Lifecycle goes to onPause. After calling show, setMaxLifecycle(fragment, Lifecycle.state.resumed) is followed by setMaxLifecycle(fragment, Lifecycle.state.resumed). The fragment Lifecycle will go to onPause. In this way, you can determine the visibility of the Fragment just by listening to the onResume and onPause methods on the Fragment.

override fun onResume(a) {
    super.onResume()
 
    determineFragmentVisible()
}

override fun onPause(a) {
    super.onPause()
    
    determineFragmentInvisible()
}
Copy the code

Used in ViewPager

The use of the old

On support and AndroidX.fragment 1.0.0, check the fragment visibility by listening for setUserVisibleHint. If the process of adding the Fragment to the FragmentManager takes place in the onCreate callback in the Activity, The first callback to setUserVisibleHint is also required before the Fragment calls onCreateView. The Fragment’s onResume and onPause are used to determine the Fragment’s visibility.

override fun setUserVisibleHint(isVisibleToUser: Boolean) {
    super.setUserVisibleHint(isVisibleToUser)
    
    if (isVisibleToUser) {
        determineFragmentVisible()
    } else {
        determineFragmentInvisible()
    }
}

override fun onResume(a) {
    super.onResume()
 
    determineFragmentVisible()
}

override fun onPause(a) {
    super.onPause()
    
    determineFragmentInvisible()
}
Copy the code

AndroidX usage

Start in the Google from androidx. Fragments 1.1.0, has carried on the adjustment to FragmentPagerAdapter and FragmentStatePagerAdapter, You can use setMaxLifecycle to control the life cycle of the Fragment. When creating the Adpter, the Behavior BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT is selected.

public static final int BEHAVIOR_SET_USER_VISIBLE_HINT = 0;
public static final int BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT = 1;

public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
    Fragment fragment = (Fragment)object;
    if(fragment ! = mCurrentPrimaryItem) {if(mCurrentPrimaryItem ! =null) {...if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
                ...
                mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED);
            } else {
                mCurrentPrimaryItem.setUserVisibleHint(false); }}...if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
            ...
            mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED);
        } else {
            fragment.setUserVisibleHint(true); }... }}Copy the code

In this way, you can determine the visibility of the Fragment just by listening to the onResume and onPause methods on the Fragment.

override fun onResume(a) {
    super.onResume()
 
    determineFragmentVisible()
}

override fun onPause(a) {
    super.onPause()
    
    determineFragmentInvisible()
}
Copy the code

Used in ViewPager2

Used in ViewPager2 fragments, use the adapter is FragmentStateAdapter, FragmentStateAdapter FragmentMaxLifecycleEnforcer internal use, FragmentMaxLifecycleEnforcer is also controlled by setMaxLifecycle fragments of life cycle

class FragmentStateAdapter {

    public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {... mFragmentMaxLifecycleEnforcer =newFragmentMaxLifecycleEnforcer(); . }public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {... mFragmentMaxLifecycleEnforcer =null;
    }

    class FragmentMaxLifecycleEnforcer {

        void updateFragmentMaxLifecycle(boolean dataSetChanged) {...for (int ix = 0; ix < mFragments.size(); ix++) {
                ...
                if(itemId ! = mPrimaryItemId) { transaction.setMaxLifecycle(fragment, STARTED); }else {
                    toResume = fragment; // itemId map key, so only one can match the predicate}... }if(toResume ! =null) { // in case the Fragment wasn't added yettransaction.setMaxLifecycle(toResume, RESUMED); }... }}}Copy the code

In this way, you can determine the visibility of the Fragment just by listening to the onResume and onPause methods on the Fragment.

override fun onResume(a) {
    super.onResume()
 
    determineFragmentVisible()
}

override fun onPause(a) {
    super.onPause()
    
    determineFragmentInvisible()
}
Copy the code

The specific implementation

Define Fragment visibility methods in IFragmentVisibility:

interface IFragmentVisibility {

    /** * the Fragment is visible. * /
    fun onVisible(a) {}

    /**
     * Fragment不可见时调用。
     */
    fun onInvisible(a) {}

    /** * Fragment is called when the Fragment is first visible. * /
    fun onVisibleFirst(a) {}

    /** * call the Fragment when it is visible. * /
    fun onVisibleExceptFirst(a) {}

    /** * Fragment Indicates whether the Fragment is currently visible to users */
    fun isVisibleToUser(a): Boolean
}
Copy the code

Fragments can be seen

Fragment visibility is affected by several factors: Whether the Fragment is in the RESUMED state, whether the Fragment is displayed, and whether the Fragment Hint is visible to users. Check whether the Fragment is visible for many times. If the Fragment is visible to users, do not check whether the Fragment is RESUMED.

// Fragment Indicates whether the Fragment is currently visible to users.
private var mIsFragmentVisible = false

// Fragment Indicates whether the Fragment is visible to the user for the first time.
private var mIsFragmentVisibleFirst = true

private fun determineFragmentVisible(a) {
    if(isResumed && ! isHidden && userVisibleHint && ! mIsFragmentVisible) { mIsFragmentVisible =true
        onVisible()
        if (mIsFragmentVisibleFirst) {
            mIsFragmentVisibleFirst = false
            onVisibleFirst()
        } else {
            onVisibleExceptFirst()
        }
    }
}
Copy the code

Fragments are not visible

When the fragments in a visible, call a determineFragmentInvisible method, fragments they became invisible.

private fun determineFragmentInvisible(a) {
    if (mIsFragmentVisible) {
        mIsFragmentVisible = false
        onInvisible()
    }
}
Copy the code

Nested fragments

The use of the old

It can be seen from the log that fragment-1 and fragment-1-1 are in the visible state, but what is strange is that fragment-2-1 is also in the visible state. This is not logical, and the judgment of the visibility logic needs to be optimized.

If isHidden = true, the Fragment is not visible. If isHidden = true, the Fragment is not visible. If isHidden = true, the Fragment is not visible. If the Fragment is isHidden = false and the Fragment is isHidden = true, the parent Fragment should be invisible. If the Fragment is isHidden = true and the Fragment is isHidden = true, the parent Fragment should be invisible. Therefore, consider whether the parent Fragment is visible (if any) when determining whether the Fragment is visible.

After you switch from fragment-1 to fragment-2, you can see that fragment-1 is not visible, fragment-2 is visible, but the fragment-1-1 is still visible, and the fragment-2-1 is still invisible. Indicate where the visibility logic can be optimized.

From fragment-1 to fragment-2, onHiddenChanged for both is called, so their visibility changes. Fragment-1-1 and fragment-2-1 don’t do anything, but their visibility should also change as the parent Fragment’s visibility changes, so reevaluate the child’s visibility when the parent’s changes.

AndroidX usage

Use setMaxLifecycle to control the Fragment lifecycle. You can see that the Fragment visibility is correct.

If you switch from fragment-1 to fragment-2, the visibility is correct.

The life cycle of the child Fragment changes according to the Activity or parent Fragment. SetMaxLifecycle changes the life cycle of the parent Fragment, and the life cycle of the child Fragment changes accordingly. Therefore, only onResume and onPause monitoring the Fragment can determine the Fragment visibility without adjusting the judgment logic.

The specific implementation

Add code to determineFragmentVisible to determine whether the parent Fragment is visible:

private fun determineFragmentVisible(a) {
    val parent = parentFragment
    if(parent ! =null && parent is VisibilityFragment) {
        if(! parent.isVisibleToUser()) {// The parent Fragment cannot be seen, and the child Fragment must also be seen
            return}}... }Copy the code

In determineFragmentVisible and determineFragmentInvisible increase judgment fragments the visibility of the code:

private fun determineFragmentVisible(a){...if(isResumed && ! isHidden && userVisibleHint && ! mIsFragmentVisible) { ... determineChildFragmentVisible() } }private fun determineFragmentInvisible(a) {
    if (mIsFragmentVisible) {
        ...
        determineChildFragmentInvisible()
    }
}

private fun determineChildFragmentVisible(a) {
    childFragmentManager.fragments.forEach {
        if (it is VisibilityFragment) {
            it.determineFragmentVisible()
        }
    }
}

private fun determineChildFragmentInvisible(a) {
    childFragmentManager.fragments.forEach {
        if (it is VisibilityFragment) {
            it.determineFragmentInvisible()
        }
    }
}
Copy the code

Lazy loading

After that, for fragments that need lazy loading, just override onVisibleFirst and load the data inside.

conclusion

For all code that uses setMaxLifecycle to control the Fragment lifecycle, the visibility of the Fragment is relatively simple. You can determine whether the Fragment is visible by listening to the onResume and onPause methods on the Fragment.

For code with old or mixed use of old and setMaxLifecycle, Fragment visibility should be determined not only by the mode of use, but also by the parent Fragment’s visibility, and when its own visibility changes, the child Fragment’s visibility should also be proactively called.

The project address

Fragment -visibility, you think it is very cool to use, please do not save your Star!

reference

How do I check whether fragments are visible to users

New Fragment function setMaxLifecycle

New implementation of lazy loading of fragments on Androidx