ActivityTask: A Helper for Async/Await on Android
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
Activityinstances by default.
awaitis oblivious to an
Activitylifecycle, it may execute continuations while in an undesired state causing an
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
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.
This library has two main classes:
ActivityScopeallows you to track the most recent instance of one of your Activity subclasses so that potential underlying recreations are transparent to you.
ActivityTaskacts as a standard
Taskreturn 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.
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)
// Set our view from the "main" layout resource
launched = true;
using (var scope = ActivityScope.Of(this))
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.