Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use await in Xamarin Android activity callbacks

The title may be a bit misleading, my question is more about why it works in this weird way.

So I have an activity with a layout that has a TextView and a ListView. I have a long running async method that prepares data to be displayed in the list. So the initial code is like this:

    protected async override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
        SetContentView(Resource.Layout.MyView);
        await SetupData();
    }

    private async Task SetupData(){
        Task.Run(async () => {
            var data = await new SlowDataLoader().LoadDataAsync();
            // For simplicity setting data on the adapter is omitted here
        });
    }

It works, in a sense that it executes without errors. However, the activity appears as a blank screen, and even the text view only renders after a certain delay. So it appears that task is actually not running asynchronously. Setting ConfigureAwait(false) on both "await" calls didn't help. Moving the SetupData() call into OnPostCreate, OnResume and OnPostResume has no effect. The only thing that made the TextView appear immediately and render the list later, when data arrived is this:

    protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
        SetContentView(Resource.Layout.MyView);
        new Handler().PostDelayed(async ()=>{
            await SetupData();
        }, 100);
    }

So the question is, why doesn't

await SetupData().ConfigureAwait(false); 

unblock the flow? Why do we have to force delay the start of the async operation to let UI finish rendering, even though (according to this http://www.wintellect.com/devcenter/paulballard/tasks-are-still-not-threads-and-async-is-not-parallel) SetupData is supposed to be able to run as a separate thread here ?

p.s. removing the code that sets data on the adapter doesn't affect this behavior - there is still a delay before the screen is rendered. So I'm not showing that code here.

like image 954
Dennis K Avatar asked Feb 02 '17 17:02

Dennis K


2 Answers

By awaiting within the UI Looper, you are blocking further code execution on the thread while your SetupData method runs.

Non-blocking Example:

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

        SetContentView(Resource.Layout.Main);
        Task.Run(() => SetupData());
        Console.WriteLine("UI Thread / Message Looper is not blocked");
    }

    void SetupData()
    {
        Task.Run(async () =>
        {
            Console.WriteLine($"Are we on the UI thread? {Looper.MainLooper.Thread == Looper.MyLooper()?.Thread}");
            // Simulate a long running task
            await Task.Delay(TimeSpan.FromSeconds(10));
            Console.WriteLine("Done fetching/calculating data");
            RunOnUiThread(() =>
            {
                // Update the data fetched/calculated on the UI thread;
                Console.WriteLine($"Are we on the UI thread? {Looper.MainLooper.Thread == Looper.MyLooper().Thread}");
            });
        }).Wait();
        Console.WriteLine("Done w/ SetupData");
    }

Output:

UI Thread / Message Looper is not blocked
Are we on the UI thread? False
Done fetching/calculating data
Are we on the UI thread? True
Done w/ SetupData
like image 121
SushiHangover Avatar answered Oct 18 '22 22:10

SushiHangover


To complement the answer from @SushiHangover, I'm adding my own answer to point out the actual bug and list possible solutions in addition to the one suggested by @SushiHangover.

Consider the Example at the very bottom of this page https://msdn.microsoft.com/en-us/library/hh156528.aspx

The real problem in the original code (and all other variants I tried) was that even though SetupData was declared as async method, it was actually running as synchronous. So when OnCreate was awaiting on a synchronous method, it was blocking (exactly what they demo in the Example above). This issue can be corrected via several ways. First, as SushiHangover suggested, do not await on this method, and since it's sync, call it as such (and may as well remove async keyword and return void from it).

Another approach, which may be more suitable in some situations, is to await on the Task that's created inside that method:

private async Task SetupData(){
    await Task.Run(async () => {
        var data = await new SlowDataLoader().LoadDataAsync();
        // For simplicity setting data on the adapter is omitted here
    });
}

Or, change this method to comply with async method requirements by returning the task:

private Task SetupData(){
    return Task.Run(async () => {
        var data = await new SlowDataLoader().LoadDataAsync();
        // For simplicity setting data on the adapter is omitted here
    });
}

Both these changes allow the await in OnCreate work as expected - OnCreate method exits, while data is still being loaded.

like image 26
Dennis K Avatar answered Oct 18 '22 21:10

Dennis K