Why Use Fragment#Setretaininstance(Boolean)

Why use Fragment#setRetainInstance(boolean)?


How do you as a developer use this

Call setRetainInstance(true). I typically do that in onCreateView() or onActivityCreated(), where I use it.

and why does it make things easier?

It tends to be simpler than onRetainNonConfigurationInstance() for handling the retention of data across configuration changes (e.g., rotating the device from portrait to landscape). Non-retained fragments are destroyed and recreated on the configuration change; retained fragments are not. Hence, any data held by those retained fragments is available to the post-configuration-change activity.

Understanding Fragment's setRetainInstance(boolean)

First of all, check out my post on retained Fragments. It might help.

Now to answer your questions:

Does the fragment also retain its view state, or will this be recreated on configuration change - what exactly is "retained"?

Yes, the Fragment's state will be retained across the configuration change. Specifically, "retained" means that the fragment will not be destroyed on configuration changes. That is, the Fragment will be retained even if the configuration change causes the underlying Activity to be destroyed.

Will the fragment be destroyed when the user leaves the activity?

Just like Activitys, Fragments may be destroyed by the system when memory resources are low. Whether you have your fragments retain their instance state across configuration changes will have no effect on whether or not the system will destroy the Fragments once you leave the Activity. If you leave the Activity (i.e. by pressing the home button), the Fragments may or may not be destroyed. If you leave the Activity by pressing the back button (thus, calling finish() and effectively destroying the Activity), all of the Activitys attached Fragments will also be destroyed.

Why doesn't it work with fragments on the back stack?

There are probably multiple reasons why it's not supported, but the most obvious reason to me is that the Activity holds a reference to the FragmentManager, and the FragmentManager manages the backstack. That is, no matter if you choose to retain your Fragments or not, the Activity (and thus the FragmentManager's backstack) will be destroyed on a configuration change. Another reason why it might not work is because things might get tricky if both retained fragments and non-retained fragments were allowed to exist on the same backstack.

Which are the use cases where it makes sense to use this method?

Retained fragments can be quite useful for propagating state information — especially thread management — across activity instances. For example, a fragment can serve as a host for an instance of Thread or AsyncTask, managing its operation. See my blog post on this topic for more information.

In general, I would treat it similarly to using onConfigurationChanged with an Activity... don't use it as a bandaid just because you are too lazy to implement/handle an orientation change correctly. Only use it when you need to.

Why is the fragment's setRetainInstance(true) method not working?

UPDATE: I decided not to use setRetainInstance(true) at all and I solved the problem using the ObjectInputStream and ObjectOutputStream classes and saved the arrHashMap object to a file in the onSaveInstanceState(Bundle outState) method of MyActivity, and retrieved the arrHashMap object from that file in the onRestoreInstanceState(Bundle savedInstanceState) method of MyActivity. I then proceeded to set the adapter with the retrived arrHashMap object.

As an added note, I changed the arrHashMap instance variable of MyFragment to a static variable so it could be accessed from MyActivity.

Save Code:

@Override
public void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);

try
{
File f = new File( this.getDir("myDir", Context.MODE_PRIVATE), "arrHashMap");
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(f));
os.writeObject(MyFragment.arrHashMap);
os.flush();
os.close();
}
catch(IOException e)
{
return;
}
}

Restore Code:

@Override
public void onRestoreInstanceState(Bundle savedInstanceState)
{
super.onRestoreInstanceState(savedInstanceState);

ArrayList<HashMap<String,String>> arrHashMap;
try
{
File f = new File( this.getDir("myDir", Context.MODE_PRIVATE), "arrHashMap");
ObjectInputStream is = new ObjectInputStream(new FileInputStream(f));
arrHashMap = ((ArrayList<HashMap<String, String>>) is.readObject() );
is.close();
}
catch (Exception e)
{
arrHashMap = null;
}
if(arrHashMap != null)
{
ListView lv = (ListView)findViewById(R.id.fragment_lv);
CustomListViewAdapter adapter = new CustomListViewAdapter(this, arrHashMap);
lv.setAdapter(adapter);
lv.setOnItemClickListener(this);
}
}

How to use setRetainInstance(boolean)?

As mentioned in @Selvin's comment, you should let the UI element to be recreated.

Some information which you should know:

setRetainInstance(true) should be used for non-UI Fragment only. And my personal advice would be not to consider this first, unless you are run out of option.

To properly handle a restart, it is important that your activity
restores its previous state through the normal Activity lifecycle, in
which Android calls onSaveInstanceState() before it destroys your
activity so that you can save data about the application state. You
can then restore the state during onCreate() or
onRestoreInstanceState().

You are right about using onSaveInstanceState(Bundle), in general, you should use to save your state. Please be noted that, it is the state you save, but not the UI or the whole Fragment.

For example, a state can be a count on how many times a button is clicked.
Check the link below on how to save state

http://developer.android.com/training/basics/activity-lifecycle/recreating.html#SaveState

Moreover, some UI states, e.g. text inputted in EditText are already handled in the system API. So you only need to handle states you maintained by yourself.

Edit:

If you are new to this, and do not know what you need to save and what do not, simply skip it first, and play around orientation change WITHOUT onSaveInstanceState. Then you would soon find out what is lost in the process, and that would be the state which you need to keep.

Why use Fragment#setRetainInstance(boolean)?


How do you as a developer use this

Call setRetainInstance(true). I typically do that in onCreateView() or onActivityCreated(), where I use it.

and why does it make things easier?

It tends to be simpler than onRetainNonConfigurationInstance() for handling the retention of data across configuration changes (e.g., rotating the device from portrait to landscape). Non-retained fragments are destroyed and recreated on the configuration change; retained fragments are not. Hence, any data held by those retained fragments is available to the post-configuration-change activity.

setRetainInstance(true) in onCreate fragment in android

I'll try to put it as simply as possible.

Your fragment, probably, holds UI elements - like:

private TextView mView

These UI elements get initialized in onCreateView(LayoutInflater, ViewGroup, Bundle) method of a fragment, using the LayoutInflater class instance.

But this LayoutInflater instance uses Activity's context to inflate your resources. So if your UI widgets are stored in your fragment class, they implicitly possess the Context of the associated activity - that is, the activity to which you've attached it to by commiting FragmentManager's transaction.

Now imagine a configuration change occures (screen rotation, for example). Activity gets destroyed and a new one is created. Old Activity's context normally should be garbage collected. But your retained fragment instance holds a strong reference to this context via each one of your stored UI elements, and thus this context is "reachable" in terms of GC, and should not be garbage-collected. A memory leak occurs.

That's why retained instance fragments should not be used as UI-fragments.

Just remember - even if you do not store a reference to Context object, like private Context context (that's what you asked), you might leak this context in a plenty of possibilities.

Is using Fragment's setRetainInstance(true) really a good practice to handle rotation change


  • Configuration changes: when suddenly screen becomes much wider and much less in height (typical landscape), it is apt for a visual component to update its display and more intelligently use the screen available. Another examples of config change are user sliding the hardware keyboard, device language changing, and so on. why re-start :

    • Android components favor declarative layout, you load a bunch of XML layouts, and work from there. Finding every View and re-arranging/updating it in real time will be a mess, not to mention the re-wiring of all the event handlers and other custom View code. Its way easier to reload another bunch of layout files.

    • Also, In Android, Activities kind of live at the mercy of system, so naturally, Activity life cycle is so designed (and recommended) that it is capable of re-creating itself on demand , any time, just as it was before it was destroyed. This pattern accommodates all re-starts, those due to configuration changes as well. If you make your Activities and Fragments capable of maintaining an eternal state, configuration changes won't be that much of a problem.

    • Retain state data (Models), not the stuff displaying it (UI and Views).

  • setRetainInstance(true): It is recommended only to be used with fragments that do not hold any reference to anything, that will be recreated on rotation. This means you should not use it on any Fragment that holds Context, Views, etc. A typical Visual fragment does. But it is very useful with Fragments that hold objects like running Threads, AsyncTasks, Data Collections, loaded assets, fetched results etc. This method helps in using a non visual Fragment, as a detachable holder, for non Context-dependent objects of an Activity.

Further understanding setRetainInstance(true)

Ok, perhaps I was slightly too harsh on the Android documentation, because it does have some useful information, but sadly none of it is linked from setRetainInstance(). From the page about fragments

Note: Each fragment requires a unique identifier that the system can
use to restore the fragment if the activity is restarted (and which
you can use to capture the fragment to perform transactions, such as
remove it). There are three ways to provide an ID for a fragment:

  • Supply the android:id attribute with a unique ID.
  • Supply the android:tag attribute with a unique string.
  • If you provide neither of the previous two, the system uses the ID of the container view.

This strongly implies that if you do setContentView(R.layout.whatever) in Activity.onCreated() and that layout contains a fragment with setRetainInstance(true), then when the activity is recreated it will be searched for again using its id or tag.

Secondly, for UI-less fragments, it states

To add a fragment without a UI, add the fragment from the activity
using add(Fragment, String) (supplying a unique string "tag" for the
fragment, rather than a view ID). This adds the fragment, but, because
it's not associated with a view in the activity layout, it does not
receive a call to onCreateView(). So you don't need to implement that
method.

And the docs link to a very good example - FragmentRetainInstance.java which I have reproduced below for your convenience. It does exactly what I speculated was the answer in my question (if (...findFragmentByTag() == null) { ...).

Finally, I created my own test activity to see exactly what functions are called. It outputs this, when you start in portrait and rotate to landscape. The code is below.

(This is edited a bit to make it easier to read.)

TestActivity@415a4a30: this()
TestActivity@415a4a30: onCreate()
TestActivity@415a4a30: Existing fragment not found.
TestFragment{41583008}: this() TestFragment{41583008}
TestFragment{41583008}: onAttach(TestActivity@415a4a30)
TestFragment{41583008}: onCreate()
TestFragment{41583008}: onCreateView()
TestFragment{41583008}: onActivityCreated()
TestActivity@415a4a30: onStart()
TestFragment{41583008}: onStart()
TestActivity@415a4a30: onResume()
TestFragment{41583008}: onResume()

<rotate device>

TestFragment{41583008}: onPause()
TestActivity@415a4a30: onPause()
TestFragment{41583008}: onStop()
TestActivity@415a4a30: onStop()
TestFragment{41583008}: onDestroyView()
TestFragment{41583008}: onDetach()
TestActivity@415a4a30: onDestroy()
TestActivity@415a3380: this()
TestFragment{41583008}: onAttach(TestActivity@415a3380)
TestActivity@415a3380: onCreate()
TestActivity@415a3380: Existing fragment found.
TestFragment{41583008}: onCreateView()
TestFragment{41583008}: onActivityCreated()
TestActivity@415a3380: onStart()
TestFragment{41583008}: onStart()
TestActivity@415a3380: onResume()
TestFragment{41583008}: onResume()

Note that the Android documentation is wrong: the UI-less fragment does receive a call to onCreateView() but it is free to return null.

Source code for TestActivity/TestFragment

import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.concentriclivers.ss.R;

// An activity for understanding Android lifecycle events.
public class TestActivity extends Activity
{
private static final String TAG = TestActivity.class.getSimpleName();

public TestActivity()
{
super();
Log.d(TAG, this + ": this()");
}

protected void finalize() throws Throwable
{
super.finalize();
Log.d(TAG, this + ": finalize()");
}

@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
Log.d(TAG, this + ": onCreate()");


TextView tv = new TextView(this);
tv.setText("Hello world");
setContentView(tv);

if (getFragmentManager().findFragmentByTag("test_fragment") == null)
{
Log.d(TAG, this + ": Existing fragment not found.");
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.add(new TestFragment(), "test_fragment").commit();
}
else
{
Log.d(TAG, this + ": Existing fragment found.");
}
}

@Override
public void onStart()
{
super.onStart();
Log.d(TAG, this + ": onStart()");
}

@Override
public void onResume()
{
super.onResume();
Log.d(TAG, this + ": onResume()");
}

@Override
public void onPause()
{
super.onPause();
Log.d(TAG, this + ": onPause()");
}

@Override
public void onStop()
{
super.onStop();
Log.d(TAG, this + ": onStop()");
}

@Override
public void onDestroy()
{
super.onDestroy();
Log.d(TAG, this + ": onDestroy()");
}




public static class TestFragment extends Fragment
{
private static final String TAG = TestFragment.class.getSimpleName();

public TestFragment()
{
super();
Log.d(TAG, this + ": this() " + this);
}

@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
Log.d(TAG, this + ": onCreate()");
setRetainInstance(true);
}

@Override
public void onAttach(final Activity activity)
{
super.onAttach(activity);
Log.d(TAG, this + ": onAttach(" + activity + ")");
}

@Override
public void onActivityCreated(Bundle savedInstanceState)
{
super.onActivityCreated(savedInstanceState);
Log.d(TAG, this + ": onActivityCreated()");
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
Log.d(TAG, this + ": onCreateView()");
return null;
}

@Override
public void onViewCreated(View view, Bundle savedInstanceState)
{
super.onViewCreated(view, savedInstanceState);
Log.d(TAG, this + ": onViewCreated()");
}

@Override
public void onDestroyView()
{
super.onDestroyView();
Log.d(TAG, this + ": onDestroyView()");
}

@Override
public void onDetach()
{
super.onDetach();
Log.d(TAG, this + ": onDetach()");
}

@Override
public void onStart()
{
super.onStart();
Log.d(TAG, this + ": onStart()");
}

@Override
public void onResume()
{
super.onResume();
Log.d(TAG, this + ": onResume()");
}

@Override
public void onPause()
{
super.onPause();
Log.d(TAG, this + ": onPause()");
}

@Override
public void onStop()
{
super.onStop();
Log.d(TAG, this + ": onStop()");
}

@Override
public void onDestroy()
{
super.onDestroy();
Log.d(TAG, this + ": onDestroy()");
}
}

}

Source code for FragmentRetainInstance.java (as of API 16):

/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.android.apis.app;

import com.example.android.apis.R;

import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ProgressBar;

/**
* This example shows how you can use a Fragment to easily propagate state
* (such as threads) across activity instances when an activity needs to be
* restarted due to, for example, a configuration change. This is a lot
* easier than using the raw Activity.onRetainNonConfiguratinInstance() API.
*/
public class FragmentRetainInstance extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// First time init, create the UI.
if (savedInstanceState == null) {
getFragmentManager().beginTransaction().add(android.R.id.content,
new UiFragment()).commit();
}
}

/**
* This is a fragment showing UI that will be updated from work done
* in the retained fragment.
*/
public static class UiFragment extends Fragment {
RetainedFragment mWorkFragment;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_retain_instance, container, false);

// Watch for button clicks.
Button button = (Button)v.findViewById(R.id.restart);
button.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
mWorkFragment.restart();
}
});

return v;
}

@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);

FragmentManager fm = getFragmentManager();

// Check to see if we have retained the worker fragment.
mWorkFragment = (RetainedFragment)fm.findFragmentByTag("work");

// If not retained (or first time running), we need to create it.
if (mWorkFragment == null) {
mWorkFragment = new RetainedFragment();
// Tell it who it is working with.
mWorkFragment.setTargetFragment(this, 0);
fm.beginTransaction().add(mWorkFragment, "work").commit();
}
}

}

/**
* This is the Fragment implementation that will be retained across
* activity instances. It represents some ongoing work, here a thread
* we have that sits around incrementing a progress indicator.
*/
public static class RetainedFragment extends Fragment {
ProgressBar mProgressBar;
int mPosition;
boolean mReady = false;
boolean mQuiting = false;

/**
* This is the thread that will do our work. It sits in a loop running
* the progress up until it has reached the top, then stops and waits.
*/
final Thread mThread = new Thread() {
@Override
public void run() {
// We'll figure the real value out later.
int max = 10000;

// This thread runs almost forever.
while (true) {

// Update our shared state with the UI.
synchronized (this) {
// Our thread is stopped if the UI is not ready
// or it has completed its work.
while (!mReady || mPosition >= max) {
if (mQuiting) {
return;
}
try {
wait();
} catch (InterruptedException e) {
}
}

// Now update the progress. Note it is important that
// we touch the progress bar with the lock held, so it
// doesn't disappear on us.
mPosition++;
max = mProgressBar.getMax();
mProgressBar.setProgress(mPosition);
}

// Normally we would be doing some work, but put a kludge
// here to pretend like we are.
synchronized (this) {
try {
wait(50);
} catch (InterruptedException e) {
}
}
}
}
};

/**
* Fragment initialization. We way we want to be retained and
* start our thread.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// Tell the framework to try to keep this fragment around
// during a configuration change.
setRetainInstance(true);

// Start up the worker thread.
mThread.start();
}

/**
* This is called when the Fragment's Activity is ready to go, after
* its content view has been installed; it is called both after
* the initial fragment creation and after the fragment is re-attached
* to a new activity.
*/
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);

// Retrieve the progress bar from the target's view hierarchy.
mProgressBar = (ProgressBar)getTargetFragment().getView().findViewById(
R.id.progress_horizontal);

// We are ready for our thread to go.
synchronized (mThread) {
mReady = true;
mThread.notify();
}
}

/**
* This is called when the fragment is going away. It is NOT called
* when the fragment is being propagated between activity instances.
*/
@Override
public void onDestroy() {
// Make the thread go away.
synchronized (mThread) {
mReady = false;
mQuiting = true;
mThread.notify();
}

super.onDestroy();
}

/**
* This is called right before the fragment is detached from its
* current activity instance.
*/
@Override
public void onDetach() {
// This fragment is being detached from its activity. We need
// to make sure its thread is not going to touch any activity
// state after returning from this function.
synchronized (mThread) {
mProgressBar = null;
mReady = false;
mThread.notify();
}

super.onDetach();
}

/**
* API for our UI to restart the progress thread.
*/
public void restart() {
synchronized (mThread) {
mPosition = 0;
mThread.notify();
}
}
}
}


Related Topics



Leave a reply



Submit