Search code examples
androidlifecyclefragmentstatepageradapter

LifeCycle of Fragment in FragmentStatePagerAdapter is unexpected


I want to create a video list using ViewPager. I must know when pager item visible, and invisible. I use VideoListAdapter extending FragmentStatePagerAdapter for ViewPager. I use Fragment method setUserVisibleHint to trigger video start or pause. But there is a problem, the Fragment in the position 0 of ViewPager throw a NullPointerException. And then i print log for releated method of Fragment.
The log i come into VideoListActivity: 07-08 17:06:50.264 E/lemon: startUpdate 07-08 17:06:50.264 E/lemon: instantiateItem 0 07-08 17:06:50.264 E/lemon: getItem 0 07-08 17:06:50.264 E/lemon: setUserVisibleHint 0 isVisibleToUser false 07-08 17:06:50.264 E/lemon: instantiateItem 1 07-08 17:06:50.264 E/lemon: getItem 1 07-08 17:06:50.264 E/lemon: setUserVisibleHint 0 isVisibleToUser false 07-08 17:06:50.264 E/lemon: setPrimaryItem 0 07-08 17:06:50.264 E/lemon: setUserVisibleHint 0 isVisibleToUser true 07-08 17:06:50.264 E/lemon: finishUpdate 07-08 17:06:50.265 E/lemon: onAttach 0 07-08 17:06:50.265 E/lemon: onAttach 1 07-08 17:06:50.265 E/lemon: onCreateView 0 07-08 17:06:50.267 E/lemon: onstart 0 07-08 17:06:50.267 E/lemon: onCreateView 1 07-08 17:06:50.269 E/lemon: onstart 1 07-08 17:06:50.270 E/lemon: startUpdate 07-08 17:06:50.270 E/lemon: setPrimaryItem 0 07-08 17:06:50.270 E/lemon: finishUpdate 07-08 17:06:50.297 E/lemon: startUpdate 07-08 17:06:50.297 E/lemon: setPrimaryItem 0 07-08 17:06:50.297 E/lemon: finishUpdate 07-08 17:06:50.297 E/lemon: startUpdate 07-08 17:06:50.297 E/lemon: setPrimaryItem 0 07-08 17:06:50.297 E/lemon: finishUpdate 07-08 17:06:50.703 E/lemon: startUpdate 07-08 17:06:50.703 E/lemon: setPrimaryItem 0 07-08 17:06:50.703 E/lemon: finishUpdate 07-08 17:06:50.704 E/lemon: startUpdate 07-08 17:06:50.704 E/lemon: setPrimaryItem 0 07-08 17:06:50.704 E/lemon: finishUpdate

The log i scroll to position 1: 07-08 17:09:41.154 E/lemon: startUpdate 07-08 17:09:41.154 E/lemon: setPrimaryItem 0 07-08 17:09:41.154 E/lemon: finishUpdate 07-08 17:09:41.966 E/lemon: startUpdate 07-08 17:09:41.966 E/lemon: instantiateItem 2 07-08 17:09:41.967 E/lemon: getItem 2 07-08 17:09:41.967 E/lemon: setUserVisibleHint 0 isVisibleToUser false 07-08 17:09:41.967 E/lemon: setPrimaryItem 1 07-08 17:09:41.967 E/lemon: setUserVisibleHint 0 isVisibleToUser false 07-08 17:09:41.967 E/lemon: setUserVisibleHint 1 isVisibleToUser true 07-08 17:09:41.967 E/lemon: finishUpdate 07-08 17:09:41.968 E/lemon: onAttach 2 07-08 17:09:41.968 E/lemon: onCreateView 2 07-08 17:09:41.971 E/lemon: onstart 2 07-08 17:09:41.971 E/lemon: startUpdate 07-08 17:09:41.971 E/lemon: setPrimaryItem 1 07-08 17:09:41.971 E/lemon: finishUpdate 07-08 17:09:41.972 E/lemon: startUpdate 07-08 17:09:41.972 E/lemon: setPrimaryItem 1 07-08 17:09:41.972 E/lemon: finishUpdate
I analyse these logs, I find that fragment in positino 0 invokes setUserVisibleHint(true) first and then invokes onAttach(), but fragment in position 1 invokes onAttach() first and setUserVisibleHint(true) after.

So I write a method onTrigger() in Fragment called in both onAttach() and setUserVisibleHint(true), but fail. I then debug my code, it's show that isAdded() in onTrigger() returns false when invoke onTrigger in onAttach().

So any suggestions here to let me know when to trigger my video start. Thanks a lot.

public class FullScreenVideoFragment extends Fragment {

    private FragmentFullScreenVideoBinding binding;
    int colorRes;
    int position;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        Log.e("lemon", "onCreateView " + position);
        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_full_screen_video, container, false);
        setView();
        return binding.getRoot();
    }

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

    public void setBgAndPosition(int position, int colorRes) {
        this.position = position;
        this.colorRes = colorRes;
    }

    @Override
    public void onAttach(Context context) {
        Log.e("lemon", "onAttach " + position);
        super.onAttach(context);
        onTriger();
    }

    @Override
    public void onDetach() {
        Log.e("lemon", "onDetach " + position);
        super.onDetach();
    }

    @Override
    public void onAttachFragment(Fragment childFragment) {
        Log.e("lemon", "onAttachFragment " + position);
        super.onAttachFragment(childFragment);
    }

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        Log.e("lemon", "setUserVisibleHint " + position + " isVisibleToUser " + isVisibleToUser);
        super.setUserVisibleHint(isVisibleToUser);
        onTriger();
    }

    private void setView() {
        binding.getRoot().setBackgroundResource(colorRes);
        binding.position.setText(String.valueOf(position));
    }

    private void onTriger() {
        if (!isVisible()) return;
        binding.position.setText(position + " start");
    }
}

public class VideoListAdapter extends FragmentStatePagerAdapter {
    private LinkedList<FullScreenVideoFragment> fragmentCaches;
    private int[] colors = new int[]{android.graphics.Color.RED, android.graphics.Color.BLUE, android.graphics.Color.GREEN};

    public VideoListAdapter(FragmentManager fm) {
        super(fm);
        fragmentCaches = new LinkedList<>();
    }

    @Override
    public Fragment getItem(int position) {
        Log.e("lemon", "getItem " + position);
        FullScreenVideoFragment fragment = generateItem();
        return fragment;
    }

    @Override
    public int getCount() {
        return 10;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        Log.e("lemon", "destroyItem " + position);
        super.destroyItem(container, position, object);
    }

    @Override
    public void setPrimaryItem(ViewGroup container, int position, Object object) {
        Log.e("lemon", "setPrimaryItem " + position);
        super.setPrimaryItem(container, position, object);
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        Log.e("lemon", "instantiateItem " + position);
        FullScreenVideoFragment fragment = (FullScreenVideoFragment) super.instantiateItem(container, position);
        fragment.setBgAndPosition(position, colors[position % 3]);
        return fragment;
    }

    @Override
    public void startUpdate(ViewGroup container) {
        Log.e("lemon", "startUpdate");
        super.startUpdate(container);
    }

    @Override
    public void finishUpdate(ViewGroup container) {
        Log.e("lemon", "finishUpdate");
        super.finishUpdate(container);
    }

    @Override
    public void restoreState(Parcelable state, ClassLoader loader) {
        Log.e("lemon", "restoreState");
        super.restoreState(state, loader);
    }

    @Override
    public Parcelable saveState() {
        Log.e("lemon", "saveState");
        return super.saveState();
    }

    private FullScreenVideoFragment generateItem() {
        FullScreenVideoFragment neededFragment = null;
        if (!fragmentCaches.isEmpty()) {
            neededFragment = fragmentCaches.get(0);
            fragmentCaches.remove(0);
            return neededFragment;
        }
        neededFragment = new FullScreenVideoFragment();
        return neededFragment;
    }
}

The fragment xml:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data></data>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextView
            android:id="@+id/position"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:textColor="@color/account_name_color"/>
    </RelativeLayout>
</layout>

Solution

  • I found that Fragments often use these three ways to switch:

    • show/hide

    • attach/detach(replace)

    • ViewPager

    These three ways lead to Fragment visible state there are some differences, so I defined the following classes to distinguish between these three ways. If a group of Fragments are using the same way to switch, I think it should be reliable, if the switch in a different way, then I do not know, no careful test.

    import android.support.annotation.IntDef;
    import android.support.v4.app.Fragment;
    
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    
    /**
     * Created by Kilnn on 2017/7/12.
     * A smart fragment know itself's visible state.
     */
    public abstract class SmartFragment extends Fragment {
    
        private boolean isFragmentVisible;
    
        @Override
        public void onResume() {
            super.onResume();
            int switchType = getSwitchType();
            if (switchType == ATTACH_DETACH) {
                notifyOnFragmentVisible();
            } else if (switchType == SHOW_HIDE) {
                if (!isHidden()) {
                    notifyOnFragmentVisible();
                }
            } else if (switchType == VIEW_PAGER) {
                //If the parent fragment exist and hidden when activity destroy,
                //when the activity restore, The parent Fragment  will be restore to hidden state.
                //And the sub Fragment which in ViewPager is also be restored, and the onResumed() method will callback.
                //And The sub Fragment's getUserVisibleHint() method will return true  if it is in active position.
                //So we need to judge the parent Fragment visible state.
                if (getUserVisibleHint() && isParentFragmentVisible()) {
                    notifyOnFragmentVisible();
                }
            }
        }
    
        @Override
        public void setUserVisibleHint(boolean isVisibleToUser) {
            super.setUserVisibleHint(isVisibleToUser);
            int switchType = getSwitchType();
            if (switchType == VIEW_PAGER) {
                if (isVisibleToUser) {
                    notifyOnFragmentVisible();
                } else {
                    notifyOnFragmentInvisible();
                }
            }
        }
    
        @Override
        public void onHiddenChanged(boolean hidden) {
            super.onHiddenChanged(hidden);
            int switchType = getSwitchType();
            if (switchType == SHOW_HIDE) {
                if (hidden) {
                    notifyOnFragmentInvisible();
                } else {
                    notifyOnFragmentVisible();
                }
            }
        }
    
        @Override
        public void onPause() {
            super.onPause();
            notifyOnFragmentInvisible();
        }
    
        private boolean isParentFragmentVisible() {
            Fragment parent = getParentFragment();
            if (parent == null) return true;
            if (parent instanceof SmartFragment) {
                return ((SmartFragment) parent).isFragmentVisible();
            } else {
                //TODO May be can't get the correct visible state if parent Fragment is not SmartFragment
                return parent.isVisible();
            }
        }
    
        public boolean isFragmentVisible() {
            // Don't judge the state of the parent fragment,
            // because if the parent fragment visible state changes,
            // you must take the initiative to change the state of the sub fragment
    //        return isFragmentVisible && isParentFragmentVisible();
            return isFragmentVisible;
        }
    
        public void notifyOnFragmentVisible() {
            if (!isFragmentVisible) {
                onFragmentVisible();
                isFragmentVisible = true;
            }
        }
    
        public void notifyOnFragmentInvisible() {
            if (isFragmentVisible) {
                onFragmentInvisible();
                isFragmentVisible = false;
            }
        }
    
        /**
         * If this method callback, the Fragment must be resumed.
         */
        public void onFragmentVisible() {
    
        }
    
        /**
         * If this method callback, the Fragment maybe is resumed or in onPause().
         */
        public void onFragmentInvisible() {
    
        }
    
        /**
         * Fragments switch with attach/detach(replace)
         */
        public static final int ATTACH_DETACH = 0;
    
        /**
         * Fragments switch with show/hide
         */
        public static final int SHOW_HIDE = 1;
    
        /**
         * Fragments manage by view pager
         */
        public static final int VIEW_PAGER = 2;
    
        @Retention(RetentionPolicy.SOURCE)
        @IntDef({ATTACH_DETACH, SHOW_HIDE, VIEW_PAGER})
        @interface SwitchType {
        }
    
        @SwitchType
        public abstract int getSwitchType();
    
    }
    

    So you can do onTrigger() in onFragmentVisible().