Question Comment implémenter les callbacks Android en C # en utilisant async / waiting avec Xamarin ou Dot42?


Comment implémentez-vous les callbacks en C # en utilisant async / waiting avec Xamarin pour Android? Et comment cela se compare-t-il à la programmation Java standard pour Android?


12
2018-05-27 20:00


origine


Réponses:


Avec Xamarin pour Android version 4.7, au moment de la rédaction de ce document, nous pourrions utiliser les fonctionnalités de .NET 4.5 pour implémenter des méthodes "asynchrones" et les "attendre". Cela m'a toujours dérangé, si un rappel est nécessaire en Java, le flux logique du code dans une fonction est interrompu, vous devez continuer le code dans la fonction suivante lorsque le rappel revient. Considérez ce scénario:

Je souhaite collecter une liste de tous les moteurs TextToSpeech disponibles sur un appareil Android, puis demander à chacun d’eux quelle langue il a installée. La petite activité "TTS Setup" que j'ai écrite présente à l'utilisateur deux boîtes de sélection ("spinners"), une liste de toutes les langues prises en charge par tous les moteurs TTS de cet appareil. L'autre case ci-dessous répertorie toutes les voix disponibles pour la langue sélectionnée dans la première case, à nouveau parmi tous les moteurs TTS disponibles.

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

Idéalement, toute l'initialisation de cette activité devrait avoir lieu dans une fonction, par ex. dans onCreate (). Pas possible avec la programmation Java standard car:

Cela nécessite deux rappels "perturbateurs" - le premier pour initialiser le moteur TTS - il devient pleinement opérationnel uniquement lorsque onInit () est rappelé. Ensuite, lorsque nous avons un objet TTS initialisé, nous devons lui envoyer une intention "android.speech.tts.engine.CHECK_TTS_DATA", et l'attendre à nouveau dans notre rappel d'activité onActivityResult (). Une autre perturbation du flux logique. Si nous parcourons une liste de moteurs TTS disponibles, même le compteur de boucles pour cette itération ne peut pas être une variable locale dans une seule fonction, mais doit plutôt être un membre de classe privé. Assez désordonné à mon avis.

Ci-dessous, je vais essayer de définir le code Java nécessaire pour y parvenir.

Messy Java code pour collecter tous les moteurs TTS et les voix leur 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();
        }
    }
}

Notez que la ligne qui crée un nouvel objet TTS avec le rappel ttsInit doit être répétée 3 fois pour continuer à parcourir tous les moteurs disponibles si des exceptions ou d'autres erreurs surviennent. Peut-être que ce qui précède pourrait être écrit un peu mieux, par ex. Je pensais que je pouvais créer une classe interne pour garder le code de boucle localisé et que mon compteur de boucles ne soit pas au moins un membre de la classe principale, mais cela reste compliqué. Suggestion pour améliorer ce code Java.

Solution beaucoup plus propre: Xamarin C # avec des méthodes asynchrones

Tout d'abord, pour simplifier les choses, j'ai créé une classe de base pour mon activité qui fournit CreateTtsAsync () pour éviter DISRUPTION 1 dans le code Java ci-dessus et StartActivityForResultAsync () pour éviter les méthodes 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'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));
    }

}

Maintenant, je peux écrire tout le code en boucle dans les moteurs TTS et les interroger sur les langues et les voix disponibles au sein d'une même fonction, en évitant une boucle sur trois fonctions différentes:

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

Le projet Java et le projet C #, utilisant Visual Studio 2012 avec Xamarin pour le module complémentaire Android, sont désormais publiés sur GitHub:

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

Qu'est-ce que tu penses?

Apprendre à le faire avec Xamarin pour une version d'évaluation gratuite d'Android était amusant, mais cela vaut-il la licence de $ pour Xamarin, puis le poids supplémentaire de chaque APK que vous créez pour Google Play Store d'environ 5 Mo en temps d'exécution. ? Je souhaite que Google fournisse la machine virtuelle Mono en tant que composant système standard sur des droits égaux avec Java / Dalvik.

P.S. J'ai passé en revue le vote sur cet article, et je constate qu'il y a aussi des votes négatifs. Je suppose qu'ils doivent provenir de passionnés de Java! :) Encore une fois, les suggestions pour améliorer mon code Java sont également les bienvenues.

P.S. 2 - Avait un intéressant échange sur ce code avec un autre développeur sur Google+, m'a aidé à mieux comprendre ce qui se passe réellement en async / en attente.

Mise à jour 29/08/2013

Dot42 a également implémenté des mots clés "asynchrones / attendus" dans leur produit C # pour Android, et j'ai essayé de transférer ce projet de test. Ma première tentative a échoué avec un crash quelque part dans les bibliothèques Dot42, en attente (de manière asynchrone bien sûr :) pour un correctif de leur part, mais il y a un fait intéressant qu'ils ont observé et implémenté en ce qui concerne les appels "asynchrones" :

Par défaut, s'il y a une certaine activité "modification de la configuration" pendant que vous attendez le résultat d'une longue opération asynchrone dans un gestionnaire d'événement d'activité, par ex. changement d'orientation, l'activité est détruite et recréée par le système. Si, après un tel changement, vous retournez d'une opération "async" au milieu d'un code de gestionnaire d'événements, l'objet "this" de l'activité n'est plus valide et si vous avez stocké des objets pointant vers des contrôles dans cette activité, ils sont également invalide (ils pointent vers les anciens objets maintenant détruits).

J'ai rencontré ce problème dans mon code de production (en Java) et j'ai travaillé en configurant l'activité à notifier, et non pas détruite et recréée lors de tels événements. Dot42 est venu avec une autre alternative, très intéressante:

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

L'extension .configureAwait (this) (plus une ligne de code supplémentaire dans l'activité OnCreate () pour configurer les choses) garantit que votre objet "this" est toujours valide, pointe vers l'instance d'activité en cours, lorsque vous revenez wait, même si la configuration le changement se produit. Je pense qu'il est bon au moins d'être conscient de cette difficulté, lorsque vous commencez à utiliser Async / Attendre avec le code de l'interface utilisateur Android, consultez d'autres articles sur le blog Dot42: http://blog.dot42.com/2013/08/how-we-implemented-asyncawait.html?showComment=1377758029972#c6022797613553604525

Mise à jour sur le crash de Dot42

Le crash asynchrone / attendue que j'ai expérimenté est maintenant corrigé dans Dot42, et ça marche très bien. En fait, mieux que le code Xamarin en raison de la gestion intelligente de cet objet dans Dot42 entre les cycles de destruction / recréation d'activité. Tout mon code C # ci-dessus doit être mis à jour pour prendre en compte de tels cycles, et actuellement, ce n'est pas possible dans Xamarin, uniquement dans Dot42. Je mettrai à jour ce code à la demande d'autres membres de SO, pour l'instant, il semble que cet article n'attire pas beaucoup d'attention.


17
2018-05-20 14:01



J'utilise le modèle suivant pour convertir les rappels en asynchronisation:

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

Ceci est très flexible et peut être utilisé dans les activités, dans les objets de rappel ect.

Soyez prudent, en l'utilisant dans le mauvais sens, vous créerez des impasses. Le verrou empêche le changement de résultat après l'expiration du délai.


0