Как реализовать обратные вызовы Android на С# с помощью async/await с помощью Xamarin или Dot42?

Как вы реализуете обратные вызовы на С#, используя async/await с Xamarin для Android? И как это сравнивается со стандартным Java-программированием для Android?

Ответ 1

С Xamarin для Android версии 4.7, на момент написания этой статьи в общедоступной бета-версии, мы можем использовать возможности .NET 4.5 для реализации "асинхронных" методов и "ожидающих" вызовов к ним. Меня всегда беспокоило, что если какой-либо обратный вызов необходим в Java, логический поток кода в функции прерывается, вы должны продолжить код в следующей функции, когда возвращается обратный вызов. Рассмотрим этот сценарий:

Я хочу собрать список всех доступных движков TextToSpeech на устройстве Android, а затем спросить каждый из них, какие языки он установил. Небольшая активность "TTS Setup", которую я написал, представляет пользователю два блока выбора ( "прядильщики" ), в котором перечислены все языки, на которых поддерживаются все устройства TTS на этом устройстве. В другом окне ниже перечислены все голоса, доступные для языка, выбранного в первом поле, снова из всех доступных двигателей TTS.

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

В идеале вся инициализация этой активности должна выполняться в одной функции, например. в onCreate(). Невозможно со стандартным программированием на Java, потому что:

Для этого требуются два "разрушительных" обратных вызова - сначала для инициализации двигателя TTS - он становится полностью работоспособным только тогда, когда вызывается onInit(). Затем, когда у нас есть инициализированный объект TTS, нам нужно отправить ему намерение "android.speech.tts.engine.CHECK_TTS_DATA" и ждать его снова в нашем обратном вызове onActivityResult(). Еще одно нарушение логического потока. Если мы итерируем через список доступных движков TTS, даже счетчик циклов для этой итерации не может быть локальной переменной в одной функции, но вместо этого должен быть сделан частным членом класса. Довольно беспорядочно, по-моему.

Ниже я попытаюсь описать необходимый код Java для этого.

Бесполезный Java-код для сбора всех движков TTS и поддержки их голосов

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

Обратите внимание, что строка, которая создает новый объект TTS с обратным вызовом ttsInit, должна повторяться 3 раза, чтобы продолжить цикл через все доступные двигатели, если происходят какие-либо исключения или другие ошибки. Возможно, вышеупомянутое может быть написано немного лучше, например. Я думал, что я мог бы создать внутренний класс, чтобы локализовать циклический код и мой счетчик циклов, по крайней мере, не быть членом основного класса, но он все еще запутан. Приветствуется предложение по улучшению этого Java-кода.

Многое лучшее решение: Xamarin С# с асинхронными методами

Во-первых, для упрощения вещей я создал базовый класс для своей Activity, который предоставляет CreateTtsAsync(), чтобы избежать разрыва 1 в коде Java выше и StartActivityForResultAsync(), чтобы избежать методов DISRUPTION 2.

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

}

Теперь я могу написать весь цикл кода через движки TTS и запросить их для доступных языков и голосов в пределах одной функции, избегая цикла в трех разных функциях:

// 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 и проект С# с использованием Visual Studio 2012 с надстройкой Xamarin для Android теперь размещены на GitHub:

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

Как вы думаете?

Изучение того, как сделать это с бесплатной пробной версией Xamarin для Android, было весело, но стоит ли $$ для лицензии Xamarin, а затем дополнительный вес каждого APK, который вы создаете для Google Play Store, около 5 МБ в Mono runtimes, мы должны распространяться? Я желаю, чтобы Google предоставила виртуальную машину Mono в качестве стандартного системного компонента на равных правах с Java/Dalvik.

P.S. Пересмотрел голосование по этой статье, и я вижу, что он получает также несколько голосов. Угадайте, что они должны появляться у энтузиастов Java!:) Опять же, предложения о том, как улучшить код Java, также приветствуются.

P.S. 2 - Если бы интересный обмен на этот код с другим разработчиком в Google+ помог мне лучше понять, что на самом деле происходит с асинхронным/ожидающим.

Обновление 8/29/2013

Dot42 также реализовал ключевые слова async/await в своем продукте С# для Android, и я попытался портировать ему этот тестовый проект. Моя первая попытка потерпела неудачу с сбоем где-то в библиотеках Dot42, ожидая (асинхронно, конечно:)) для исправления, но есть интересный факт, который они наблюдали и реализовывали, когда дело доходит до асинхронных вызовов от обработчиков событий активности Android

По умолчанию, если есть какое-то действие "изменение конфигурации", когда вы ожидаете результата длинной операции async внутри обработчика события активности, например, изменение ориентации, деятельность разрушается и воссоздается системой. Если после такого изменения вы вернетесь из операции "async" в середину кода обработчика события, объект активности 'this' больше недействителен, и если вы сохранили некоторый объект, указывающий на элементы управления в этом действии, они также недействительными (они указывают на старые, теперь уничтоженные объекты).

Я столкнулся с этой проблемой в своем производственном коде (на Java) и работал над ней, настроив активность, чтобы ее уведомляли, а не уничтожали и воссоздавали на таких событиях. Dot42 пришел с другой альтернативой, довольно интересной:

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

Расширение .configureAwait(this) (плюс еще одна строка кода в действии OnCreate() для настройки вещей) гарантирует, что ваш объект 'this' все еще действителен, указывает на текущий экземпляр активности, когда вы возвращаетесь с ожиданием, даже если происходит изменение конфигурации. Я думаю, что хорошо, по крайней мере, осознавать эту трудность, когда вы начинаете использовать async/await с кодом Android UI, см. Больше записей об этом в блоге Dot42: < а3 >

Обновление при сбое Dot42

Асинхронный/ожидающий сбой, который я испытал, теперь зафиксирован в Dot42, и он отлично работает. На самом деле, лучше кода Xamarin из-за умной обработки объекта 'this' в Dot42 между циклами уничтожения активности/отдыха. Весь мой код С# выше должен быть обновлен, чтобы учесть такие циклы, и в настоящее время это невозможно в Xamarin, только в Dot42. Я буду обновлять этот код по требованию от других членов SO, поскольку теперь кажется, что эта статья не получает большого внимания.

Ответ 2

Я использую следующую модель для преобразования обратных вызовов в 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();
    }
}

Это очень гибкий и может использоваться в действиях, внутри ect обратных вызовов.

Будьте осторожны, использование этого неправильного пути приведет к созданию взаимоблокировок ect. Блокировка предотвращает изменение результата после истечения таймаута.