Yet Another Dev Blog

Fragments. Retaining state and objects.

There is a lot of good stuff written already about Activity and Fragment lifecycle and how to manage state and object retaining properly. So following is just a recap for future myself, rather than a tutorial.

Let's take a look at Activities and their destiny. There are 3 cases when and how activity can be destroyed.

First case: User enters activity A, from there goes to B, and then navigates back to A. Android won't save any instance state of activity B because user won't need this particular instance anymore. When he comes again to B he will get a fresh instance.

Second case: User enters activity A and rotates screen (or other configuration change occurs). Android has to recreate activity, so first it calls onSaveInstanceState(Bundle outState), destroys A and creates new instance, then calls onRestoreInstanceState(Bundle savedInstanceState) to restore state of views (the same can be done in onCreate(Bundle savedInstanceState)).

Third case: User enters activity A, from there goes to B, C, D… up the activity stack. At some point Android kills A to reclaim memory (or because you turned on useful feature Developer options -> Don't keep activities). Because user may eventually come back to A, system saves A's state before killing and later recreates it with savedInstanceState.



Fragment is tightly coupled to activity's lifecycle. It also has methods onSaveInstanceState(), onRestoreInstanceState() you can do restore in onActivityCreated() or onViewStateRestored().

This is how I usually create fragment. Why prefer static factory method, see for example here:

1
2
3
4
5
6
7
public static MyFragment newInstance(int index) {
  MyFragment fragment = new MyFragment();
  Bundle args = new Bundle();
  args.putInt("index", index);
  fragment.setArguments(args);
  return fragment;
}

Whatever we pass in arguments bundle also gets saved in case fragment has to be recreated later. Great! Now, let's say we have to pass an object to a fragment which is non-serializable. We can't put it in bundle, so my first thought would be:

1
2
3
4
5
6
7
8
9
10
public class MyFragment extends Fragment {
  private Wallet wallet;
  
  @Override
  public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      // non-serializable object
      wallet = ((WalletApplication) getActivity().getApplication()).getWallet();
  }
}

I think this get.get.get and a cast could look better. Ideally, we should not "talk to strangers", so we could use some DI here (read more on DI forms in Martin Fowler's article):

  • Constructor injection
  • Setter injection
  • Interface injection
1
2
3
4
5
6
public static MyFragment newInstance(int index, Wallet wallet) {
  MyFragment fragment = new MyFragment();
  fragment.setWallet(wallet);
  /// index goes into args 
  return fragment;
}

Now any activity knows, that in order to construct this fragment it'd have to provide a Wallet dependency.

Run the code, rotate the device and get NPE. This is our second case: Activity is recreated, so is fragment by calling no-arg constructor together with the same bundle that was passed to setArguments last time. Instance fields of course lost, wallet is null. Android doc helps us solve this by adding setRetainInstance(true). Rotating works fine now, fragment instance is saved in memory and not recreated.

When we get to third case, there is no retaining like with configuration change. So we end up with restrictions, that default constructor has to have no arguments - new Fragment(), and static factory method newInstance(Dependency), where we could pass a parameter, won't be called at the recreation time.

We can code following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class MainActivity extends FragmentActivity
      implements WalletProvider {
  
  @Override
  public Wallet provideWallet() {
      return wallet;
  }
  
}

public class TransactionsFragment extends ListFragment {
  private Wallet wallet;
  
  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
      super.onActivityCreated(savedInstanceState);
  
      // get Wallet dependency
      try {
          wallet = ((WalletProvider) getActivity()).provideWallet();
      } catch (ClassCastException e) {
          throw new ClassCastException(getActivity().getClass().getSimpleName() +
                                       " should implement WalletProvider interface");
      }
  
  }
}

Why do I put this in onActivityCreated(), rather than onCreate()? Because fragment's onCreate() is not guaranteed to be called after activity's onCreate() and our dependency might not yet be initialized in activity.



setRetainInstance(true) has also couple of features you should keep in mind.

  • Bundle savedInstanceState is always null. This kinda makes sense, why would you want to restore state if fragment is not destroyed?

  • Fragment's lifecycle becomes slightly different, i.e. onCreate() and onDestroy() won't be called:

In my case I had wallet.addEventListener(txEventListener) in onCreate() and wallet.removeEventListener(txEventListener) in onDestroy(). It was perfect, since these are not called on config change with retained instance and I wasn't removing-adding the same listener. However, if I add listener in onActivityCreated() to be sure that activity is fully created, I have to remove it in onDestroyView().

Comments