Further Understanding Setretaininstance(True)

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();
}
}
}
}

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.

setRetainInstance(true); doing it wrong?

For the first screen, i would use a seperate activity. CaseyB's answer is really good, and it is actually how I hand it as well (now, when you have alot of orientation changes, and a few things on the backstack, you'll get forcecloses, but its inevitable). What I would do is the first screen, make it its own activity, then call a second FragmentActivity and make the fragment on the Left side static (meaning define it in XML). That way you dont have to worry about it. Then, you can focus on the right fragment. If you set the setRetainInstance, then you dont need to worry about recreating the views. But, if it goes to the backstack, then you lose everything. It never calls onDestroy, only onDetach. And onCreate is only called the first time the fragment is created. So, I would avoid using onCreate at all, and focus on using onActivityCreated() and onAttach() to instantiate values and whatnot

Update -
Use the onCreate() method to set setRetainInstance(true) and call your JSON and network code to create the views. Its important to do it here, so that it only gets called once.
This should work entirely for orientation changes and going back and forth between fragments (the backstack will work, just you cant set it to retainInstance while in the backstack. That was a typo on my part).

Then, any other code you want called when the fragment is brought back up, use onAttach() since onCreate() wont be called since it was never destoryed

Then, in order to get control of your items again, you cant use getActivity().findViewById(), you have to use this.getView().findViewById(). I dont know why that is the case (since you would think that it would be attached to the activity viewgroup, but it isnt). That should be the only hiccup you should hit. Outside of that, it should work perfectly fine.

Does Fragments with setRetainInstance(true) survive process shutdowns?

Sounds like you're mixing up two concepts here.

  • Saving state across Configuration Changes does not involve serialization. If you request setRetainInstance() for a Fragment then that means it will fully stay in memory and not be re-created only for configuration changes. A similar mechanism is available for Activity objects but they need to explicitly define an Object which is going to be saved. This works via Activity.onRetainNonConfigurationInstance(), not via onSaveInstanceStae().
  • The other mechanism involves serialization and possibly (maybe not always, not sure) file system I/O to be able to reproduce state information even if an Activity/Fragment is destroyed (which happens independently of its hosting Process, btw). This works via Activity.onSaveInstanceState() and Fragment.onSaveInstanceState().
  • Of course, you can use the second mechanism for the purpose of the first, thus slowing down the way your app deals with configuration changes. Depending on your internal state, the slowdown could me marginal of significant.

Regarding your questions.

  • "My Fragment in contrast, is allowed to contain variables which are not serializable." Well, the same is true for your Activity. It can contain non-serializable objects which can be saved across config changes as described above.
  • "the fragment cannot be stored to disk when a process is shut down and must be recreated when an activity was restored." No, both mechanisms are available for both object types.

Hope I could contribute to clarifying this a bit.

Edit after your first comment.

Regarding the comment:

  • "onRetainNonConfigurationInstance is deprecated": Yes. I mentioned it for demonstration purposes because of a specific wording in your question. Also, with Android 2 devices having a 46% market share as per today (official Google figures), this method will definitely stay around for a very long time, deprecated or not.
  • "My main concern is about what will happen to the fragment instance when my hosting process is killed and removed from the memory": Your fragment instance will be removed from memory and there's of course no way it is restored as-is with its complete internal state automatically. This is only done when you setRetainInstanceState in the case of config changes. (But note that this relates to the Instance, in other words, the full object.)

Regarding your edit:

  • Once more, yes, your Fragment's Bundle will be stored and restored to/from the Bundle regardless of setRetainInstanceState if you use Fragment.onSaveInstanceState() for this purpose, for everything that makes sense.
  • It is not true that "all of its visible state" will be saved as the text you refer to claims; for example, the visibility attribute will not be saved. Whether that's supposed to be a bug or a feature I don't know, but it's a fact. But this is only a side remark; UI elements will save most of their relevant state.
  • "the state of the process is written to the file system": No! The state of objects which are able to save their state to a Bundle and actually implement saving their state will be saved in a Bundle, this means that you must provide such information yourself if you want your Fragment to save some state information. Also, again: No, this does not only relate to killing the process but also to deleting Activity and Fragment objects which are not visible; like the last Activity shown -- the Process may well stay alive.
  • "bundles are read for resuming the process": No, the Bundle will be read to pass it to the re-construction of Activity and/or Fragment objects, there is nothing done automatically in this process (except library objects which save their state also restore their state), but Android does not "resume" the "Process" from these Bundles.
  • "Since the retaining of fragments is not concerned with life cycle methods": Again, I think you're mixing up the two concepts. The "retaining" of a Fragment is only performed upon configuration changes _IF_ you request it via setRetainInstance, but we're mostly talking about the re-creation of Fragment objects from a Bundle here, which does involve the life cycle methods as documented by Google.
  • "I would not know how to retain e.g. a pointer to a network connection": Again, this must be a statement based on your mix-up. Of course you can keep a reference to a network connection upon config change (as requested per setRetainInstance) because when that happens, everything is simply kept in memory. Also, even if your Fragment gets deleted (because it became invisible) and your process is still there (because it shows the next Activity), you can (and should) keep references to objects which are expensive to re-create, such as a network connection, in your Application object, which exists as long as your process lives (more or less). It is only when your whole app is killed by Android that you lose everything, but the serialization we're discussing happens much more often.

Your conclusion:

I concluded that they surely needed to be recreated and that the life cycle methods are therefore to be preferred over setRetainInstance(true) whenever possible. Does this assumption make any sense?

Unfortunately not, since you are mixing up completely independent concepts.

I'll give it a final try:

  • You will want to keep a network connection reference which you need throughout your app in your Application object because it would be a lousy user experience if you created it from scratch on a regular basis throughout your app.
  • Your Application object will only die if Android kills your app.
  • Your Activity and Fragment objects will be deleted from your app regularly when the user moves forward within your app.
  • When the user presses "back", Android will re-create Activity and Fragment objects from Bundles using lifecycle methods. Saving something in a Bundle makes sense if you have expensive computations to do to re-create the internal state. You can live without the Bundle mechanism because Android will always save the Intent so if you don't do anything then you'll start without saved state.
  • When a configuration change occurs, Android lets you optimize user experience by keeping objects in memory across the config change. Here, Activity life cycle methods get involvwed and it's up to your implementation to use the saved data effectively. For Fragments, this is where setRetainInstance' comes into play: YourFragment` will survive the config change in memory if you set it.

Android - retaining fragment?

setRetainInstance(true): The Fragment's state will be retained (and not destroyed!) across configuration changes (e.g. screen rotate). The state will be
retained even if the configuration change causes the "parent" Activity to be destroyed. However, the view of the Fragment gets destroyed!

Lifecycle Calls:

  • onPause() -> onStop() -> onDestroyView() -> onDetach()
  • onAttach() -> onCreateView() -> onStart() -> onResume()

setRetainInstance(false): The Fragment's state will not be retained across configuration changes (default).

Lifecycle Calls:

  • onPause() -> onStop() -> onDestroyView() -> onDestroy() -> onDetach()
  • onAttach() -> onCreate() -> onCreateView() -> onStart() -> onResume()

Important: setRetainInstance(true) does not work with fragments on the back stack. setRetainInstance(true) is especially useful for long running operations
inside Fragments which do not care about configuration changes.

Get async response data from Application to Launcher Activity

If you're looking for best practices, you shouldn't extend an Application class for this.

There is many ways to persist your request state on screen rotation.

Consider to use a retained Fragment. This approach is deeply discussed:

Understanding Fragment's setRetainInstance(boolean)

Further understanding setRetainInstance(true)

All you need to do is this:

1. Fragment class

public class RequestFragment extends Fragment {

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

// This will be a guarantee that request is sent only once,
// because fragment won't be recreated on screen rotation:
setRetainInstance(true);

// Pereform sending request here.
}

}


Related Topics



Leave a reply



Submit