如何使用异步实现在C#中的Andr​​oid回调/等待与Xamarin或Dot42? [英] How to implement Android callbacks in C# using async/await with Xamarin or Dot42?

查看:734
本文介绍了如何使用异步实现在C#中的Andr​​oid回调/等待与Xamarin或Dot42?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

如何实现异步使用C#中的回调/等待与Xamarin为Android?这又如何比较标准的Java编程为Android?

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?

推荐答案

通过Xamarin的Andr​​oid版本4.7,在写这篇文章的时候还在公开发布测试版,我们可以使用.NET 4.5的功能来实现异步的方法和'等待'打给他们的电话。它总是困扰着我,说如果有回调需要在Java中的code函数中的逻辑流中断,你必须继续code回调返回时的一个功能。考虑这种情况:

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:

我想收集在Android设备上的所有可用TextToSpeech引擎的列表,然后问他们每个人已安装的语言。小TTS设置的活动,我写了,presents给用户两个选择框(微调),一个列出所有的语言,所有的TTS引擎对这个设备的支持。下面的其他框列出了可从所有可用TTS引擎在第一个框中选中,再次语言的声音。

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.

在理想情况下本次活动的所有初始化应该发生在一个函数,如中的onCreate()。不可能与标准的Java编程,因为:

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

这需要两个颠覆性回调 - 首先初始化TTS引擎 - 它充分运作,只有当的OnInit()被调用了。然后,当我们有一个初始化的TTS对象,我们需要把它发送一个android.speech.tts.engine.CHECK_TTS_DATA的意图,并等待它在我们的活动回调onActivityResult再次导致()。逻辑流程的另一个中断。如果我们通过提供TTS引擎的列表迭代,即使本次迭代循环计数器不能在一个函数中的局部变量,但必须由一个私有的类成员代替。 pretty的凌乱在我看来。

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.

下面我会试着勾勒必要的Java code来实现这一点。

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

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();
        }
    }
}

需要注意的是,与ttsInit回调创建一个新的TTS对象行,将重复3次,以继续通过所有可用的发动机循环如有异常或其他错误发生。也许上面可以写一个更好一点,如我以为我可以创建一个内部类,以保持循环code本地化和我的循环计数器,至少不是主要类的成员,但它仍然是混乱的。建议对于此Java code欢迎的改进。

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.

首先,把事情简单化我创造了我的活动,提供CreateTtsAsync(),以避免上述中断1在Java code的基类,和StartActivityForResultAsync(),以避免中断2种方法。

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));
    }

}

现在我可以写整个code通过TTS引擎循环,并询问他们可用的语言,一个函数中的声音,避免在整个三个不同功能的循环运行:

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.
    ...
}

Java项目和C#项目,使用与Xamarin的Visual Studio 2012为Android插件,现在都公布在GitHub上:

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

学习如何做到这一点与Xamarin为Android免费试用很有趣,但它是值得的$$为Xamarin许可证,然后在每个APK你们创造谷歌播放约5 MB存储在单声道运行时将我们额外的重量要分发?我希望谷歌提供单声道虚拟机作为标准的系统组件上使用Java / Dalvik的平等权利。

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。来自这篇文章的投票,而我看到,它也得到了一些向下票。猜猜它们必须从Java爱好者的到来! :)此外,也欢迎。如何提高我的Java code建议

P.S。 2 - 就这个code与Google+上的的另一个开发了一个有趣的交流,帮我更好地了解实际发生的情况与异步/等待。

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.

Dot42还实施了异步/等待的关键字在他们的C#的产品为Android,我试图移植到它这个测试项目。我第一次尝试失败,在Dot42库中的某个地方崩溃,从他们的修复等待(当然:)的异步),但他们观察到一个有趣的事实,并实现当涉及到异步从Android的活动事件处理程序调用

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:

在默认情况下,如果有一些活动配置更改你在等待的同时活动的事件处理程序内长的异步操作的结果,如如取向变化,活性被破坏,并通过系统重新创建。如果这样的改变之后,你从异步操作返回的事件处理程序code中间,活动的这个对象不再有效,如果你存储指向控制某个对象本次活动中,他们也无效(它们指向旧的,现在损坏的对象)。

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).

我在生产code(Java)中遇到这个问题,已经解决各地通过配置被通知的活动,而不是破坏并重新创建这样的事件。 Dot42带着另一种选择,挺有意思的:

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);

在.configureAwait(本)扩展(加上活动多了一个code线的OnCreate()设置的事情)可确保您的'这个'对象仍然是有效的,指向活动的当前实例,当您返回从计谋,即使配置发生变化。我觉得这是很好的,至少要知道这个困难,当你开始使用异步/等待与Android UI code,看到这个在Dot42博客更多新手必看:<一href="http://blog.dot42.com/2013/08/how-we-implemented-asyncawait.html?showComment=1377758029972#c6022797613553604525">http://blog.dot42.com/2013/08/how-we-implemented-asyncawait.html?showComment=1377758029972#c6022797613553604525

异步/计谋崩溃,我经历是现在固定在Dot42,和它的伟大工程。其实,比Xamarin code,由于智能处理这个对象的Dot42活动破坏/休闲周期之间更好。我所有的C#code以上,应更新,以考虑到这样的周期,目前这是不可能的Xamarin,仅在Dot42。我会更新由其他组织成员的需求是code,就目前看来,这篇文章并没有得到太多的关注。

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.

这篇关于如何使用异步实现在C#中的Andr​​oid回调/等待与Xamarin或Dot42?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆