Selecting a specified page in ViewPager when starting

Motivation

I realized the pages, each of which has some fragments hierarchically, in the previous post. Now I need to add a new feature to the sample application. The feature is to resume the page which was displayed last time when restarting the application. Let's get started!

First implementation

In the beginning I thought it was very easy. The first implementation is like below.

@Override
protected void onCreate(Bundle savedInstanceState) {

    // ...

    TitlePageIndicator indicator = (TitlePageIndicator)findViewById(R.id.indicator);
    indicator.setViewPager(pager);
    indicator.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
        @Override
        public void onPageSelected(int position) {
            TestFragmentPage current = (TestFragmentPage)adapter.getItem(position);
            current.onPageSelected();

            // Store the position of current page
            PrefUtils.setInt(MainActivity.this, R.string.pref_last_tab, position);
        }

        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}

        @Override
        public void onPageScrollStateChanged(int state) {}
    });
}

@Override
protected void onResume() {
    super.onResume();

    ViewPager pager = (ViewPager)findViewById(R.id.pager);
    PagerAdapter adapter = pager.getAdapter();

    // Resume the last page
    int lastPage = PrefUtils.getInt(this, R.string.pref_last_tab, 0);
    if(lastPage < adapter.getCount()) {
        pager.setCurrentItem(lastPage);
    }
}

// ...


PrefUtils class is very simple like this.

public class PrefUtils {

    public static int getInt(Context context, int key, int defValue) {
        SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context);
        return pref.getInt(context.getString(key), defValue);
    }

    public static boolean setInt(Context context, int key, int value) {
        SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context);
        SharedPreferences.Editor editor = pref.edit();
        editor.putInt(context.getString(key), value);
        return editor.commit();
    }
}


The points of above change are

  1. Store the selected page's position in ViewPager.OnPageChangeListener#onPageSelected method to the preference area
  2. Resume the last page in Activity#onResume method

Big problem

It is time to test the new application. I started the application and saw 'THIS' page. Next I moved to 'IS' page on the right side of 'THIS' page. After I closed the application, I restarted the application. I expected that the application would show 'IS' page but the result was not at all. The application has crashed unexpectedly. The log on Logcat console is below.


03-19 23:12:35.352: E/AndroidRuntime(5104): FATAL EXCEPTION: main
03-19 23:12:35.352: E/AndroidRuntime(5104): java.lang.RuntimeException: Unable to resume activity {com.yohpapa.research.viewpagersample/com.yohpapa.research.viewpagersample.MainActivity}: java.lang.NullPointerException
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.app.ActivityThread.performResumeActivity(ActivityThread.java:2575)
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:2603)
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2089)
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.app.ActivityThread.access$600(ActivityThread.java:130)
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1195)
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.os.Handler.dispatchMessage(Handler.java:99)
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.os.Looper.loop(Looper.java:137)
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.app.ActivityThread.main(ActivityThread.java:4745)
03-19 23:12:35.352: E/AndroidRuntime(5104): at java.lang.reflect.Method.invokeNative(Native Method)
03-19 23:12:35.352: E/AndroidRuntime(5104): at java.lang.reflect.Method.invoke(Method.java:511)
03-19 23:12:35.352: E/AndroidRuntime(5104): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:786)
03-19 23:12:35.352: E/AndroidRuntime(5104): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
03-19 23:12:35.352: E/AndroidRuntime(5104): at dalvik.system.NativeStart.main(Native Method)
03-19 23:12:35.352: E/AndroidRuntime(5104): Caused by: java.lang.NullPointerException
03-19 23:12:35.352: E/AndroidRuntime(5104): at com.yohpapa.research.viewpagersample.MainActivity$TestFragmentPage.onPageSelected(MainActivity.java:216)
03-19 23:12:35.352: E/AndroidRuntime(5104): at com.yohpapa.research.viewpagersample.MainActivity$1.onPageSelected(MainActivity.java:46)
03-19 23:12:35.352: E/AndroidRuntime(5104): at com.viewpagerindicator.TitlePageIndicator.onPageSelected(TitlePageIndicator.java:781)
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.support.v4.view.ViewPager.scrollToItem(ViewPager.java:545)
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.support.v4.view.ViewPager.setCurrentItemInternal(ViewPager.java:523)
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.support.v4.view.ViewPager.setCurrentItemInternal(ViewPager.java:494)
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.support.v4.view.ViewPager.setCurrentItem(ViewPager.java:475)
03-19 23:12:35.352: E/AndroidRuntime(5104): at com.yohpapa.research.viewpagersample.MainActivity.onResume(MainActivity.java:70)
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1184)
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.app.Activity.performResume(Activity.java:5082)
03-19 23:12:35.352: E/AndroidRuntime(5104): at android.app.ActivityThread.performResumeActivity(ActivityThread.java:2565)
03-19 23:12:35.352: E/AndroidRuntime(5104): ... 12 more


OMG... NullPointerException happened. The place where the exception happened is below.

public static class TestFragmentPage extends Fragment {

    // ...

    public void onPageSelected() {
        FragmentManager manager = getChildFragmentManager();
        int numFragments = manager.getBackStackEntryCount();
        if(numFragments <= 0) {
            View view = getView();
            view.setFocusableInTouchMode(true);     // <- Here is line 216.
            view.requestFocus();

            // ...
}


Why was the view which was returned from Fragment#getView method null? I need to examine the serious problem more deeply.

Examination

First I wondered whether the application called onPageSelected method before onCreateView method, so I checked it. The result was that onPageSelected method was called before another one. The callstack when the exception happened was

  1. TestFragmentPage.onPageSelected(MainActivity.java:216)
  2. MainActivity$1.onPageSelected(MainActivity.java:46)
  3. TitlePageIndicator.onPageSelected(TitlePageIndicator.java:781)
  4. MainActivity.onResume(MainActivity.java:70)


It means Activity#onResume method is called before it's child fragment's onCreateView method except for the case the first page is displayed. Why is the case the first page is displayed OK? It is because even if the current page is changed to the first page, ViewPager.OnPageChangeListener#onPageSelected method is not called.

Solution

I thought that if I can make sure that Fragment#getView method is called after Fragment#onCreateView method called, it would work well (no exception!) So I tried to apply a tricky solution like below.

public class MainActivity extends FragmentActivity {

    private boolean _isJustAfterResume = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        // ...

        TitlePageIndicator indicator = (TitlePageIndicator)findViewById(R.id.indicator);
        indicator.setViewPager(pager);
        indicator.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageSelected(int position) {
                TestFragmentPage current = (TestFragmentPage)adapter.getItem(position);

                // Notify the current page whether it is just after the activity resumed or not.
                current.onPageSelected(isJustAfterResume);
                isJustAfterResume = false;

                // Store the position of current page
                PrefUtils.setInt(MainActivity.this, R.string.pref_last_tab, position);
            }

            // ...
        });
    }

    @Override
    protected void onResume() {
        super.onResume();

        // Remember that the activity is resumed
        isJustAfterResume = true;

        // ...
    }
}

public static class TestFragmentPage extends Fragment {

    // ...

    private boolean isFirstSelected = false;

    public void onPageSelected(boolean isInitial) {

        // If it is the time just after the parent activity resumed,
        // Setup of the focus is put off until onResume method executed.
        if(isInitial) {
            isFirstSelected = true;
        } else {
            setupFocus();
        }
    }

    public void onResume() {
        super.onResume();

        // If setup of the focus is put off, do it here.
        // because we want to make sure that onCreateView method has already been executed.
        if(isFirstSelected) {
            setupFocus();
            isFirstSelected = false;
        }
    }

    private void setupFocus() {
        // Here is the same implementation as old onPageSelected method
    }
}


The points of above change are

  1. The host activity notifies it's children (pages) whether it is just after the activity resumed or not.
  2. The children (pages) put off the notification if it is just after their parent resumed. If not, execute setup of the focus immediately.
  3. If the notification is put off, setup of the focus is executed on onResume method to make sure it is done after onCreateView method executed.

Conclution

I found my lovely application working well now. It means my concept is realy right!

Another problem

When I rotated my Android phone, however, the application crashed unfortunately. The log on Logcat console was below.


03-20 15:20:43.067: E/AndroidRuntime(19481): FATAL EXCEPTION: main
03-20 15:20:43.067: E/AndroidRuntime(19481): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.yohpapa.research.viewpagersample/com.yohpapa.research.viewpagersample.MainActivity}: java.lang.NullPointerException
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2059)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2084)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.app.ActivityThread.handleRelaunchActivity(ActivityThread.java:3512)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.app.ActivityThread.access$700(ActivityThread.java:130)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1201)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.os.Handler.dispatchMessage(Handler.java:99)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.os.Looper.loop(Looper.java:137)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.app.ActivityThread.main(ActivityThread.java:4745)
03-20 15:20:43.067: E/AndroidRuntime(19481): at java.lang.reflect.Method.invokeNative(Native Method)
03-20 15:20:43.067: E/AndroidRuntime(19481): at java.lang.reflect.Method.invoke(Method.java:511)
03-20 15:20:43.067: E/AndroidRuntime(19481): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:786)
03-20 15:20:43.067: E/AndroidRuntime(19481): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
03-20 15:20:43.067: E/AndroidRuntime(19481): at dalvik.system.NativeStart.main(Native Method)
03-20 15:20:43.067: E/AndroidRuntime(19481): Caused by: java.lang.NullPointerException
03-20 15:20:43.067: E/AndroidRuntime(19481): at com.yohpapa.research.viewpagersample.MainActivity$TestFragmentPage.setupFocus(MainActivity.java:240)
03-20 15:20:43.067: E/AndroidRuntime(19481): at com.yohpapa.research.viewpagersample.MainActivity$TestFragmentPage.onPageSelected(MainActivity.java:221)
03-20 15:20:43.067: E/AndroidRuntime(19481): at com.yohpapa.research.viewpagersample.MainActivity$1.onPageSelected(MainActivity.java:48)
03-20 15:20:43.067: E/AndroidRuntime(19481): at com.viewpagerindicator.TitlePageIndicator.onPageSelected(TitlePageIndicator.java:781)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.support.v4.view.ViewPager.scrollToItem(ViewPager.java:545)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.support.v4.view.ViewPager.setCurrentItemInternal(ViewPager.java:523)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.support.v4.view.ViewPager.setCurrentItemInternal(ViewPager.java:494)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.support.v4.view.ViewPager.onRestoreInstanceState(ViewPager.java:1220)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.view.View.dispatchRestoreInstanceState(View.java:11910)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:2584)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:2590)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:2590)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.view.View.restoreHierarchyState(View.java:11888)
03-20 15:20:43.067: E/AndroidRuntime(19481): at com.android.internal.policy.impl.PhoneWindow.restoreHierarchyState(PhoneWindow.java:1608)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.app.Activity.onRestoreInstanceState(Activity.java:928)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.app.Activity.performRestoreInstanceState(Activity.java:900)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.app.Instrumentation.callActivityOnRestoreInstanceState(Instrumentation.java:1130)
03-20 15:20:43.067: E/AndroidRuntime(19481): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2037)
03-20 15:20:43.067: E/AndroidRuntime(19481): ... 12 more


NullPointerException happened again. Mmm... I need to dive into the problem.

Second round of examination

I checked the place where NullPointerException happened. It was below.

public static class TestFragmentPage extends Fragment {

    // ...

    private void setupFocus() {

        FragmentManager manager = getChildFragmentManager();
        int numFragments = manager.getBackStackEntryCount();
        if(numFragments <= 0) {
            View view = getView();
            view.setFocusableInTouchMode(true);     // <- Here is line 240.
            view.requestFocus();

            // ...
}


Oh... It is the same place where I encountered the first problem before. So I decided to check whether setupFocus method is called after onCreateView method called in the same way as before or not. I found the execution order when rotating the device.

  1. MainActivity#onCreate
  2. TestFragmentPage#onCreate (3 times from MainActivity#onCreate)
  3. TestFragmentPage#onCreateView (3 times from MainActivity#onStart)
  4. TestFragmentPage#setupFocus (from MainActivity#onRestoreInstanceState)


I am not sure why TestFragment#onCreate was called from MainActivity#onCreate but I guess the Activity tried to resume it's last state. But my application's Activity is not sure about it, so the Activity is designed to re-create some TestFragmentPage objects again and connect itself with new created TestFragmentPage objects. However the new TestFragmentPage objects is not initialized (their onCreate method and onCreateView method are not called.) It is the reason why the application crashed for NullPointerException.


I asked google the solution for the problem and found the answer on stackoverflow.com.

You could add: android:configChanges="screenSize|orientation" In AndroidManifest.xml.


This will prevent Android calling onCreate on screen orientation change.If you want to perform special handling of orientation change, you can override onConfigurationChanges.

http://stackoverflow.com/questions/9039877/android-fragment-screen-rotate


So I added this attribute to my Activity's tag on AndroidManifest.xml. Then I tested the application again and rotated it. It has worked fine!

Final Conclusion

I am so happy to solve these problems and now I am preparing to share the lovely application with developers. Please stay tuned!