async/await disaster

xamarin.dev1xamarin.dev1 USMember ✭✭
edited January 2016 in Cross Platform with Xamarin

Let's say I have to build a very simple app:

  • Screen has button and label
  • If user presses button it should become disabled and triggers execution of async task - issuing http request
  • While request is running label text value should say: "running"
  • If request is completed successfully label text value should say: "done"
  • If request is failed label text value should say: "failed"
  • While request is running button should be disabled (to avoid multiple requests in parallel)

Very simple:)
Let's say we have model class that allows us to make http request using C# async/await pattern (very nice and powerful thing that allows you to forget about invoke on main/ui thread), we went through xamarin async overview: https://developer.xamarin.com/guides/cross-platform/advanced/async_support_overview/ and ready to write the code:

class Model
{
    private Model() { }

    static Model()
    {
        Instance = new Model();
    }

    public static Model Instance { get; private set; }

    public async Task DoRequest()
    {
    ...
    }
 }


public class MainActivity : Activity
{
    private Button _button;
    private TextView _textview;

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

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

        _button = FindViewById<Button>(Resource.Id.Button);
        _textview = FindViewById<TextView>(Resource.Id.TextView);

        _button.Click += async delegate
        {
            //disable button
            _button.Enabled = false;
            //notify user that task is running
            _textview.Text = "running";

            try
            {
                //do some work using static Model instance
                await Model.Instance.DoRequest();
                _textview.Text = "done";
            }
            catch
            {
        //error occured
                _textview.Text = "failed";
            }
            finally
            {
                //enable button if request completed
                _button.Enabled = true;
            }
        };
    }
}

It works really good, but... try to rotate your device or put app into background/foreground (do any action which will force Android to recreate activity/fragment) while request is still running and you will see the problem:

the code after await will run in context of "destroyed" activity and will have no affect: _button, _textView properties are pointing to nothing as whole activity(view) has been recreated, so we run just junk code block with obsolete objects.

In our sample we just run junk code block that doesn't crash the app, but if for example you will try to execute fragment transaction instead it will.

Looking through native android examples will give us solution to solve the problem: AsyncTask (async/await) and Observers (Events).
Model event-based class:

class Model
{
    private Model() { }

    static Model()
    {
        Instance = new Model();
    }

    public static Model Instance { get; private set; }

    private async Task DoRequest()
    {
    //trigger events here
    }

    public void DoRequestAsync()
    {
        var exectuion = DoRequest();
    }

    public State RequestState;
    public event EventHandler RequestStarted;
    public event EventHandler RequestDone;
    public event EventHandler RequestFailed;
}

enum State
{
    Undefined,
    Runing,
    Done,
    Failed
}

Updated activity that uses event-base model (subscribe on create, unsubscribe on destroy):

public class MainActivity : Activity
{
    private Button _button;
    private TextView _textview;

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

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

        _button = FindViewById<Button>(Resource.Id.Button);
        _textview = FindViewById<TextView>(Resource.Id.TextView);

        Bind();

        Model.Instance.RequestStarted += RequestStateChanged;
        Model.Instance.RequestDone += RequestStateChanged;
        Model.Instance.RequestFailed += RequestStateChanged;

        _button.Click += delegate
        {
            //do some work using static Model instance
            Model.Instance.DoRequestAsync();
        };
    }

    protected override void OnDestroy()
    {
        base.OnDestroy();

        Model.Instance.RequestStarted -= RequestStateChanged;
        Model.Instance.RequestDone -= RequestStateChanged;
        Model.Instance.RequestFailed -= RequestStateChanged;

    }

    private void Bind()
    {
        _button.Enabled = Model.Instance.RequestState == State.Runing;
        switch (Model.Instance.RequestState)
        {
            case State.Runing:
                _textview.Text = "running";
                break;
            case State.Done:
                _textview.Text = "done";
                break;
            case State.Failed:
                _textview.Text = "failed";
                break;
        }
    }

    private void RequestStateChanged(object sender, EventArgs e)
    {
        Bind();
    }
}

That does work. But look how complex it becomes. Instead of single async method we have to create 3 events, one extra property for keep state of request.

Imagine that you have more complex ui/model with:

  • tons of public async method implemented
  • public non-void async methods (Task, that makes things event more complex:))

That is situation I have right now. We have nice working iOS app based on async/await models. It 's working like a charm on iOS which doesn't recreate ui all the time. Now we porting our app to android and reusing our models as part xamarin cross-platform approach and it becomes nightmare.

So my questions are:
- How do you deal with async/await on Android?
- If you don't use async/await what to use instead taking in mind that models should be shared between android/ios. Any frameworks, approaches, patterns are appreciated.

My thoughts as conclusion:

  • never make public async methods in Model (ViewModel) that are assumed to be called in context of UI (activity/fragment)
  • remove that "nice" async/await examples from Xamarin documentation, if you follow them you get into trouble:)

Posts

  • TomOpgenorthTomOpgenorth CAXamarin Team Xamurai

    The problem isn't await/async, in this case it's the way Android handles orientation changes. There are a couple of things you can do to hand this specific scenario:

    1. Use a headless fragment with RetainInstance set to true. If you perform the download in there, the fragment will stay in memory during the configuration changes.
    2. Use an Intent Service . You can fire off an intent, and let the IntentService do the work in the background. When then IntentService is done, it notifies the Activity via a BroadcastReceiver.

    Which of those two approaches to pick from depends on the nature of the web request if the web request is "long running" or not.

  • xamarin.dev1xamarin.dev1 USMember ✭✭

    @TomOpgenorth
    At my sample I have static Model.Instance which "holds" async request (so I don't need reataininstance fragment or service, I do already have static object), the problem is with async/await, the code "after" await is executed in context of obsolete, destroyed "activity", so as I see async/await approach is not working well with Android.

    Solution with BroadcastRecevier is same "event/callback" based solution I mentioned in the post.

    As what I see for now async/await is not usable within activity/fragment that are recreated by android, you have to use "event/callback" based interaction between model and UI.

    And question here what is best practice/replacement for async/await in cross-platfrom xamarin project where you do reuse your business logic in models between ios and android UI.

  • TomOpgenorthTomOpgenorth CAXamarin Team Xamurai

    My advice for Android (because this is the Xamarin Android forum) still stands: Use a retained fragment or an intent service. You can make them wrappers for the work your Model is doing, and the fragment (or service) communicates the results to the activity (regardless of what instance of the class is running at the time).

    It's all a matter of where you do the work. Because activities are very temporary, it can get complicated to have them perform "long running" tasks (as you're discovering). All this said, if you have specific Xamarin samples/docs which get you into trouble, I'd love to hear about them so that we can update/clarify them.

    Async/await works fine, but it has to obey/conform to the underlying platform.

    I politely suggest that a question on cross-platform advice would be better suited for the [Xamarin Cross-Platform forum)[https://forums.xamarin.com/categories/cross-platform].

  • xamarin.dev1xamarin.dev1 USMember ✭✭

    Tom, Is it technical way to move this thread into cross-platform forum, or I have to "duplicate" it?
    Thanks.

  • TomOpgenorthTomOpgenorth CAXamarin Team Xamurai

    Moving this thread in hopes to stimulate the conversation for @xamarin.dev1. The question is:

    What is best practice/replacement for async/await in cross-platfrom xamarin project where you do reuse your business logic in models between ios and android UI.

  • xamarin.dev1xamarin.dev1 USMember ✭✭

    Up :)

Sign In or Register to comment.