All apps need to perform tasks that may take longer than a few milliseconds. If a task blocks the UI thread for longer than a few seconds, Android will terminate it, crashing the application.
If we want to do work in the background, we use a new thread. To do this, we make use of the Task Parallel Library (TPL) and the async
/await
keywords:
public async Task DoWorkAsync() { await Task.Run(() => { // some long running task }); }
await
keyword:await DoWorkAsync();
doWork.Click += async (sender, args) => { await DoWorkAsync(); }
void
methods by simply inserting the async
keyword before the return type:protected override async void OnCreate(Bundle bundle) { base.OnCreate(bundle); await DoWorkAsync(); }
Task<>
: public async Task<bool> DoWorkAsync() { await Task.Run(() => { // some long running task }); return true; }
var success = await DoWorkAsync();
RunOnUiThread()
method:Activity.RunOnUiThread(() => { // UI interaction });
Post()
method:View.Post(() => { // UI interaction });
One of the most important areas to consider when creating the UI of an app is how it will perform on the device. No matter how great we design the UI, if it freezes or hangs, the Android OS may terminate the app or the user will uninstall it. This is because Android is continually updating the UI and handling events. If we were to perform a long-running task, the UI would have to wait for our task to complete.
The most common causes of a frozen UI are network or I/O operations. If we are opening a file, then we can do so on a background thread. Although this operation is very quick, it may not be quick enough for the app or user. By moving the task into the background, the file can be opened in often less than a second but with no chance of the UI blocking.
In the case of network operations, such as downloading an image thumbnail, moving the task into the background will allow the user to scroll through a list view at high speed. As the images are downloaded, they can be displayed without causing the list to freeze until the download is complete.
To prevent the UI freezing, we use threads or do the work in the background. There are several ways to do this but the easiest and simplest is to let the compiler do all the work for us. When we do this, we make use of the TPL and the async
and await
keywords. We mark methods as asynchronous using the async
keyword. Then, when we invoke the asynchronous method, we use the await
keyword.
When we use these keywords, the compiler will rewrite our code during the build process to actually execute the method on a new thread. This is a great way to keep our code clean as well as the app performing optimally.
Using the async
and await
keywords doesn't automatically create a new thread unless we specify what to execute on a new thread. We can await the Task.Run
method to execute a block of code in the background, or we can await another asynchronous method. When the awaited code is to be executed, a new thread is created and used. When the execution is finished, it is returned to the caller and back to the caller's thread.
Unlike the synchronous methods, if our method is to return a value, we have to use the generic Task<>
return type. This is because, although we are returning a value, the compiler is writing additional code that requires additional members from our method. If our asynchronous method does not return a value, we use the Task
return type.
So, instead of simply returning the value, such as bool
, we wrap it in the generic Task<bool> tag
. And, instead of void
, we have a Task
return type. When we use the await
keyword, the compiler rewrites the code to extract the returned value, which we then use.
There is a special case in which we can await tasks within a void
method. For events, this usually does not matter as the sender usually does not care what happens. For events that may require a handled notification, we must set the properties before starting the background work.
Another special case is when overriding a method that returns void
. As we cannot change the return type, we have to work around it by ensuring that we invoke the base method before awaiting any tasks. If we do not, then the method will return immediately without invoking the base method, which, in the case of Android lifecycle events, is not valid.
If we want to update the UI from within a block of code that is executing on another thread, we cannot do so from that thread. All UI operations must be performed on the UI thread. Android provides two means of doing this, using an activity or the view itself.
We can update the UI from another thread if we have access to the activity that holds the view, using the RunOnUiThread()
method defined in the activity itself. Alternatively, we can use the Post()
method or the PostDelayed()
method, defined on a view. These methods accept an Action
instance, or a block of code, which is then executed on the UI thread.
It is important to remember that the lifetime of a background thread is not related to the lifetime of the caller. If an activity starts a new thread and then the activity is destroyed, the thread will continue to execute in the background. This may cause undesired results or crash the app. Thus, when creating a thread, it should be terminated when the caller is no longer available. To avoid running into problems, we can use threads for shorter tasks and services for longer tasks.
We do have the option to use .NET threads or the thread pool along with the Android AsyncTask
instance. However, this involves new types and more complex code:
private class DownloadFilesTask : AsyncTask<Uri, int, long> { protected override long DoInBackground(params Uri[] uris) { // on BACKGROUND thread foreach (var uri in uris) { if (IsCancelled) break; } return uris.Length; } protected override void OnProgressUpdate(params int[] progress) { // on UI thread } protected override void OnPostExecute(long result) { // on UI thread } }
And then to execute this logic, we will do the following:
new DownloadFilesTask().Execute(url1, url2, url3);