Mastodon

Sunday, May 10, 2015

Taming The Android Activity, Part 2

In part one of this series I covered how to use a Fragment to maintain state through configuration changes. In part two I will cover how to launch a new Activity (in-process or not) and wait for its results using C#'s async/await feature.

Starting Activities

As mentioned in part one, the Activity is designed to let applications reuse pieces of other applications. For instance, if you need to let the user pick a photo then you can construct an Intent object that describes what you want to do (pick a photo), and the OS will find an Activity for one of the installed applications that can satisfy that request. The other Activity may be in another application, but the Intent allows you to send it information about what you want, and then later it can send back the results (through another Intent).

The way the API works is you call StartActivity (a method in Activity) and give it either an Intent object or a Type (which will be used to construct an Intent). When you do that the OS will find the proper Activity, create it (possibly in another process), and then show it the user. Once that Activity is finished (either by the user pressing the back button or the Activity calling Finish()) then your original Activity will resume.

However, that's only for a simple case: launching a new Activity and ignoring any results. If you actually wanted to get information back from the other Activity then you need to instead call StartActivityForResult. This method is just like StartActivity, but it also takes an int that can be used to identify the request. Then you have to override the OnActivityResult method, which will give you back the request identifier along with a ResultCode and an Intent (for extra information). That basically looks like this:

private const int ImagePickerRequestCode = 1000;

private void LaunchOtherActivity()
{
    var intent = new Intent(Intent.ActionGetContent);
    intent.SetType("image/*");
    StartActivityForResult(intent, ImagePickerRequestCode);
}

public override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
    if (requestCode == ImagePickerRequestCode && resultCode == Result.Ok)
    {
        // Get the image from data
    }
}

The Problem

There are several problems with this, and the root of all of them is that all results for any Activity you launch must go through OnActivityResult. This means that any time you launch an Activity and want results you have to put some code in that function to handle the results. This is why you need the annoying int requestCode argument: that is the only way to identify which Activity you were launching so that you know what to do with the results. Now you have to be sure that each type of Activity that you launch in this way uses a different number, which is annoying.

Another problem is that this splits up your code. You start the Activity in one place, and then you handle the results elsewhere. There's no callback or anything that connects the two. You just have to know to go find that other function to see how the results are handled.

The worst problem is that this makes it very difficult to properly decouple responsibilities between views. What if you want to launch another Activity from a view inside a popup? You still have to handle the results in the Activity itself, and then you somehow have to get the results back to the view inside the popup.

This was my motivation for finding a better way. When you write a large application in Android you might have many different kinds of activities that you want to launch and then gather the results from, and forcing all of that code to go through a single function makes it very difficult to maintain.

Imagine a Better Way

Before we start trying to implement anything let's first think about what we want the API to look like (always a good first step to avoid APIs like this).

Obviously launching a new Activity and getting results is an asynchronous operation so we're going to have some kind of asynchronous API. We could do that with callbacks, which would be a huge improvement, but C# has an awesome feature that makes this even better: async/await. Using that feature we could imagine an API that looks like this:

var intent = new Android.Content.Intent(Android.Content.Intent.ActionGetContent);
intent.SetType("image/*");
var result = await StartActivityForResultAsync(intent);
if (result.ResultCode == Result.Ok)
{
    // Get the image from data
}

This looks awesome. There's no request code, and all of the code is in one place. Best of all, there's not even a callback. This code reads like normal synchronous code.

Gathering Requirements

Now that we know what we want the API to look like let's think about what we need to accomplish it. One of the benefits of the work we did in part one of this series is that each Activity (and also each Fragment) in our application shares a base class. That provides us with an opportunity to put some bookkeeping in the base class that can help us achieve our goal. Based on the API we have to start with, and the API we are trying to build, we need to keep track of this information:

  1. A request code for each launched Activity. To avoid requiring the caller to give us one we also need to somehow come up with new unique request codes automatically.
  2. A TaskCompletionSource that can be used to provide the results asynchronously to the caller (via its Task property).
  3. A mapping between the request codes and the TaskCompletionSources.

Based on this we can start to see an implementation forming. We can use an auto-incrementing request code (starting at some arbitrary value), and we can use a Dictionary to map between the request codes and the TaskCompletionSources.

While we're at it we will also solve another challenge that we often face when dealing with these Activity results: OnActivityResult is called before OnResume, which in some cases may be too early to actually deal with the results. Since we're already handling sending the results back asynchronously we can fix this by also delaying the results until after OnResume, which is far more useful.

Implementation

In the spirit of keeping as much logic in the Fragment instead of the Activity we will be putting this API in the FragmentBase class that we created in part one of this series. This is also important because we don't want to lose the bookkeeping information if there is a configuration change (like if the user rotates the device after you start the new Activity but before you get the results).

Our FragmentActivity class will be the same as before, but we will add some new code to the FragmentBase class. First, we need an interface for the results returned by our async method:

/// <summary>
/// The information returned by an Activity started with StartActivityForResultAsync.
/// </summary>
public interface IAsyncActivityResult
{
    /// <summary>
    /// The result code returned by the activity.
    /// </summary>
    Result ResultCode { get; }

    /// <summary>
    /// The data returned by the activity.
    /// </summary>
    Intent Data { get; }
}

Now we need to add our new methods to the FragmentBase class. I have omitted the parts that were unchanged for brevity.

/// <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
{
    // This is an arbitrary number to use as an initial request code for StartActivityForResultAsync.
    // It just needs to be high enough to avoid collisions with direct calls to StartActivityForResult, which typically would be 0, 1, 2...
    private const int FirstAsyncActivityRequestCode = 1000;

    // This is static so that they are unique across all implementations of FragmentBase.
    // This is important for the fragment initializer overloads of StartActivityForResultAsync.
    private static int _nextAsyncActivityRequestCode = FirstAsyncActivityRequestCode;
    private readonly Dictionary<int, AsyncActivityResult> _pendingAsyncActivities = new Dictionary<int, AsyncActivityResult>();
    private readonly List<AsyncActivityResult> _finishedAsyncActivityResults = new List<AsyncActivityResult>();

    #region Async Activity API

    public Task<IAsyncActivityResult> StartActivityForResultAsync<TActivity>(CancellationToken cancellationToken = default(CancellationToken))
    {
        return StartActivityForResultAsyncCore(requestCode => Activity.StartActivityForResult(typeof(TActivity), requestCode), cancellationToken);
    }

    public Task<IAsyncActivityResult> StartActivityForResultAsync(Intent intent, CancellationToken cancellationToken = default(CancellationToken))
    {
        return StartActivityForResultAsyncCore(requestCode => Activity.StartActivityForResult(intent, requestCode), cancellationToken);
    }

    public override void OnActivityResult(int requestCode, Result resultCode, Intent data)
    {
        AsyncActivityResult result;
        if (_pendingAsyncActivities.TryGetValue(requestCode, out result))
        {
            result.SetResult(resultCode, data);
            _pendingAsyncActivities.Remove(requestCode);
            _finishedAsyncActivityResults.Add(result);
        }

        base.OnActivityResult(requestCode, resultCode, data);
    }

    public override void OnResume()
    {
        base.OnResume();

        FlushPendingAsyncActivityResults();
    }

    private Task<IAsyncActivityResult> StartActivityForResultAsyncCore(Action<int> startActivity, CancellationToken cancellationToken)
    {
        var asyncActivityResult = SetupAsyncActivity();
        startActivity(asyncActivityResult.RequestCode);

        if (cancellationToken.CanBeCanceled)
        {
            cancellationToken.Register(() =>
                {
                    Activity.FinishActivity(asyncActivityResult.RequestCode);
                });
        }

        return asyncActivityResult.Task;
    }

    private void FlushPendingAsyncActivityResults()
    {
        foreach (var result in _finishedAsyncActivityResults)
        {
            result.Complete();
        }
        _finishedAsyncActivityResults.Clear();
    }

    private AsyncActivityResult SetupAsyncActivity()
    {
        var requestCode = _nextAsyncActivityRequestCode++;
        var result = new AsyncActivityResult(requestCode);
        _pendingAsyncActivities.Add(requestCode, result);

        return result;
    }

    private class AsyncActivityResult : IAsyncActivityResult
    {
        private readonly TaskCompletionSource<IAsyncActivityResult> _taskCompletionSource = new TaskCompletionSource<IAsyncActivityResult>();

        public int RequestCode { get; private set; }

        public Result ResultCode { get; private set; }

        public Intent Data { get; private set; }

        public Task<IAsyncActivityResult> Task { get { return _taskCompletionSource.Task; } }

        public AsyncActivityResult(int requestCode)
        {
            RequestCode = requestCode;
        }

        public void SetResult(Result resultCode, Intent data)
        {
            ResultCode = resultCode;
            Data = data;
        }

        public void Complete()
        {
            _taskCompletionSource.SetResult(this);
        }
    }

    #endregion
}

Now let's take a quick tour and see how this all works. First, there are two overloads of StartActivityForResultAsync. One takes a type of Activity as a generic argument because it's very common to want to start a new Activity in-process and just refer to it by its type. The other one takes an Intent, which is more flexible. You will typically use this variant for out-of-process activities.

These methods also take an optional CancellationToken, which can be used to cancel the launched Activity. If the token is cancelled then the code that is waiting on the async result will get a ResultCode that indicates that it was cancelled.

Both of those methods call into the same core routine that sets up the bookkeeping. It is responsible for creating the object that tracks the results and returning the Task that we can wait on.

Once the Activity has been started we return the Task object to the caller, and then we wait for a call to OnActivityResult. The OnActivityResult method is forward to the Fragment automatically.1 In OnActivityResult we check to see if the request code is one that is in our Dictionary of pending async activities. If it is then we record the ResultCode and the Intent, add it to a list of finished results, and then remove the item from our Dictionary. Later we will get a call to OnResume, which just goes through the list of finished results and calls the Complete method. That is the method that actually pushes the results into the TaskCompletionSource (by calling SetResult), which unblocks anything waiting on the Task.

And that's it. Now we have enough to start a new Activity and wait for its result in a single, small function. I have posted a full version of the code with a small example app on GitHub.

Summary

So far in the first two parts of this series I have covered how to preserve state across configuration changes and how to simplify the process of starting new activities and waiting for their results. With those two challenges solved we can avoid a lot of ugly boilerplate code. However, there is still one significant source of tedious boilerplate code: communicating between these activities still has to be done through Intent objects. In the next part of this series I will extend this code even further to allow for directly accessing the new Fragment object for in-process activities.


  1. In the original version of this article I claimed incorrectly that the OnActivityResult method did not exist in the Fragment class in the Android SDK. This is incorrect. The OnNewIntent and OnAttachedToWindow methods don't exist on Fragment, and we added those and forwarded them from the our FragmentActivity class, but OnActivityResult is built in. 

9 comments:

  1. Great article. Have you ever had to deal with nested fragments? My experience has been that OnActivityResult is not called for nested fragments; it is only called on the activity. (Nested fragments also have some "special" behavior with regard to RetainInstance, but I digress.) The problem I'm describing kind of kills the idea of having this code in a single place, since you end up launching an activity from a fragment, but handling the results of that call in the activity. Would love to know if you have any thoughts on hacking that!

    ReplyDelete
  2. I have not tried using nested fragments, but this code should be useful even with nested fragments because if you use the code described here then the code to deal with the activity result is handled in the base class. That is, you would have an activity and a top-level fragment for that activity, and those would be responsible for handling the bookkeeping necessary to be able to launch new activities and get their results. Any code anywhere in your application can then use that API (as long as it has access to the top-level activity fragment) without having to add any new code to either the activity or that top level fragment. Therefore you should be able to use this in a nested fragment as well, but you have to have the code shown here as a foundation.

    On a related note, your comment made me realize that I had made an error in my post. I claimed that OnActivityResult didn't exist in the base Fragment class in the Android SDK. I was confusing that method with some others. I corrected the post. Thanks!

    ReplyDelete
  3. I didn't null out _button because it gets recreated in OnCreateView, which overwrites the original instance. If there are view references that are not overwritten in OnCreateView then you should do the work to null them out in OnDestroyView. This is important because each view holds a reference to its context, and the context in this case is the activity. Therefore leaking a view means leaking an activity, which may mean leaking many other things.

    You may also find, as we did, that the GC doesn't always run when you want it to on Android. Typically a GC is triggered by a new allocation that hits some arbitrary limit (the nursery size for a generational garbage collector), but you may want to be more proactive if you know there are a lot of objects that can be collected. One thing I left out of the implementation of FragmentActivity which we used in our own implementation is some code in OnDestroy that would post a message to do a GC. The idea is that the activity likely holds a bunch of things in memory, and when it goes away it makes sense to do a GC right then. I went ahead and added that to the GitHub repository so you can check it out.

    The caveat is that because Android objects in Xamarin use finalizers they still won't get collected right away. Instead they have to go on the finalizer queue, and then once they're finalized they have to wait until the next GC to actually get collected. Also it appears that other things might be holding the activity in memory just long enough that it still won't even be finalized the first time. The result is even with this new code you have to rotate several times (going through OnDestroy multiple times) before the activity will actually be collected. That doesn't mean there's a leak, but it does mean some memory is being used unnecessarily until the GC runs a few times. If you have images you should be a lot more aggressive in disposing them as early as possible so that they don't require finalization.

    ReplyDelete
  4. I finally got around to reading this. Been on my "to read" list for way too long.

    I really like the idea of awaiting the result of another activity. Much simple interface as you explained. Makes me think on how ZXing.Net is implemented: https://github.com/Redth/ZXing.Net.Mobile/blob/master/src/ZXing.Net.Mobile/Android/MobileBarcodeScanner.cs#L57

    However, I do see one big problem: what if the caller activity gets destroyed by Android because of low memory while the callee is running? A new Activity will be created in the Android runtime and this one will receive the OnActivityResultCallback. However, the newly created Xamarin counterpart object is not awaiting this call. If any activity is still awaiting this, it is the Xamarin counterpart of the destroyed Activity. This will most likely go wrong. Or am I missing something?

    ReplyDelete
  5. That is an excellent question. Part 2 covers how to avoid losing state due to the destruction of the Activity due to configuration changes, but it doesn't stop the loss of state if the Activity is destroyed due to low memory. That's the corner case that I alluded to at the end of part 3. In our app I've dealt with the loss of state by just accepting that some state can be lost (basically I just treat it as reloading the document from scratch), but I did add some code to save/restore a few important pieces of state. However, for this use case the state is very important and can't really be serialized, which makes the whole thing kind of break down. Basically I've just decided that for in-process activities that scenario is unlikely enough to ignore, but you may decide differently.

    ReplyDelete
  6. (I meant part 1 covers the configuration changes, not 2)

    ReplyDelete
  7. When I try to call
    StartActivityForResultAsync(Intent intent, CancellationToken cancellationToken = default(CancellationToken))

    in my code, I am getting a n exception message saying: "Object reference not set to an instance of an object."

    ReplyDelete
    Replies
    1. That's not nearly enough information to help you. Try putting some example code up on GitHub and create an issue on that repo with the information from the exception object. I can comment on the GitHub issue to help you out. Just paste the link to the issue here so I can find it.

      Delete