Updated Fragment view state lost when using setRetainInstance(true) - android

I am trying to save fragment state. I have an activity and several fragments. The sequence of actions: add first fragment, change view manually (make visibility of first LinearLayout GONE and second LinearLayout VISIBLE), detach fragment, add another one, detach it and again attach first fragment.
Adding/attaching/detaching works good but setRetainInstanse(true) saves only initial fragment state.
Finally I get first LinearLayout visible at my fragment (instead of second) so I've tried to make it by hands but it doesn't work:
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null) {
if (savedInstanceState.containsKey(BUNDLE_IS_LOADING)) {
if (savedInstanceState.getBoolean(BUNDLE_IS_LOADING)) {
mBlockContent.setVisibility(View.GONE);
mBlockProgress.setVisibility(View.VISIBLE);
} else {
mBlockContent.setVisibility(View.VISIBLE);
mBlockProgress.setVisibility(View.GONE);
}
}
}
}
setRetainInstance(true);
}
#Override
public void onSaveInstanceState(Bundle b) {
super.onSaveInstanceState(b);
b.putBoolean(BUNDLE_IS_LOADING,
mBlockProgress.getVisibility() == View.VISIBLE);
}
I use compatibility library rev. 11.
Solution for me:
private boolean isProgressing;
private void saveViewsState() {
isProgressing = mBlockProgress.getVisibility() == View.VISIBLE;
}
private void switchToProgress() {
mBlockContent.setVisibility(View.GONE);
mBlockProgress.setVisibility(View.VISIBLE);
}
private void switchToContent() {
mBlockContent.setVisibility(View.VISIBLE);
mBlockProgress.setVisibility(View.GONE);
}
#Override
public void onSaveInstanceState(Bundle b) {
super.onSaveInstanceState(b);
saveViewsState();
}
#Override
public void onPause() {
super.onPause();
saveViewsState();
}
#Override
public void onResume() {
super.onResume();
if (isProgressing) {
switchToProgress();
} else {
switchToContent();
}
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (isProgressing) {
switchToProgress();
} else {
switchToContent();
}
}

Related

Maintaining list items positions on device rotation in an android fragment

In a fragment I am trying to save the scroll state of the RecyclerView list, but somehow it is not saving the state. As it is a fragment, I am overriding the onSaveInstanceState() and onActivityCreated() methods to save the scroll position. Even tried implementing in onViewStateRestored() method. I saw related some posts on saving the scroll state but it ain't working. Kindly let me know where am I failing. Below is my code:
public class RecipeListFragment extends Fragment
implements RecipeListContract.View {
#BindView(R.id.recipe_list_recycler_view)
RecyclerView mRecipeListRecyclerView;
#BindView(R.id.recipe_list_progress_bar)
ProgressBar mRecipeListProgressBar;
#BindInt(R.integer.grid_column_count)
int mGridColumnCount;
#BindString(R.string.recipe_list_sync_completed)
String mRecipeListSyncCompleted;
#BindString(R.string.recipe_list_connection_error)
String mRecipeListConnectionError;
GridLayoutManager gridLayoutManager;
Parcelable savedRecyclerLayoutState;
Unbinder unbinder;
private static final String SAVED_LAYOUT_MANAGER
= "com.learnwithme.buildapps.bakingapp.ui.recipelist.fragment";
private RecipeListContract.Presenter mRecipeListPresenter;
private RecipeListAdapter mRecipeListAdapter;
public RecipeListFragment() { }
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup
container, #Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_recipe_list, container,
false);
unbinder = ButterKnife.bind(this, view);
mRecipeListAdapter = new RecipeListAdapter(
getContext(),
new ArrayList<>(0),
recipeId -> mRecipeListPresenter.loadRecipeDetails(recipeId)
);
mRecipeListAdapter.setHasStableIds(true);
gridLayoutManager = new GridLayoutManager(getContext(),
mGridColumnCount);
mRecipeListRecyclerView.setLayoutManager(gridLayoutManager);
mRecipeListRecyclerView.setHasFixedSize(true);
mRecipeListRecyclerView.setAdapter(mRecipeListAdapter);
return view;
}
#Override
public void onPause() {
super.onPause();
mRecipeListPresenter.unsubscribe();
}
#Override
public void onResume() {
super.onResume();
mRecipeListPresenter.subscribe();
}
#Override
public void onSaveInstanceState() { }
#Override
public void onSaveInstanceState(Bundle bundle) {
super.onSaveInstanceState(bundle);
if(bundle != null) {
bundle.putParcelable(SAVED_LAYOUT_MANAGER,
mRecipeListRecyclerView
.getLayoutManager()
.onSaveInstanceState());
Timber.d("instance state=>",
mRecipeListRecyclerView.getLayoutManager().onSaveInstanceState());
}
}
#Override
public void onViewStateRestored(#Nullable Bundle bundle) {
super.onViewStateRestored(bundle);
if(bundle != null) {
savedRecyclerLayoutState =
bundle.getParcelable(SAVED_LAYOUT_MANAGER);
Timber.d("onViewStateRestored savedRecyclerLayoutState=>",
savedRecyclerLayoutState);
mRecipeListRecyclerView
.getLayoutManager()
.onRestoreInstanceState(savedRecyclerLayoutState);
}
}
#Override
public void onActivityCreated(#Nullable Bundle bundle) {
super.onActivityCreated(bundle);
if(bundle != null) {
savedRecyclerLayoutState =
bundle.getParcelable(SAVED_LAYOUT_MANAGER);
Timber.d("onViewStateRestored savedRecyclerLayoutState=>",
savedRecyclerLayoutState);
mRecipeListRecyclerView
.getLayoutManager()
.onRestoreInstanceState(savedRecyclerLayoutState);
}
}
#Override
public void onDestroyView() {
super.onDestroyView();
unbinder.unbind();
}
public static RecipeListFragment newInstance() {
return new RecipeListFragment();
}
#Override
public void setPresenter(RecipeListContract.Presenter recipeListPresenter) {
this.mRecipeListPresenter = recipeListPresenter;
}
#Override
public void showRecipeList(List<Recipe> recipeList) {
mRecipeListAdapter.refreshRecipes(recipeList);
}
#Override
public void loadProgressBar(boolean show) {
setViewVisibility(mRecipeListRecyclerView, !show);
setViewVisibility(mRecipeListProgressBar, show);
}
#Override
public void displayCompletedMessage() {
Toast.makeText(getContext(), mRecipeListSyncCompleted,
Toast.LENGTH_SHORT).show();
}
#Override
public void displayErrorMessage() {
Toast.makeText(getContext(), mRecipeListConnectionError,
Toast.LENGTH_SHORT).show();
}
#Override
public void displayRecipeDetails(int recipeId) {
startActivity(RecipeDetailsActivity.prepareIntent(getContext(),
recipeId));
}
private void setViewVisibility(View view, boolean visible) {
if (visible) {
view.setVisibility(View.VISIBLE);
} else {
view.setVisibility(View.INVISIBLE);
}
}
}
I have resolved the issue myself. The problem was to save the scroll position of the device in onSaveInstanceState() and restoring the same in onViewRestored() method.
private Parcelable mRecipeListParcelable;
private int mScrollPosition = -1;
#Override
public void onSaveInstanceState(Bundle bundle) {
super.onSaveInstanceState(bundle);
int scrollPosition = ((GridLayoutManager)
mRecipeListRecyclerView.getLayoutManager())
.findFirstCompletelyVisibleItemPosition();
mRecipeListParcelable = gridLayoutManager.onSaveInstanceState();
bundle.putParcelable(KEY_LAYOUT, mRecipeListParcelable);
bundle.putInt(POSITION, scrollPosition);
}
#Override
public void onViewStateRestored(#Nullable Bundle bundle) {
super.onViewStateRestored(bundle);
if(bundle != null) {
mRecipeListParcelable = bundle.getParcelable(KEY_LAYOUT);
mScrollPosition = bundle.getInt(POSITION);
}
}
Also, in the loadProgress() method I had to set the scrollToPosition() with the scroll position saved.
#Override
public void loadProgressBar(boolean show) {
setViewVisibility(mRecipeListRecyclerView, !show);
setViewVisibility(mRecipeListProgressBar, show);
mRecipeListRecyclerView.scrollToPosition(mScrollPosition);
}
Also, one more thing to remember is that no need to restore anything in onResume() method as the presenter callbacks would get called and the view is reset anyway.

Android exception when popping backstack

This is a followup question to these questions:
popBackStack() after saveInstanceState()
Application crashes in background, when popping a fragment from stack
I am creating an application which uses a service and is reacting to events which are created by the service. One of the events is called within a fragment and is popping from the backstack like this:
getSupportFragmentManager().popBackStack(stringTag, FragmentManager.POP_BACK_STACK_INCLUSIVE);
When the app is in the foreground it works fine. When the app is in the background, I get an
IllegalStateException: Can not perform this action after onSaveInstanceState
I have already tried overriding onSaveInstanceState with an empty method.
Why do I get this exception only when the app is in the background and how can I solve it?
Try something like this.
public abstract class PopActivity extends Activity {
private boolean mVisible;
#Override
public void onResume() {
super.onResume();
mVisible = true;
}
#Override
protected void onPause() {
super.onPause();
mVisible = false;
}
private void popFragment() {
if (!mVisible) {
return;
}
FragmentManager fm = getSupportFragmentManager();
fm.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
}
when you implement the above code alone when you resume the app you will find yourself in a fragment that you actually want to be popped. You can use the following snipped to fix this issue:
public abstract class PopFragment extends Fragment {
private static final String KEY_IS_POPPED = "KEY_IS_POPPED";
private boolean mPopped;
#Override
public void onSaveInstanceState(Bundle outState) {
outState.putBoolean(KEY_IS_POPPED, mPopped);
super.onSaveInstanceState(outState);
}
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
mPopped = savedInstanceState.getBoolean(KEY_IS_POPPED);
}
}
#Override
public void onResume() {
super.onResume();
if (mPopped) {
popFragment();
}
}
protected void popFragment() {
mPopped = true;
// null check and interface check advised
((PopActivity) getActivity()).popFragment();
}
}
Original Author

Android fragment lifecycle issue with actionbar

I want to realize the navigation of the fragments using the following code:
public abstract class BaseFragment extends Fragment {
private static String TAG = "BaseFragment";
private BaseFragmentActions baseFragmentActions;
#Override
public void onAttach(Context context) {
super.onAttach(context);
Activity activity = null;
if (context instanceof Activity){
activity = (Activity) context;
}
Log.i(TAG, "onAttach = ");
try {
baseFragmentActions = (BaseFragmentActions)activity;
} catch (ClassCastException e) {
}
Log.i("onAttach",""+(getBackStackCount()!=0));
baseFragmentActions.resetToolbarNavigation(getBackStackCount()!=0);
}
#Override
public void onDetach() {
super.onDetach();
Log.i("BaseFragment", "onDestroy = " + (getBackStackCount() - 1));
baseFragmentActions.resetToolbarNavigation((getBackStackCount() - 1) != 0);
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
}
private int getBackStackCount() {
int b = getActivity().getSupportFragmentManager().getBackStackEntryCount();
Log.i("getBackStackEntryCount", "====== "+b);
return b;
}
public interface BaseFragmentActions {
public void resetToolbarNavigation(boolean backNavigationEnabled);
}
}
All my fragments extend this Base Activity. And inside my main activity i implement BaseFragmentActions, and implemented this method:
#Override
public void resetToolbarNavigation(boolean backNavigationEnabled) {
Log.i("BaseActivity", "reset " + backNavigationEnabled);
getSupportActionBar().setDisplayHomeAsUpEnabled(backNavigationEnabled);
if (backNavigationEnabled) {
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Log.i("resetToolbarNavigation", "setNavigationOnClickListener");
onBackPressed();
}
});
} else {
initNavigation();
syncState();
}
}
Everything works fine but when I change the screen orientation we obtain error that getSupportActionBar = null.
This is because of what I call going to attach. How can I fix this error? I tried to make checking whether getSupportActionBar is not zero. I'm not getting an error, but "up" Arrow replaced hamburger...
Advise what you can do in this case. Also share links to navigate the implementation of such fragments. Sorry if something wrong written, or I made a grammatical error)).
Hi sorry for the delay in the answer, the problem you're having is because when onAttach is called the getSupportActionBar() is not set yet, instead you need to make sure the Activity is already created when interacting with Activity components, so just put your call inside the onActivityCreated method of your Fragment like this:
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
baseFragmentActions.resetToolbarNavigation(getBackStackCount()!=0);
}

Why does FragmentManager.findFragmentByTag(TAG_TASK_FRAGMENT) not return null

I read this http://www.androiddesignpatterns.com/2013/04/retaining-objects-across-config-changes.html. And I played the example code in the link. To my surprise, fm.findFragmentByTag(TAG_TASK_FRAGMENT) does not return null when I rotate the phone, if I remove setRetainInstance(true) in the TaskFragment.onCreate(). I copied the code here with one line change (remove setRetainInstance(true)).
Please explain why fm.findFragmentByTag(TAG_TASK_FRAGMENT) does not return null in this case.
public class MainActivity extends Activity implements TaskFragment.TaskCallbacks {
private static final String TAG_TASK_FRAGMENT = "task_fragment";
private TaskFragment mTaskFragment;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
FragmentManager fm = getFragmentManager();
mTaskFragment = (TaskFragment) fm.findFragmentByTag(TAG_TASK_FRAGMENT);
if (mTaskFragment == null) {
mTaskFragment = new TaskFragment();
fm.beginTransaction().add(mTaskFragment, TAG_TASK_FRAGMENT).commit();
}
}
#Override
public void onPreExecute() { }
#Override
public void onProgressUpdate(int percent) { }
#Override
public void onCancelled() { }
#Override
public void onPostExecute() { }
}
public class TaskFragment extends Fragment {
interface TaskCallbacks {
void onPreExecute();
void onProgressUpdate(int percent);
void onCancelled();
void onPostExecute();
}
private TaskCallbacks mCallbacks;
private DummyTask mTask;
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mCallbacks = (TaskCallbacks) activity;
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// I remove this call to produce the problem
// setRetainInstance(true);
mTask = new DummyTask();
mTask.execute();
}
#Override
public void onDetach() {
super.onDetach();
mCallbacks = null;
}
private class DummyTask extends AsyncTask<Void, Integer, Void> {
#Override
protected void onPreExecute() {
if (mCallbacks != null) {
mCallbacks.onPreExecute();
}
}
#Override
protected Void doInBackground(Void... ignore) {
for (int i = 0; !isCancelled() && i < 100; i++) {
SystemClock.sleep(100);
publishProgress(i);
}
return null;
}
#Override
protected void onProgressUpdate(Integer... percent) {
if (mCallbacks != null) {
mCallbacks.onProgressUpdate(percent[0]);
}
}
#Override
protected void onCancelled() {
if (mCallbacks != null) {
mCallbacks.onCancelled();
}
}
#Override
protected void onPostExecute(Void ignore) {
if (mCallbacks != null) {
mCallbacks.onPostExecute();
}
}
}
}
SetRetainInstance controls whether the entire fragment (and its contents) is retained in memory or whether it is recreated as a new Fragment from its Bundle.
The only time it would return null is the very first time the app is run. After that it has been added to the FragmentManager and is always available. (Rotating the device does not clear the FragmentManager regardless of whether you use SetRetainInstance or not)
You seem to think that SetRetainInstance controls whether the fragment is kept in the FragmentManager or not. It does not.
In your example, the AsyncTask starts running the first time the Fragment is created. SetRetainInstance is used to stop the OnDestroy method of the Fragment being called. After an orientation change, the fragment and its running task is still in the FragmentManager and the task is still running. Without SetRetainInstance, when the Orientation change occurs, the fragment is destroyed and recreated from its bundle when you retrieve it from the FragmentManager. This puts the AsyncTask in a delicate state as the task could still be running even if its hosting Fragment has been destroyed possibly leading to a crash.
See this question for a more in depth explanation.
Understanding Fragment's setRetainInstance(boolean)

how to set listview's position after configuration change

I'm struggling to recover my position in a listview on screen rotation configuration change.
Amongst the many things I've tried I came to this:
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState == null) {
...
mVisibleItem = -1;
} else {
if (savedInstanceState.containsKey(LV_VISIBLE_ITEM)) {
mVisibleItem = savedInstanceState.getInt(LV_VISIBLE_ITEM);
}
}
setRetainInstance(true);
}
and here I'm trying to set the position in the listview
#Override
public void onResume() {
super.onResume();
if (mVisibleItem > 0) {
mlvDictionaryIndex.setSelectionFromTop(mVisibleItem, 0);
}
}
However, much to my surprise, after rotating the screen and watching mVisibleItem gets set with the correct value, in onResume I see that mVisibleItem equals -1. How come?
use onSavedInstanceState to write in the bundle the returned value of ListView.onSaveInstanceState(), and restored it onActivityCreated
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (mListView != null) {
outState.putParcelable(LISTVIEW_INTERNAL_STATE_KEY, mListView.onSaveInstanceState());
}
}
after the data are reload then you can call
mListView.onRestoreInstanceState(savedInstanceState.getParcelable(LISTVIEW_INTERNAL_STATE_KEY));
Override onSaveInstanceState such as below"
#Override
public void onSaveInstanceState(Bundle savedInstanceState) {
super.onSaveInstanceState(savedInstanceState);
savedInstanceState.putInt("pos", pos);
}
Then in your onCreate method have read the savedInstanceState to check if this is an orientation change or a new activity.
private int pos = 0;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (savedInstanceState == null) {
//This is a new activity
}else{
pos = savedInstanceState.getInt("");
}
Now you have the position in the list, and you can scroll to this in configuration change.
Maybe after the data in the listview is reloaded,the code below will work.
mlvDictionaryIndex.setSelectionFromTop(mVisibleItem, 0);
So you can use post() method, just like below:
post(new Runnable() {
public void run() {
mlvDictionaryIndex.setSelectionFromTop(mVisibleItem, 0);
}
});

Categories

Resources