ActivityTask: A Helper for Async/Await on Android

Jérémie Laval

At Google I/O, several members of the Xamarin team attended the Architecture Component talk on Android lifecycle (which I recommend you watch). While the solutions presented there are interesting and, in some cases, map to patterns we already have in .NET, it resonated with those of us present because of how those Android lifecycle details make one specific C# feature more cumbersome to use: async/await.

With async/await, the two main nitpicks that pertain to Android developers are:

  • Because of the way the Android resource system works, a change of configuration (e.g. screen rotation) will recreate Activity instances by default.
  • Because await is oblivious to an Activity lifecycle, it may execute continuations while in an undesired state causing an IllegalStateException.

Note that we already introduced something that partially address those qualms in the form of the ActivityController, with the added bonus of providing convenient asynchronous wrapper methods for StartActivityForResult-based workflows.

Based on prior experience playing with C# 7’s new customizable async state machine drivers (see ValueTask for the more publicized example of this new C# feature), I figured there was potentially a way to leverage this to help mitigate those two issues without requiring too many changes in your code.

Enter ActivityTask (available on NuGet).

This library has two main classes:

  • ActivityScope allows you to track the most recent instance of one of your Activity subclasses so that potential underlying recreations are transparent to you.
  • ActivityTask acts as a standard Task return value in an async method, but customizes the state machine driver (in cooperation with ActivityScope) to make continuation scheduling aware of the activity lifecycle.

Under the hood, ActivityScope registers a listener at the Application level to listen to global activity lifecycle events. When it detects the activity it’s tracking is about to die, it marks it so that when it’s recreated it can re-associate with it. Because it implements an implicit conversion operator to Activity, you can pass the scope wherever an activity instance is needed to ensure you always use a valid value.

As for ActivityTask, the implementation is pretty boring (it pretty much defers to a TaskCompletionSource). The interesting part is ActivityScopeMethodBuilder, which drives the async state machine. For all intents and purposes, it will behave like the default driver for Task. However, thanks to the extra ActivityScope method argument, it will also make sure that any continuation is only executed when the activity tracked by the scope is in a usable state. If not, it will simply keep the continuation queued up until the tracked activity is resumed.

To see how all of this fits together, here’s the code for the activity of the test app in the GitHub repository:

[Activity(Label = "ActivityTaskTest", MainLauncher = true, Icon = "@mipmap/icon")]
public class MainActivity : Activity
{
	static bool launched = false;

	protected override async void OnCreate(Bundle savedInstanceState)
	{
		base.OnCreate(savedInstanceState);

		// Set our view from the "main" layout resource
		SetContentView(Resource.Layout.Main);

		if (!launched)
		{
			launched = true;
			using (var scope = ActivityScope.Of(this))
				await DoAsyncStuff(scope);
		}
	}

	TextView MyLabel(Activity activity) => activity.FindViewById(Resource.Id.myLabel);

	async ActivityTask DoAsyncStuff(ActivityScope scope)
	{
		await Task.Delay(3000); // Medium network call
		MyLabel(scope).Text = "Step 1";
		await Task.Delay(5000); // Big network call
		MyLabel(scope).Text = "Step 2";
	}
}

This example simulates launching a series of asynchronous operations the very first time an activity is created. Beforehand though, it creates an ActivityScope to track the lifetime of the current activity and pass it down. Between each asynchronous sub-step, the activity instance is used to fetch the label onscreen and update its text.

The idea is to trigger a damaging event during one of those Task.Delay calls. You can test this by rotating your device (thus killing and recreating the activity), or by pressing the home button to pause the activity and reopen it after the delay expires.

For instance, if rotating the screen after “Step 1” is shown, you should see the original text of the label briefly reappears (because the layout was inflated from scratch), and soon after you’ll see “Step 2” be set, which means the async method used the correct new activity instance to locate the label.

If pausing the activity after “Step 1” is shown, resuming the activity after the second delay expires should result in “Step 2” being displayed immediately, as the callback was executed during the resume process instead of running while the activity was in the background.

Discuss this post in the Xamarin Forums

0 comments

Discussion is closed.

Feedback usabilla icon