Background
Like many Android developers I’ve been excitedly following the development of Google’s WorkManager over the last couple of years. Finally a reliable means of scheduling tasks even if the app exits, crashes or runs in the background! It’s basically a guarantee that the task will be done.
I’m not going to talk about the architecture or how it works, there are plenty of good articles on the subject. What I am going to talk about is getting started with both the Worker and ListenableWorker in Xamarin Android. This post was inspired by following the official blog post on WorkManager in Android, as well as a comment by Tim Cook on the blog post. But we’ll get back to that.
Worker
Workers are the backbone of WorkManager. A Worker is run synchronously on a background thread. It has a simple DoWork() method where you can run something like zipping a file. You don’t need to worry about threading. You should be using Workers where possible.
Tim Cook’s comment on this article is interesting:
Android docs say “WorkManager performs background work asynchronously on your behalf”, but when talking about Worker it says “doWork is a synchronous call – you are expected to do the entirety of your background work in a blocking fashion and finish it by the time the method exits. If you call an asynchronous API in doWork() and return a Result, your callback may not operate properly. If you find yourself in this situation, consider using a ListenableWorker”
Your task must finish when DoWork() is completed, so Workers are not suitable for async tasks. If you’ve been working with .NET for the last couple of years you’ll realize that a lot of code is now async so you will probably have a need for at least one ListenableWorker.
ListenableWorker
ListenableWorkers run asynchronously starting on the main thread, which you should then move to a background thread. A StartWork() method kicks off execution, and returns a ListenableFuture/callback when the method completes.
A ListenableWorker can also report on its progress easily to UI elements, which the Worker cannot do. It’s great for downloading or uploading data.
Demo
Let’s create a demo to show both of these in action, as well as how to view the status of a worker. You can view the source code to this demo at https://github.com/DamienDennehy/XamarinAndroidWorkerDemos
Getting Started
Create a Xamarin.Android app in Visual Studio to get started.
UI Updates
Let’s make a few tweaks to the UI so that it is more suited to our needs. https://github.com/DamienDennehy/XamarinAndroidWorkerDemos/commit/140ad02a5018ae888b28fb12a02d03ea6e7db01b
We want a simple screen to allow two menu options to be clicked, Start Worker and Start Listenable Worker.
NuGet Packages
While the 2019 article on the Xamarin WorkManager suggests you install the Xamarin.Android.Arch.Work.Runtime package, this information is out of date.
Install the following packages instead in this order:
- Xamarin.AndroidX.Browser
- Xamarin.Google.Android.Material
- Xamarin.AndroidX.Work.Runtime
- Xamarin.AndroidX.Concurrent.Futures
Adding & running a Worker
Let’s add a Simple Worker.
using Android.Content;
using Android.Util;
using AndroidX.Work;
using System.Threading;
namespace XamarinAndroidWorkerDemos.Workers
{
public class SimpleWorker : Worker
{
public const string TAG = "SimpleWorker";
public SimpleWorker(Context context, WorkerParameters workerParams) :
base(context, workerParams)
{
}
public override Result DoWork()
{
Log.Debug(TAG, "Started.");
//Perform a process here, simulated by sleeping for 5 seconds.
Thread.Sleep(5000);
Log.Debug(TAG, "Completed.");
return Result.InvokeSuccess();
}
}
}
We can run the SimpleWorker from the MainActivity.
var simpleWorkerRequest = new OneTimeWorkRequest.Builder(typeof(SimpleWorker))
.AddTag(SimpleWorker.TAG)
.Build();
WorkManager.GetInstance(this).BeginUniqueWork(
SimpleWorker.TAG, ExistingWorkPolicy.Keep, simpleWorkerRequest)
.Enqueue();
If you’re not familiar with the WorkManager syntax, we’re declaring an instance of the work and starting it.
Start the Worker by clicking “Start Worker”. You will see…nothing really, because we haven’t wired anything up to the UI. Check the Debug logs to verify the Worker ran.
07-13 23:33:35.046 D/SimpleWorker( 7464): Started.
07-13 23:33:40.051 D/SimpleWorker( 7464): Completed.
Adding & running a ListenableWorker
Let’s add a Simple ListenableWorker.
using Android.Content;
using Android.Util;
using AndroidX.Concurrent.Futures;
using AndroidX.Work;
using Google.Common.Util.Concurrent;
using Java.Lang;
using System.Threading.Tasks;
namespace XamarinAndroidWorkerDemos.Workers
{
public class SimpleListenableWorker : ListenableWorker, CallbackToFutureAdapter.IResolver
{
public const string TAG = "SimpleListenableWorker";
public SimpleListenableWorker(Context context, WorkerParameters workerParams) :
base(context, workerParams)
{
}
public override IListenableFuture StartWork()
{
Log.Debug(TAG, "Started.");
return CallbackToFutureAdapter.GetFuture(this);
}
public Object AttachCompleter(CallbackToFutureAdapter.Completer p0)
{
Log.Debug(TAG, $"Executing.");
//Switch to background thread.
Task.Run(async () =>
{
//Perform a process here, simulated by a delay for 5 seconds.
await Task.Delay(5000);
Log.Debug(TAG, "Completed.");
//Set a Success Result on the completer and return it.
return p0.Set(Result.InvokeSuccess());
});
return TAG;
}
}
}
There’s a lot to digest here, so let’s go through it.
- The StartWork method is pretty simple, it just returns a ListenableFuture.
- Once the AttachCompleter method starts, we immediately switch off the Main thread to a background thread by using Task.Run.
- We simulate a 5 second delay.
- We set a Success Result on the completer and return it.
We can run the ListenableSimpleWorker from the MainActivity.
var simpleListenableWorkerRequest =
new OneTimeWorkRequest.Builder(typeof(SimpleListenableWorker))
.AddTag(SimpleListenableWorker.TAG)
.Build();
WorkManager.GetInstance(this).BeginUniqueWork(
SimpleListenableWorker.TAG, ExistingWorkPolicy.Keep, simpleListenableWorkerRequest)
.Enqueue();
Check the Debug logs to make sure it executed.
07-13 23:29:29.795 D/SimpleListenableWorker( 7464): Started.
07-13 23:29:29.807 D/SimpleListenableWorker( 7464): Executing.
07-13 23:29:35.077 D/SimpleListenableWorker( 7464): Completed.
Viewing the State of a Worker
There are a couple of changes we have to make to view the State of a Worker.
In order for an Activity to observe the status of a Worker, it has to inherit IObserver from AndroidX.Lifecycle.IObserver and implement the OnChanged method of IObserver.
However this event seems to only fire if the Activity inherits AppCompatActivity from AndroidX.AppCompat.App and not Android.Support.V7.App.
Change MainActivity to inherit from AndroidX.AppCompat.App. When you do this, you’ll also have to change certain elements, for example Android.Support.V7.Widget.Toolbar to AndroidX.AppCompat.Widget.Toolbar.
Observe the Workers using the WorkManager:
var workManager = WorkManager.GetInstance(this);
var simpleWorkerObserver = workManager.GetWorkInfosByTagLiveData(SimpleWorker.TAG);
simpleWorkerObserver.Observe(this, this);
var simpleListenableWorkerObserver = workManager.GetWorkInfosByTagLiveData(SimpleListenableWorker.TAG);
simpleListenableWorkerObserver.Observe(this, this);
In the OnChanged method, display some details on the UI.
var workInfos = p0.JavaCast<JavaList<WorkInfo>>();
StringBuilder textViewText = default;
RunOnUiThread(() =>
{
textViewText = new StringBuilder(_textView.Text);
});
foreach (var workInfo in workInfos)
{
//Ignore the default Xamarin Tag when getting the Tag.
var name = workInfo.Tags.First(t => !t.Contains("."));
textViewText.Append($"{System.Environment.NewLine}{name}:{workInfo.GetState()}");
}
RunOnUiThread(() =>
{
_textView.Text = textViewText.ToString();
});
When you run the app, you should see the UI update when you start a Worker.
Displaying the progress of a ListenableWorker
It’s pretty simple to report the progress of a ListenableWorker. A SetProgressAsync method is available which allows you pass progress information such as a percentage progress back to an Observer. Let’s update the ListenableWorker to loop 5 times with a delay of 1 second each.
var delaySeconds = 5;
var progress = 0;
var progressIncrement = 100 / delaySeconds;
var dataBuilder = new Data.Builder();
for (int i = 0; i < delaySeconds+1; i++)
{
await Task.Delay(1000);
progress += progressIncrement;
dataBuilder.PutInt("Progress", progress);
SetProgressAsync(dataBuilder.Build());
}
Back in MainActivity in OnChanged, let’s get the progress value.
foreach (var workInfo in workInfos)
{
//Ignore the default Xamarin Tag when getting the Tag.
var name = workInfo.Tags.First(t => !t.Contains("."));
var progress = workInfo.Progress?.GetInt("Progress", -1) ?? -1;
if (progress == -1)
{
textViewText.Append($"{System.Environment.NewLine}{name}:{workInfo.GetState()}");
}
else
{
textViewText.Append($"{System.Environment.NewLine}{name}:{workInfo.GetState()} {progress}%");
}
}
Run the Workers again. You should see progress for the ListenableWorker.
Conclusion
Hopefully this will be of some help to you getting started with Workers. I think WorkManager is a real game changer in Android and I’m looking forward to seeing what my team can do with it.