How to implement Android callbacks in C# using async/await with Xamarin or Dot42?

后端 未结 2 841
野性不改
野性不改 2021-01-04 09:14

How do you implement callbacks in C# using async/await with Xamarin for Android? And how does this compare to standard Java programming for Android?

相关标签:
2条回答
  • 2021-01-04 09:33

    With Xamarin for Android version 4.7, at the time of this writing still in publicly available beta, we may use .NET 4.5 features to implement 'async' methods and 'await' calls to them. It always bothered me, that if any callback is needed in Java, the logical flow of code in a function is interrupted, you have to continue the code in the next function when the callback returns. Consider this scenario:

    I want to collect a list of all available TextToSpeech engines on an Android device, and then ask each of them which languages it has installed. The little “TTS Setup” activity that I wrote, presents to the user two selection boxes (“spinners”), one listing all the languages that all the TTS engines on this device support. The other box below lists all the voices available for the language selected in the first box, again from all available TTS engines.

    TtsSetup screen capture, first spinner lists all TTS languages, second all voices After choosing English and clicking the voices spinner

    Ideally all the initialization of this activity should happen in one function, e.g. in onCreate(). Not possible with standard Java programming because:

    This requires two “disruptive” callbacks – first to initialize TTS engine – it becomes fully operational only when the onInit() is called back. Then, when we have an initialized TTS object, we need to send it an “android.speech.tts.engine.CHECK_TTS_DATA” intent, and await it result again in our activity callback onActivityResult(). Another disruption of logic flow. If we are iterating through a list of available TTS engines, even the loop counter for this iteration cannot be a local variable in a single function, but must be made a private class member instead. Pretty messy in my opinion.

    Below I’ll try to outline the necessary Java code to achieve this.

    Messy Java code to collect all TTS engines and the voices their support

    public class VoiceSelector extends Activity {
    private TextToSpeech myTts;
    private int myEngineIndex; // loop counter when initializing TTS engines
    
    // Called from onCreate to colled all languages and voices from all TTS engines, initialize the spinners
    private void getEnginesAndLangs() {
        myTts = new TextToSpeech(AndyUtil.getAppContext(), null);
        List<EngineInfo> engines;
        engines = myTts.getEngines(); // at least we can get the list of engines without initializing myTts object…
        try { myTts.shutdown(); } catch (Exception e) {};
        myTts = null;
        myEngineIndex = 0; // Initialize the loop iterating through all TTS engines
        if (engines.size() > 0) {
            for (EngineInfo ei : engines)
                allEngines.add(new EngLang(ei));
            myTts = new TextToSpeech(AndyUtil.getAppContext(), ttsInit, allEngines.get(myEngineIndex).name());
            // DISRUPTION 1: we can’t continue here, must wait until  ttsInit callback returns, see below
        }
    }
    
    private TextToSpeech.OnInitListener ttsInit = new TextToSpeech.OnInitListener() {
    @Override
    public void onInit(int status) {
        if (myEngineIndex < allEngines.size()) {
            if (status == TextToSpeech.SUCCESS) {
                // Ask a TTS engine which voices it currently has installed
                EngLang el = allEngines.get(myEngineIndex);
                Intent in = new Intent(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA);
                in = in.setPackage(el.ei.name); // set engine package name
                try {
                    startActivityForResult(in, LANG_REQUEST); // goes to onActivityResult()
                    // DISRUPTION 2: we can’t continue here, must wait for onActivityResult()…
    
                } catch (Exception e) {   // ActivityNotFoundException, also got SecurityException from com.turboled
                    if (myTts != null) try {
                        myTts.shutdown();
                    } catch (Exception ee) {}
                    if (++myEngineIndex < allEngines.size()) {
                        // If our loop was not finished and exception happened with one engine,
                        // we need this call here to continue looping…
                        myTts = new TextToSpeech(AndyUtil.getAppContext(), ttsInit, allEngines.get(myEngineIndex).name());
                    } else {
                        completeSetup();
                    }
                }
            }
        } else
            completeSetup();
    }
    
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == LANG_REQUEST) {
            // We return here after sending ACTION_CHECK_TTS_DATA intent to a TTS engine
            // Get a list of voices supported by the given TTS engine
            if (data != null) {
                ArrayList<String> voices = data.getStringArrayListExtra(TextToSpeech.Engine.EXTRA_AVAILABLE_VOICES);
                // … do something with this list to save it for later use
            }
            if (myTts != null) try {
                myTts.shutdown();
            } catch (Exception e) {}
            if (++myEngineIndex < allEngines.size()) {
                // and now, continue looping through engines list…
                myTts = new TextToSpeech(AndyUtil.getAppContext(), ttsInit, allEngines.get(myEngineIndex).name());
            } else {
                completeSetup();
            }
        }
    }
    

    Note that the line that creates a new TTS object with ttsInit callback, has to be repeated 3 times in order to continue looping through all the available engines if any exceptions or other errors happen. Maybe the above could be written a little bit better, e.g. I thought that I could create an internal class to keep the looping code localized and my loop counter to at least not be a member of the main class, but it’s still messy. Suggestion for improvements of this Java code welcome.

    Much cleaner solution: Xamarin C# with async methods

    First, to simplify things I created a base class for my Activity that provides CreateTtsAsync() to avoid DISRUPTION 1 in the Java code above, and StartActivityForResultAsync() to avoid DISRUPTION 2 methods.

    // Base class for an activity to create an initialized TextToSpeech
    // object asynchronously, and starting intents for result asynchronously,
    // awaiting their result. Could be used for other purposes too, remove TTS
    // stuff if you only need StartActivityForResultAsync(), or add other
    // async operations in a similar manner.
    public class TtsAsyncActivity : Activity, TextToSpeech.IOnInitListener
    {
        protected const String TAG = "TtsSetup";
        private int _requestWanted = 0;
        private TaskCompletionSource<Java.Lang.Object> _tcs;
    
        // Creates TTS object and waits until it's initialized. Returns initialized object,
        // or null if error.
        protected async Task<TextToSpeech> CreateTtsAsync(Context context, String engName)
        {
            _tcs = new TaskCompletionSource<Java.Lang.Object>();
            var tts = new TextToSpeech(context, this, engName);
            if ((int)await _tcs.Task != (int)OperationResult.Success)
            {
                Log.Debug(TAG, "Engine: " + engName + " failed to initialize.");
                tts = null;
            }
            _tcs = null;
            return tts;
        }
    
        // Starts activity for results and waits for this result. Calling function may
        // inspect _lastData private member to get this result, or null if any error.
        // For sure, it could be written better to avoid class-wide _lastData member...
        protected async Task<Intent> StartActivityForResultAsync(Intent intent, int requestCode)
        {
            Intent data = null;
            try
            {
                _tcs = new TaskCompletionSource<Java.Lang.Object>();
                _requestWanted = requestCode;
                StartActivityForResult(intent, requestCode);
                // possible exceptions: ActivityNotFoundException, also got SecurityException from com.turboled
                data = (Intent) await _tcs.Task;
            }
            catch (Exception e)
            {
                Log.Debug(TAG, "StartActivityForResult() exception: " + e);
            }
            _tcs = null;
            return data;
        }
    
        protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
        {
            base.OnActivityResult(requestCode, resultCode, data);
            if (requestCode == _requestWanted)
            {
                _tcs.SetResult(data);
            }
        }
    
        void TextToSpeech.IOnInitListener.OnInit(OperationResult status)
        {
            Log.Debug(TAG, "OnInit() status = " + status);
            _tcs.SetResult(new Java.Lang.Integer((int)status));
        }
    
    }
    

    Now I can write the entire code looping through the TTS engines and querying them for available languages and voices within one function, avoiding a loop run throughout three different functions:

    // Method of public class TestVoiceAsync : TtsAsyncActivity
    private async void GetEnginesAndLangsAsync()
    {
        _tts = new TextToSpeech(this, null);
        IList<TextToSpeech.EngineInfo> engines = _tts.Engines;
        try
        {
            _tts.Shutdown();
        }
        catch { /* don't care */ }
    
        foreach (TextToSpeech.EngineInfo ei in engines)
        {
            Log.Debug(TAG, "Trying to create TTS Engine: " + ei.Name);
            _tts = await CreateTtsAsync(this, ei.Name);
            // DISRUPTION 1 from Java code eliminated, we simply await TTS engine initialization here.
            if (_tts != null)
            {
                var el = new EngLang(ei);
                _allEngines.Add(el);
                Log.Debug(TAG, "Engine: " + ei.Name + " initialized correctly.");
                var intent = new Intent(TextToSpeech.Engine.ActionCheckTtsData);
                intent = intent.SetPackage(el.Ei.Name);
                Intent data = await StartActivityForResultAsync(intent, LANG_REQUEST);
                // DISTRUPTION 2 from Java code eliminated, we simply await until the result returns.
                try
                {
                    // don't care if lastData or voices comes out null, just catch exception and continue
                    IList<String> voices = data.GetStringArrayListExtra(TextToSpeech.Engine.ExtraAvailableVoices);
                    Log.Debug(TAG, "Listing voices for " + el.Name() + " (" + el.Label() + "):");
                    foreach (String s in voices)
                    {
                        el.AddVoice(s);
                        Log.Debug(TAG, "- " + s);
                    }
                }
                catch (Exception e)
                {
                    Log.Debug(TAG, "Engine " + el.Name() + " listing voices exception: " + e);
                }
                try
                {
                    _tts.Shutdown();
                }
                catch { /* don't care */ }
                _tts = null;
            }
        }
        // At this point we have all the data needed to initialize our language
        // and voice selector spinners, can complete the activity setup.
        ...
    }
    

    The Java project, and the C# project, using Visual Studio 2012 with Xamarin for Android add-on, are now posted on GitHub:

    https://github.com/gregko/TtsSetup_C_sharp
    https://github.com/gregko/TtsSetup_Java

    What do you think?

    Learning how to do this with Xamarin for Android free trial was fun, but is it worth the $$ for Xamarin license, and then the extra weight of each APK you create for Google Play Store of about 5 MB in Mono runtimes we have to distribute? I wish Google provided Mono virtual machine as standard system component on equal rights with Java/Dalvik.

    P.S. Reviewed the voting on this article, and I see that it gets also some down-votes. Guess they must be coming from Java enthusiasts! :) Again, suggestions on how to improve my Java code are also welcome.

    P.S. 2 - Had an interesting exchange on this code with another developer on Google+, helped me to understand better what actually happens with async/await.

    Update 8/29/2013

    Dot42 also implemented 'async/await' keywords in their C# product for Android, and I tried porting to it this test project. My first attempt failed with a crash somewhere in Dot42 libraries, awaiting (asynchronously, of course :) ) for a fix from them, but there is an interesting fact they observed and implemented when it comes to 'async' calls from Android activity event handlers:

    By default, if there is some activity "configuration change" while you're awaiting a result of a long async operation inside an activity event handler, such as e.g. orientation change, the activity is destroyed and re-created by the system. If after such change you return from an 'async' operation to the middle of an event handler code, the 'this' object of the activity is no longer valid, and if you stored some object pointing to controls within this activity, they are also invalid (they point to the old, now destroyed objects).

    I hit this problem in my production code (in Java) and worked-around by configuring the activity to be notified, and not destroyed and recreated on such events. Dot42 came with another alternative, quite interesting:

    var data = await webClient
                 .DownloadDataTaskAsync(myImageUrl)
                 .ConfigureAwait(this);
    

    The .configureAwait(this) extension (plus one more code line in activity OnCreate() to setup things) ensures that your 'this' object is still valid, points to the current instance of activity, when you return from await, even if configuration change occurs. I think it's good to at least be aware of this difficulty, when you start using async/await with Android UI code, see more writeup on this at Dot42 blog: http://blog.dot42.com/2013/08/how-we-implemented-asyncawait.html?showComment=1377758029972#c6022797613553604525

    Update on Dot42 crash

    The async/await crash I experienced is now fixed in Dot42, and it works great. Actually, better than Xamarin code due to the smart handling of 'this' object in Dot42 between the activity destruction/recreation cycles. All of my C# code above should be updated to take into account such cycles, and currently it's not possible in Xamarin, only in Dot42. I'll update that code on demand from other SO members, for now it seems that this article does not get much attention.

    0 讨论(0)
  • 2021-01-04 09:54

    I use the following model to convert call backs to async:

    SemaphoreSlim ss = new SemaphoreSlim(0);
    int result = -1;
    
    public async Task Method() {
        MethodWhichResultsInCallBack()
        await ss.WaitAsync(10000);    // Timeout prevents deadlock on failed cb
        lock(ss) {
             // do something with result
        }
    }
    
    public void CallBack(int _result) {
        lock(ss) {
            result = _result;
            ss.Release();
        }
    }
    

    This is very flexible and can be used in Activities, inside callback objects ect.

    Be careful, using this the wrong way will create deadlocks ect. The lock prevents result changing after if the timeout runs out.

    0 讨论(0)
提交回复
热议问题