diff --git a/app/src/main/java/com/philliphsu/clock2/RecyclerViewFragment.java b/app/src/main/java/com/philliphsu/clock2/RecyclerViewFragment.java index 6c74156..e3025ca 100644 --- a/app/src/main/java/com/philliphsu/clock2/RecyclerViewFragment.java +++ b/app/src/main/java/com/philliphsu/clock2/RecyclerViewFragment.java @@ -47,11 +47,18 @@ public abstract class RecyclerViewFragment< protected abstract void onScrolledToStableId(long id, int position); /** - * @return the adapter to set on the RecyclerView. SUBCLASSES MUST OVERRIDE THIS, BECAUSE THE - * DEFAULT IMPLEMENTATION WILL ALWAYS RETURN AN UNINITIALIZED ADAPTER INSTANCE. + * @return the adapter to set on the RecyclerView. Called in onCreateView(). + * @param savedInstanceState the same Bundle used to save out and restore state for this Fragment + * when the configuration is changed. Implementors may find this useful + * if their ViewHolder type(s) require saving and restoring state across + * configurations. */ - @Nullable - protected A getAdapter() { + protected abstract A onCreateAdapter(Bundle savedInstanceState); + + /** + * @return the adapter instance created from {@link #onCreateAdapter(Bundle)} + */ + protected final A getAdapter() { return mAdapter; } @@ -69,7 +76,7 @@ public abstract class RecyclerViewFragment< public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = super.onCreateView(inflater, container, savedInstanceState); mList.setLayoutManager(getLayoutManager()); - mList.setAdapter(mAdapter = getAdapter()); + mList.setAdapter(mAdapter = onCreateAdapter(savedInstanceState)); return view; } diff --git a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java index 186e6c4..7f4de49 100644 --- a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java +++ b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java @@ -6,7 +6,6 @@ import android.media.RingtoneManager; import android.net.Uri; import android.os.Bundle; import android.os.Handler; -import android.support.annotation.Nullable; import android.support.v4.content.Loader; import android.support.v7.widget.RecyclerView; import android.util.Log; @@ -136,11 +135,8 @@ public class AlarmsFragment extends RecyclerViewFragment< dialog.show(getFragmentManager(), TAG_TIME_PICKER); } - @Nullable @Override - protected AlarmsCursorAdapter getAdapter() { - if (super.getAdapter() != null) - return super.getAdapter(); + protected AlarmsCursorAdapter onCreateAdapter(Bundle savedInstanceState) { // Create a new adapter. This is called before we can initialize mAlarmController, // so right now it is null. However, after super.onCreate() returns, it is initialized, and // the reference variable will be pointing to an actual object. This assignment "propagates" @@ -262,7 +258,43 @@ public class AlarmsFragment extends RecyclerViewFragment< @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - outState.putInt(KEY_EXPANDED_POSITION, getAdapter().getExpandedPosition()); + /* + * From Fragment#onSaveInstanceState(): + * - This is called "at any time before onDestroy()". + * - "This corresponds to Activity.onSaveInstanceState(Bundle) and most of the discussion + * there applies here as well". + * From Activity#onSaveInstanceState(): + * - "If called, this method will occur before {@link #onStop} + * [which follows onPause() in the lifecycle]. There are + * no guarantees about whether it will occur before or after {@link #onPause}." + * + * isResumed() is true "for the duration of onResume() and onPause()". + * From the results of a few trials, this never seemed to call through, so i'm assuming + * isResumed() returned false every time. + */ + if (/*isResumed() && */getAdapter() != null) { + // Normally when we scroll far enough away from this Fragment, *its view* will be + // destroyed, i.e. the maximum point in its lifecycle is onDestroyView(). However, + // if the configuration changes, onDestroy() is called through, and then this Fragment + // and all of its members will be destroyed. This is not + // a problem if the page in which the configuration changed is this page, because + // the Fragment will be recreated from onCreate() to onResume(), and any + // member initialization between those points occurs as usual. + // + // However, when the page in which the configuration changed + // is far enough away from this Fragment, there IS a problem. The Fragment + // *at that page* is recreated, but this Fragment will NOT be; the ViewPager's + // adapter will not reinstantiate this Fragment because it exceeds the + // offscreen page limit relative to the initial page in the new configuration. + // + // As such, we should only save state if this Fragment's members (i.e. its RecyclerView.Adapter) + // are not destroyed + // because that indicates the Fragment is both registered in the adapter AND is within the offscreen + // page limit, so its members have been initialized (recall that a Fragment in a ViewPager + // does not actually need to be visible to the user for onCreateView() to onResume() to + // be called through). + outState.putInt(KEY_EXPANDED_POSITION, getAdapter().getExpandedPosition()); + } } ///////////////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchFragment.java b/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchFragment.java index f596d82..334ab09 100644 --- a/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchFragment.java +++ b/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchFragment.java @@ -259,11 +259,8 @@ public class StopwatchFragment extends RecyclerViewFragment< updateAllFabs(); } - @Nullable @Override - protected LapsAdapter getAdapter() { - if (super.getAdapter() != null) - return super.getAdapter(); + protected LapsAdapter onCreateAdapter(Bundle savedInstanceState) { return new LapsAdapter(); } diff --git a/app/src/main/java/com/philliphsu/clock2/timers/TimersFragment.java b/app/src/main/java/com/philliphsu/clock2/timers/TimersFragment.java index 0dc3503..6c263f7 100644 --- a/app/src/main/java/com/philliphsu/clock2/timers/TimersFragment.java +++ b/app/src/main/java/com/philliphsu/clock2/timers/TimersFragment.java @@ -83,11 +83,8 @@ public class TimersFragment extends RecyclerViewFragment< startActivityForResult(intent, REQUEST_CREATE_TIMER); } - @Nullable @Override - protected TimersCursorAdapter getAdapter() { - if (super.getAdapter() != null) - return super.getAdapter(); + protected TimersCursorAdapter onCreateAdapter(Bundle savedInstanceState) { // Create a new adapter. This is called before we can initialize mAsyncTimersTableUpdateHandler, // so right now it is null. However, after super.onCreate() returns, it is initialized, and // the reference variable will be pointing to an actual object. This assignment "propagates"