Mastodon

Saturday, April 25, 2015

Taming The Android Activity, Part 1

The Activity is a core building block in an Android application, but in some cases it can be a pain to work with. This article is part one of a series showing how to deal with some of the complexities of the Activity in a clean way using some of the capabilities of C# available to Xamarin.Android developers.

Typically each screen in an Android application is represented by an Activity, and moving from one screen to another is done by starting a new Activity. In fact, the Activity and its related class Intent are the keys to Android's ability to let applications reuse pieces of other applications. That cross-application use case was the motivation behind the design of the Activity, but that design has the unfortunate side effect of making some common in-process use cases surprisingly difficult to deal with.

This series will cover three common scenarios that Android developers face in which the design of the Activity class causes problems:

  1. Dealing with device rotation (and other configuration changes)
  2. Launching and waiting for the results of another Activity
  3. Communicating between (in-process) Activitys

Each of these is very common and yet is surprisingly difficult to do on Android, especially compared to the way that view controllers work in iOS.

Configuration Changes

The Android documentation has this to say about configuration changes (emphasis added):

If the configuration of the device (as defined by the Resources.Configuration class) changes, then anything displaying a user interface will need to update to match that configuration. Because Activity is the primary mechanism for interacting with the user, it includes special support for handling configuration changes.

Unless you specify otherwise, a configuration change (such as a change in screen orientation, language, input devices, etc) will cause your current activity to be destroyed, going through the normal activity lifecycle process of onPause(), onStop(), and onDestroy() as appropriate.

What this means is that any "configuration change" will cause your Activity object to be destroyed and then recreated. What's a "configuration change"? There are actually a lot of things that count (see the complete list), but one of the most common is an orientation change. As a result, if a user rotates his device from portrait to landscape (or vice versa) then your Activity will be destroyed and recreated.

The most obvious consequence of having your Activity get destroyed is that all of the views also have to be recreated, but the most serious consequence is that all state stored in the Activity (meaning your fields and properties) will be lost, and a brand new instance of your Activity class will be created from scratch instead.

Handling Configuration Changes

There are a few approaches to dealing with this problem:

  1. Disable the behavior
  2. Serialize and deserialize your state
  3. Use a retained Fragment

The first option is to disable this behavior, which can be done by setting the configChanges property of the Activity to the configuration changes that you don't want to cause the Activity to be destroyed. This seems like a great option, and you will find many people recommending it. However, Google discourages this approach:

This technique should be considered a last resort when you must avoid restarts due to a configuration change and is not recommended for most applications.

The second option was the best approach for a while, and many apps no doubt still use this approach. When an Activity is being destroyed for a configuration change it will receive a call to OnSaveInstanceState. This method has Bundle argument that can be used to stash data using a key/value system. Basic types are supported by default, but more complex data types are difficult to deal with. Once the new Activity is created it will receive a call to OnRestoreInstanceState, which is handed the same Bundle object in order to fetch the stashed data.

The downside of this approach is that it is tedious and error prone. If you forget any fields then your application will have subtle bugs any time a user rotates his device. Ideally we could just throw a whole object from the old Activity to the new one. In fact, there is a deprecated API for doing just this (see OnRetainNonConfigurationInstance and GetLastNonConfigurationInstance), but that mechanism has been replaced by a better one.

Since Android 3.0 the current recommended solution to dealing with configuration changes is with a retained Fragment.

Fragments

A Fragment is kind of like a miniature Activity. It has a lifecycle, state, and optionally a view hierarchy. A Fragment is attached to an Activity via the Activity's FragmentManager.

By default whenever an Activity is destroyed for a configuration change its associated Fragments are also destroyed. However, you can request that a Fragment be retained by setting the RetainInstance property to true, which allows the entire object and all of its fields (but not its views) to be reused.

Therefore, the best technique for dealing with configuration changes is to actually move most of your Activity's code into a Fragment instead. The Activity is then merely a dumb shell used to create the Fragment and deal with Activity-specific issues like Intents. Here is an overview of how this technique works:

  1. When the Activity is created it uses its FragmentManager to search for an existing instance of its Fragment (identified by a tag string).
  2. If an existing Fragment was not found then a new one is created and added to the Activity.

Other than dealing with things like Intents and results from other Activitys (both to be covered in future segments of this series) the Activity class doesn't have much else to do. As you can imagine, the code to perform these steps will end up looking fairly boilerplate. In fact, this is a great opportunity for some code reuse.

Example

As an example of how to deal with this problem in a generic way let's start with the Xamarin.Android stock template that you get when you create a new application. If you run this template without any modifications then you get a simple application with a button on it. When you click the button it shows you how many times it has been clicked. In order to demonstrate the problem click the button a few times and then rotate the device. The first thing you'll notice is that the button text reverts to the original "Hello World" text. If you click the button again you'll also notice that the count starts over. This is the bug we will be fixing.

In order to fix this we will introduce two new base classes: FragmentActivity and FragmentBase. These classes can be dropped in to any application, and we will build on them further throughout this series to add other useful capabilities. For this problem, though, the classes are pretty simple. Here is the code:

/// <summary>
/// An Activity that uses a retained Fragment for its implementation.
/// </summary>
public abstract class FragmentActivity<TFragment> : Activity where TFragment : FragmentBase, new()
{
    /// <summary>
    /// The top-level fragment which manages the view and state for this activity.
    /// </summary>
    public FragmentBase Fragment { get; protected set; }

    /// <summary>
    /// The tag string to use when finding or creating this activity's fragment. This will be contructed using the type of this generic instance.
    /// </summary>
    protected string FragmentTag
    {
        get
        {
            return GetType().Name;
        }
    }

    /// <inheritdoc />
    protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);

        LoadFragment();
    }

    /// <inheritdoc />
    public override void OnAttachedToWindow()
    {
        base.OnAttachedToWindow();
        Fragment.OnAttachedToWindow();
    }

    /// <inheritdoc />
    protected override void OnNewIntent(Intent intent)
    {
        Fragment.OnNewIntent(intent);
    }

    /// <summary>
    /// Loads the fragment for this activity and stores it in the Fragment property.
    /// </summary>
    protected virtual void LoadFragment()
    {
        Fragment = FragmentBase.FindOrCreateFragment<TFragment>(this, FragmentTag, global::Android.Resource.Id.Content);
    }

    /// <inheritdoc />
    protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
    {
        Fragment.OnActivityResult(requestCode, resultCode, data);
    }
}

/// <summary>
/// The base class for top-level fragments in Android. These are the fragments which maintain the view hierarchy and state for each top-level
/// Activity. These fragments all use RetainInstance = true to allow them to maintain state across configuration changes (i.e.,
/// when the device rotates we reuse the fragments). Activity classes are basically just dumb containers for these fragments.
/// </summary>
public abstract class FragmentBase : Fragment
{
    /// <summary>
    /// Tries to locate an already created fragment with the given tag. If the fragment is not found then a new one will be created and inserted into
    /// the given activity using the given containerId as the parent view.
    /// </summary>
    /// <typeparam name="TFragment">The type of fragment to create.</typeparam>
    /// <param name="activity">The activity to search for or create the view in.</param>
    /// <param name="fragmentTag">The tag which uniquely identifies the fragment.</param>
    /// <param name="containerId">The resource ID of the parent view to use for a newly created fragment.</param>
    /// <returns></returns>
    public static TFragment FindOrCreateFragment<TFragment>(Activity activity, string fragmentTag, int containerId) where TFragment : FragmentBase, new()
    {
        var fragment = activity.FragmentManager.FindFragmentByTag(fragmentTag) as TFragment;
        if (fragment == null)
        {
            fragment = new TFragment();
            activity.FragmentManager.BeginTransaction().Add(containerId, fragment, fragmentTag).Commit();
        }

        return fragment;
    }

    /// <inheritdoc />
    public override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);

        RetainInstance = true;
    }

    /// <summary>
    /// Called when this fragment's activity is given a new Intent.
    /// </summary>
    /// <remarks>The default implementation does nothing</remarks>
    public virtual void OnNewIntent(Intent intent)
    {
    }

    /// <summary>
    /// Called when this fragment's activity is attached to a window.
    /// </summary>
    /// <remarks>The default implementation does nothing</remarks>
    public virtual void OnAttachedToWindow()
    {
    }
}

As you can see, FragmentActivity is a generic abstract class with a type parameter to specify the type of the Fragment that it should use. This class creates and holds on to the Fragment and forwards a few useful messages to the Fragment that otherwise would be handled by the Activity. The FragmentBase class is an abstract class that is used as the base class for any Fragments used by a FragmentActivity. Its static FindOrCreateFragment method, as its name implies, is used to find an existing instance of a Fragment or create a new one if one is not found. Also note that RetainInstance is set to true when this Fragment is created, which is the key to allowing the instance to be reused. The other methods are just there to receive notifications from the Activity.

The way you use these classes is simple. For each Activity you will create a pair of classes: a Fragment that inherits from FragmentBase and an Activity that inherits from FragmentActivity. The generic type argument links them together. In many cases the Activity doesn't need any additional code (it will just be an empty class), and all of the important code goes in the Fragment. To see this in action this is how I adapted the Xamarin.Android template to use these new classes (complete code):

[Activity(Label = "FragmentActivityTest", MainLauncher = true, Icon = "@drawable/icon")]
public class MainActivity : FragmentActivity<MainActivityFragment>
{
}

public class MainActivityFragment : FragmentBase
{
    private int _count = 0;
    private Button _button;

    public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
    {
        var view = inflater.Inflate(Resource.Layout.Main, container, attachToRoot: false);

        _button = view.FindViewById<Button>(Resource.Id.myButton);

        if (_count != 0)
        {
            UpdateButtonText();
        }

        _button.Click += delegate
            {
                _count++;
                UpdateButtonText();
            };

        return view;
    }

    private void UpdateButtonText()
    {
        _button.Text = string.Format("{0} clicks!", _count);
    }
}

The first thing you'll notice is that, as I mentioned before, the Activity has no code in it. Looking at the Fragment you'll notice that it looks very similar to the original code, but with a few key tweaks. Let's look at what I had to change.

The first change was the loading of the layout file. The original code looked like this:

SetContentView(Resource.Layout.Main);

SetContentView is a method of Activity, but we're not in an Activity anymore. Instead, we just have to load the layout and return it. For this purpose we are given a LayoutInflater, and we use it like this:

var view = inflater.Inflate(Resource.Layout.Main, container, attachToRoot: false);

There are two additional arguments being used here, and both are important. The first is the container ViewGroup that is passed to us as an argument to OnCreateView. This is the container in which the view we return will be inserted. The second is a boolean that tells the LayoutInflater that we don't want it to insert the inflated view into that container.1

Moving on, we have an extra bit of initialization code:

if (_count != 0)
{
    UpdateButtonText();
}

This code is necessary because while the Fragment is reused across configuration changes the view is not. That means this method will be called multiple times on the same Fragment instance2, and it is up to us to initialize the view with the current state3. In order to handle this I created a field for the button and refactored the code for updating the text into a method that I can call in multiple places. Since the initial text comes from the layout file and doesn't match the format used after clicking the button I check first to see if the button has ever been pressed.

Lastly, I return the view that was created since that is needed by the Activity in order to insert it into the view hierarchy.

Summary

As you can see, this approach makes it very easy to handle configuration changes by mostly just ignoring them. The only remaining wrinkle is that the views are still destroyed and recreated, which requires a bit of extra initialization. This is necessary because the views may actually adapt to the configuration change. The good news is that we no longer have to tediously save and restore every state field.

I use this technique in every Android application I write, and it works very well. I hope that this post and the example code will help you save time dealing with the same issue.

In the next part of this series I will explain how to deal with the problem of starting a new Activity and waiting for its results.


  1. This may seem strange. Why are we passing in the container if we aren't inserting the view into it? It turns out that there is a good reason for this, which is explained in detail by this article. If you leave off the last argument the default is true, and you will get an exception because the view is inserted twice. If you pass null as the second argument then you won't crash, but your top-level LayoutParams in your layout file will be ignored. Thus we pass in the container and we tell it not to attach. 

  2. There is a corresponding method OnDestroyView, which can be used to do any necessary cleanup when the old view is destroyed during a configuration change (or when the Fragment itself is going to be destroyed). 

  3. Some views do their own saving and restoring of temporary state during configuration changes. For instance, an EditText will save the text that has been entered, the cursor location, and the current selection. This is handled by OnSaveInstanceState and OnRestoreInstanceState in the View class

3 comments:

  1. Hi Adam,

    Thanks for your blog posts. I learned C#, iOS, and Android entirely by bumbling around myself, so I appreciate your posts which teach more nuanced, quality software engineering practices instead of brute force coding which I see happening a lot in the mobile space.

    I have a question about how this concept would be extended to an Activity that contains multiple fragments. For example I have an activity that is a split-pane setup (think UISplitViewController in iOS) when the screen is wide enough and is otherwise a single pane. Each pane is its own fragment. The master pane is further complicated in that it has Fragments pushed onto it (via BeginTransaction().Replace() ) to mimic pushing view controllers on a UINavigationController in iOS.

    The Android docs say RetainInstance = true; only works for Fragments not added to the back stack, which wouldn't work in such a situation because the backstack is needed for the pushing/popping of view controllers on the master pane.

    Have you ever encountered this scenario. Perhaps I am missing an obvious solution?

    Thanks again for the article.

    Cory

    ReplyDelete
  2. That is an excellent question. This article series is primarily targeted towards single-fragment activities as a replacement for bare activities, but your use case is actually one of the original use cases of fragments. That is, Google intended fragments to be useful as a way of reusing UI components between phone and tablet UXes where a tablet app may show two fragments side by side while the phone app shows one after the other.

    That's a more complex situation, but I think much of the same advice still applies. From what I can tell, despite the note in the documentation, it is still valid to set RetainInstance to true for a fragment on the back stack, but you have to understand how that affects the life cycle of the fragment.

    I know for a fact that one of our applications uses retained fragments on the back stack, and I'm not aware of any issues with that. The fragments keep their state through configuration changes and work properly when navigated to via the back stack. Our use case is replicating an iOS-like navigation controller in a dialog activity (a non-fullscreen modal activity), and for that purpose we have both a retained fragment to keep track of the state of the activity (as described in this article) as well as a fragment for the current view. The view fragment can be replaced with another one, and the old one goes on the back stack. Again, this works for us even when those fragments have RetainInstance set to true. So my advice is to just try it.

    ReplyDelete
  3. Thanks Adam. I am trying using RetainInstance = true and everything is working well so far.

    I did a memory dump when I initially started and noticed that there were multiple activity instances when rotating. My code is in a state of flux right now so I am not too concerned about it right now. I do have a question though. Since the Fragment instance is never destroyed, references to UI elements persist when the Activity is killed. I notice that you do not set your _button variable to null when OnDestroyView() is called. Is it safe to assume that things will be OK as long as all view references are rebound in OnCreateView()?

    ReplyDelete