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.
[TOC]
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:
:::csharp
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
}
}
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.
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:
:::csharp
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.
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:
- 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. - A
TaskCompletionSourcethat can be used to provide the results asynchronously to the caller (via itsTaskproperty). - 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.
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:
:::csharp
/// <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.
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.
Footnotes
-
In the original version of this article I claimed incorrectly that the
OnActivityResultmethod did not exist in theFragmentclass in the Android SDK. This is incorrect. TheOnNewIntentandOnAttachedToWindowmethods don't exist onFragment, and we added those and forwarded them from the ourFragmentActivityclass, butOnActivityResultis built in. ↩