Async/Await y Task mejores practicas para Xamarin

Sobre el tema:

Contenido

La programación asincrónica puede ser la magia debajo del desarrollo de aplicaciones móviles que se ilumina a su manera. Puede usar métodos asincrónicos para tareas de larga ejecución, como llamar a las API, descargar datos o animar la interfaz de usuario dinámicamente.

El uso de métodos asincrónicos de una manera incorrecta puede provocar la inmovilización de la interfaz de usuario de la aplicación y los usuarios no pueden usar la aplicación hasta que se complete la tarea. Sí, esto es realmente malo para su experiencia de usuario (UX) y usted puede ser consciente de ello.

Usted puede encontrar una gran cantidad de gran información por ahí, pero aquí estamos centrando nuestra energía en las mejores prácticas.

⚠ Nota: Le aconsejo que busque más información si no está muy familiarizado con la programación asincrónica.

Una vez que hayas aprendido los conceptos básicos de async await y te sientas bastante cómodo con él, estás listo para ir. Creo que todos sabemos que a veces hacemos algunas malas prácticas porque no somos conscientes de cómo funcionan algunas cosas y, por supuesto, hay mucha documentación dispersa que puede ser un poco confusa al principio.

La idea de este artículo es ayudarle a averiguar las prácticas recomendadas con el fin de mejorar sus aplicaciones y habilidades, obtener un buen ciclo de desarrollo y, en general, tener una gran experiencia con Xamarin.

📘 Nota: Aquí voy a mostrarle sólo las mejores prácticas, no estoy profundizando en los detalles hablando de TPL (Biblioteca paralela de tareas), máquina de estado, y así sucesivamente. Pero usted tendrá todas las referencias disponibles por si acaso desea profundizar en algo.

Async/Await

Tal vez Async/Await es uno de esos temas que sientes que sabes mucho, pero luego no lo haces. La realidad es que hay muchas maneras diferentes de usar Async/Await, y eso es parte del problema. Es difícil entender lo que hace cada método diferente, cada campo diferente en las tareas, lo que hacen todas las diferentes palabras clave y cómo difieren porque a veces son muy similares y solo se crearon por necesidad y eficiencia.

Async/Await es básicamente una sintaxisis azucarada en esteroides para hacer llamar métodos asincrónicos más fácil y para hacer que su código asincrónico más fácilmente legible.

Comencemos con algunos ejemplos de prácticas recomendadas.

Invocar tareas

Invoquemos tareas. Imagine que tenemos una tarea típica, TaskService, y todo este servicio que está haciendo es ejecutar un retraso internamente. Esto podría ser muy familiar si usa HttpClient, por ejemplo.

TaskService es simplemente invocar una tarea, una vez que tenemos esa tarea, lo que devuelven los servicios es solo una cadena de tarea.

...
private void MyMethod()
{
     ...
     var taskResult = service.TaskService().Result;
     ...
}
...

Esta es una mala manera de empezar. Estamos invocando el método, pero luego estamos llamando a .Result al final de la misma.

Aquí .Result esperará el resultado de la tarea y cuando eso suceda es cuando obtenemos el resultado, si la tarea no se ha completado, simplemente espera en la fila. Si el resultado vuelve, entonces puede continuar la ejecución.

Esto es sincrónico. Nunca desea hacer eso en el UIThread.

¿Cómo esto puede afectar a mi aplicación? Cuando estemos en el UIThread, bloquearemos/congelaremos/bloquearemos la interfaz de usuario hasta que obtengamos el resultado.

🚨 Nota: No utilice .Result en el constructor o en animaciones, por ejemplo.

Muy bien. Veamos cómo solucionar esto.

...
private async void MyMethod()
{
     ...
     var taskResult = await service.TaskService();
     ...
}
...

Todo lo que tenemos que hacer es llamar a await en la tarea y hacer que nuestro método sea asincrónico para obtener el resultado. Pero al final del comportamiento, todavía podemos usar la aplicación, incluso podemos ejecutar el método varias veces si lo desea, a continuación, se completará la ejecución.

Esperando múltiples tareas

Una vez que hayas descubierto que puedes esperar tareas, lo siguiente que vemos es que la gente espera tareas por separado. Imagine que tiene como tres puntos de conexión de servicios REST a los que necesita llamar y desea llamarlos de forma asincrónica.

...
private async void MyMethod()
{
     ...
     var taskResult1 = await service.TaskService1();
     ...
     var taskResult2 = await service.TaskService2();
     ...
     var taskResult3 = await service.TaskService3();
     ...
}
...

Lo que hace este código es: esperar al primero, esperar al segundo, esperar al tercero, y ¿sabes lo que pasa? Estás esperando, esperando y esperando. Y no vas a llegar al final hasta que todo esté hecho.

Supongamos que cada tarea de servicio se completa en un segundo, tardará 3 segundos en completar la tarea. Tal vez pienses que esto no se ve tan mal porque estás esperando, así que no estás bloqueando tu interfaz de usuario, pero verás que la primera va, la segunda, la tercera, y así sucesivamente.

⚠ Nota: Cuando tiene varias tareas de larga ejecución, se puede sentir la mala experiencia del usuario.

Echemos un vistazo a la solución de este problema.

...
private async void MyMethod()
{
     ...
     var tasks = new List<Task>();
     
     tasks.Add(service.TaskService1());
     tasks.Add(service.TaskService2()):
     tasks.Add(service.TaskService3());
     ...
     await Task.WhenAll(tasks);
     ...
}
...

Lo que estamos haciendo aquí es mantener las tareas en una colección. En este caso, los estoy colocando en una List, y luego podemos llamar a «Task.WhenAll» en toda la colección.

WhenAll significa que la tarea solo se devolverá una vez que se realicen todas las tareas. Cuando se realiza una tarea, significa que se ha completado, o se canceló, o se ha producido un error a través de una excepción.

⚠ Nota: La tarea se puede completar por separado. Si algunos de ellos regresan temprano, está bien, sólo se completará una vez que llegue la última tarea.

Esto es hermoso, ¿verdad? Antes de que el método tardará 3 segundos en completar la tarea. Pero si los ejecutamos con Task.WhenAll, veremos que todos se dispararon, y luego todos se completaron juntos en un segundo.

Threading

Antes de ejecutar una tarea, cuando pulsamos un botón y ejecutamos un comando, en realidad se ejecuta en el subproceso principal. Una vez que esperamos la tarea, ¿sigue siendo el subproceso principal, es un subproceso diferente? Los desarrolladores no piensan en esto. Es como: «Bueno, vino del hilo principal, tal vez todavía está allí» o a veces ni siquiera piensan en el contexto porque espera, funciona, es simplemente hermoso, se ejecuta de forma asíncrona.

Pero adivina qué, tienes que ser consciente de dónde corre.

En el ejemplo siguiente, imagine que tenemos un subproceso de suspensión para simular operaciones de tareas consecutivas de larga ejecución. Echemos un vistazo a eso.

...
private async Task MyMethod()
{
     ...
     var taskResult = await service.LongTaskService();

     //Simulate CPU intensive operation here
     ...
}
...

Aquí cuando llamamos al servicio, se iniciará en e UI thread y esperará, hacer cosas en segundo plano, volver al hilo principal (o UIThread), donde los subprocesos están durmiendo. Aquí puede pensar que está en un hilo en segundo plano, pero no es así porque el await estaba en un hilo en segundo plano y luego continua su ejecución en el UI thread.

En este caso, tu aplicación se va a bloquear por la siguiente operación intensiva. Muy similar a .Result pero no lo usamos, nosotros estamos usando await. Esto es complicado de explicar.

Veamos cómo arreglar esto.

...
private async Task MyMethod()
{
     ...
     var taskResult = await service.LongTaskService().ConfigureAwait(false);

     //Simulate CPU intensive operation here
     ...
}
...

Aquí sólo tenemos que utilizar la palabra clave mágica «ConfigureAwait(false)«. De forma predeterminada, una tarea tendrá ConfigureAwait(true) que hace que la ejecución continúe en el UIThread.

Cuando decimos ConfigureAwait(false), estamos diciendo que permito que la ejecución continúe en un subproceso en segundo plano, pero esta garantía es que lo que suceda después, no va a estar en el subproceso principal.

Tenga en cuenta que rara vez necesita volver al contexto donde estaba antes. Cuando se utiliza Task.ConfigureAwait(false), el código ya no intenta reanudar donde estaba antes, en su lugar, si es posible, el código se completa en el subproceso que completó la tarea, evitando así un cambio de contexto. Esto aumenta ligeramente el rendimiento y puede ayudar a evitar interbloqueos también.

Esto es particularmente importante cuando el método se llama un gran número de veces, para tener una mejor capacidad de respuesta.

Tasks o Tareas

Returning tasks o Tareas de retorno

En el ejemplo siguiente, en el método AwaitStringTaskAsync solo devolvemos el resultado con return await en lugar de devolver la task directamente. Esto puede ser un método auxiliar que podría estar haciendo algunas cosas, pero hemos agregado las palabras clave async/await aquí porque pensamos que teníamos que hacerlo.

...
private async void MyMethod()
{
     ...
     await AwaitStringTaskAsync();
     ...
}

private async Task<string> AwaitStringTaskAsync()
{
     return await service.GetStringAsync();
}
...

La realidad es que realmente podemos hacer es simplemente devolver la tarea, y eso es realmente más rápido porque no tenemos que esperar (await) dos veces. Ahora, hay una espera en el medio que hace cambio de contexto y un montón de cosas por detrás. Por lo tanto, lo que realmente estamos haciendo es agregar una sobrecarga.

Echemos un vistazo a cómo solucionar esto.

...
private async void MyMethod()
{
     ...
     await AwaitStringTaskAsync();
     ...
}

private Task<string> AwaitStringTaskAsync()
{
     return service.GetStringAsync();
}
...

A veces los métodos no necesitan ser asincrónicos, pero pueden devolver una tarea (Task) y dejar que el otro lado la controle según sea necesario. Tenga en cuenta que si no tenemos un retorno de espera de resultado (return await) , pero devolvemos la tarea (Task) en su lugar, el retorno se produce de inmediato.

Si la última linea del código es un return await, puede considerar la posibilidad de refactorizar tu codigo para que el tipo de valor devuelto del método sea Task<TResult> en lugar de async Task. Con esto, estamos evitando la sobrecarga, lo que hace que el código sea más ligero.

💡Tip: La única vez que realmente queremos esperar (await) es cuando queremos hacer algo con el resultado de la tarea asincrónica en la continuación del método.

Evitar métodos «async void»

Los métodos asincrónicos que devuelven vacío o nada tienen un propósito específico: hacer posibles los controladores de eventos asincrónicos.

Cuando se produce una excepción fuera de un método async Task async Task<T>, esa excepción se captura y se coloca en el objeto Task.

Con los métodos async void, no hay ningún objeto Task, por lo que las excepciones producidas de un método async void se generarán directamente en el SynchronizationContext que estaba activo cuando se inició el método async void.

...
public async void AsyncVoidMethod()
{
    //Bad!
}

public async Task AsyncTaskMethod()
{
    //Good!
}
...

💡 Tip: considere la posibilidad de usar async Task en lugar de async void.

En el ejemplo siguiente, nunca se alcanzará el bloque catch dentro del método ThisWillNotCatchTheException(). Pero podemos arreglar esto simplemente reemplazando el async void con la async Task como se puede ver en el método ThisWillCatchTheException().

...
public async void AsyncVoidMethodThrowsException()
{
    throw new Exception("Hmmm, something went wrong! :(");
}

public void ThisWillNotCatchTheException()
{
    try
    {
        AsyncVoidMethodThrowsException();
    }
    catch(Exception ex)
    {
        //The below line will never be reached
        Debug.WriteLine(ex.Message);
    }
}
...
public async Task AsyncTaskMethodThrowsException()
{
    throw new Exception("Hmmm, something went wrong!");
}

public async Task ThisWillCatchTheException()
{
    try
    {
        await AsyncTaskMethodThrowsException();
    }
    catch (Exception ex)
    {
        //The below line will actually be reached
        Debug.WriteLine(ex.Message);
    }
}
...

⚠ Nota: Los métodos async void son difíciles de testear y escribir UnitTest debido al control de errores. Por lo tanto, si los usas considera trabajar con métodos asincrónicos que devuelven una tarea (Task).

Return Task dentro de bloques try/catch o using

Return Task puede provocar un comportamiento inesperado utilizado dentro de un bloque try/catch (una excepción iniciada por el método asincrónico nunca se detectará, por ejemplo) o dentro de un bloque using porque la tarea se devolverá de inmediato.

...
public Task<string> ReturnTaskExceptionNotCaught()
{
    try
    {
        return service.TaskService(); // Bad!
    }
    catch (Exception ex)
    {
        //The below line will never be reached
        Debug.WriteLine(ex.Message);
    }
}
...

En el primer ejemplo anterior, si se produce una excepción dentro de una tarea dentro de TaskService(), no se detectará por el método ReturnTaskExceptionNotCaught(), incluso si está dentro del bloque try/catch, pero se detectará en un método externo generado por el compilador que espera la tarea devuelta por ReturnTaskExceptionNotCaught().

No hay manera de cómo explicar esto sin entrar en muchos detalles. Así que te dejo este hilo por si quieres ver mas detalles.

...
public Task<string> ReturnTaskIssueWithUsing()
{
    using (var resource = new Resource())
    {
        //By the time the resource is actually referenced, may have been disposed already
        return resource.TaskResource(); //Bad!
    }
}
...

Por otro lado, el metodo ReturnTaskIssueWithUsing liberara el objeto Resource  tan pronto como el método TaskResource() se devuelva, que es probable que mucho antes de que se complete realmente. Esto significa que el método este probablemente bugeado (porque Resource se elimina demasiado pronto).

Vamos a ver cómo solucionar esto!

En este punto, es posible que sepa que try/catch cambia la forma en que se controlan las excepciones. Haciendo que nuestro método asincronos (async) puedan esperar (await) el resultado y detectar (catch) la excepción que se producirá.

💡 Tip: If you need to Si necesita envolver el código asincrónico en un bloque try/catch o using, utiliza return await en su lugar.

💡 Tip: Recuerde que la única razón por la que desea agregar async/await a su método es que desea manipular el resultado antes de devolverlo.

...
public async Task<string> ReturnTaskExceptionNotCaught()
{
    try
    {
        return await service.TaskService(); // Good!
    }
    catch (Exception ex)
    {
        //The below line will be reached
        Debug.WriteLine(ex.Message);
    }
}

public async Task<string> ReturnTaskIssueWithUsing()
{
    using (var resource = new Resource())
    {
        return await resource.TaskResource(); //Good!
    }
}
...

Igual que antes, haciendo que nuestro método sea sincrono (async) podemos esperar (await) el resultado e indicar el método que no puede eliminar el objeto Resource porque estamos esperando el resultado para continuar con el proceso.

Manejo de excepciones

Con tareas (Task), hablamos de .Result, hablamos de await, pero a veces quieres lanzar y olvidar (FireAndForget). A veces, solo queremos ejecutar una tarea. No nos importa si se completa o no. Sólo queremos que corra.

En el ejemplo siguiente, supongamos que tenemos una excepción en FireAndForgetTask, pero no lo sabemos. Este es el código típico que solo necesitamos ejecutar, pero agregamos un try/catch en caso de que algo se pase.

...
public ICommand MyCommand => new Command(() =>
{
     try
     {
     ...
     //Something that can throw an exception
     service.FireAndForgetTask();
     ...
     }
     catch (Exception ex)
     {
         Console.WriteLine($"Exception occurred: {ex.Message}");
     }
});
...

En este caso, lo que sucede es porque tenemos FireAndForgetTask, su ejecución continuará hasta que estemos fuera del «bloque catch». Si ocurre alguna excepción allí, quién sabe a dónde va. Por lo tanto, la captura del error va a depender de cómo se estructura tu aplicación.

Cuando el comando finaliza, a continuación, se produce la excepción. Incluso si la aplicación no se rompe, es posible que la funcionalidad que programaste no se ejecute. Esto es casi peor a que se rompa la aplicación porque es muy difícil detectar el error.

¡Vamos a arreglar esto!

...
public ICommand MyCommand => new Command(() =>
{
     ...
     //Something that can throw an exception
     service.FireAndForgetTask()
     .ContinueWith(continuationAction: (task) =>
     {
         Console.WriteLine($"Exception occurred: {task.Exception.Message}");
     }, continuationOptions: TaskContinuationOptions.OnlyOnFaulted);
     ...
});
...

Todo lo que hacemos aquí es usar «task.ContinueWith«. Lo que hace ContinueWith es, una vez que nuestra tarea se completa, independientemente, si la esperábamos o no, esta continuación sucederá y podemos pasar una Acción. En esta acción, tenemos la tarea (Task) como parámetro, por lo que podemos inspeccionar nuestra tarea.

También podemos establecer estas ContinuationOptions, donde puede especificar OnlyOnFaultedOnlyOnCanceled y mucho más para controlar esos escenarios.

💡 Tip: Con ContinueWith no puedes tener varios «ContinueWith» en una sola tarea. Si desea manejar varias opciones diferentes, simplemente manéjelas todas en el mismo «ContinueWith».

Volviendo al ejemplo, usando ContinueWith hacemos un buen control de excepciones, veremos el comando terminado, luego la prueba con errores y tenemos nuestra impresión de excepción.

Esto es muy bueno si estás registrando tus logs en App Center o algo parecido, ahora realmente obtienes esa excepción.

Cronometrar tareas

A veces queremos establecer un límite de tiempo para algunas tareas, ¿cómo lo hacemos? Algunas personas usan Task.WaitAll porque tiene un segundo parámetro que es un tiempo de espera, pero el problema con este método es que se ejecutan sincrónicamente igual que antes con .Result. Echemos un vistazo.

...
private void MyMethod()
{
     ...
     // 3 minutes task
     var longTaskService= service.LongTaskService();

     var tasks = new Task[] { longTaskService };
     var timeoutSeconds = 10;

     Task.WaitAll(tasks, timeout: TimeSpan.FromSeconds(timeoutSeconds));
     ...
}
...

En el ejemplo anterior tenemos una tarea que se ejecutará durante 3 minutos y solo queremos esperar 10 segundos por solicitud. Por lo tanto, Task.WaitAll está haciendo el trabajo por nosotros porque tiene un segundo parámetro que es un tiempo de espera (timeout). Toma en cuenta es este es un tiempo de espera en el que estas literalmente esperando. Por lo tanto, debe tener cuidado porque puedes bloquear la aplicación.

Echemos un vistazo a una alternativa.

/* View Model Context */
...
private async Task MyMethod()
{
     ...
     var timeoutSeconds = 10;
     var cancellationTokenSource = new CancellationTokenSource(delay: TimeSpan.FromSeconds(timeoutSeconds));
     
     // 3 minutes task
     await service.LongTaskService(cancellationTokenSource.Token);
     ...
}
...
...
/* Service Context */
...
public Task<string> LongTaskService(CancellationToken cancellationToken = default)
{
     var getStringTask = Task.Run(() =>
     {
        //Simulate work
        Task.Delay((int)TimeSpan.FromMinutes(3).TotalMilliseconds); 

        return "I took 3 min of your life :(";
     }, cancellationToken: cancellationToken);

     return getStringTask;
}
...

Una buena alternativa para esto es el uso de un CancellationTokenSource que toma un retraso donde puedes pasar el intervalo de tiempo que deseas. El «Token» está pasando como un parámetro opcional en el método LongTaskService donde se implementa el token.

Sólo lo estamos pasando dentro del «Task.Run» porque «CancellationToken» intentará cancelar la tarea. A continuación, dentro de la propia tarea, también puedes lanzar la excepción CancellationRequested.

Normalmente vemos esto en librerías de terceros que toman un TaskCancellationToken a través de un método, y luego utilizarlo para que tu manejes la cancelación. Esto es genial porque sólo estás estableciendo un tiempo de espera y eso es lo que se necesita en este caso.

💡 Tip: también puede cancelar tareas manualmente. Leer más aquí.

Lanzar y olvidar tareas de larga duración

A menudo, en el desarrollo de aplicaciones, desea que un proceso llame a otro subproceso y continúe el flujo de proceso, sin esperar una respuesta del subproceso llamado. Este patrón se denomina patrón «lanza y olvida» o «Fire and Forget«.

Task.Run() fue lanzado para facilitar la vida del desarrollador. Es un atajo. De hecho, Task.Run se implementa realmente en términos de la misma lógica utilizada para Task.Factory.StartNew, simplemente pasando algunos parámetros predeterminados.

El problema en el ejemplo siguiente es que estamos ejecutando una tarea larga, y cualquier cosa puede suceder y puede afectar al rendimiento de nuestra aplicación.

...
private void MyMethod()
{
     ...
     //Fire and forget long running task using Task.Run 
     Task.Run(() => FireAndForgetLongTask());
     ...
}
...

⚠ Nota: El código que se muestra es solo para fines de ejemplo. Las tareas de fuego real y olvido siempre deben tener un mecanismo de cancelación y control de excepciones!!

Una mejor manera de administrar esto es mediante el uso de Task.Factory.StartNew que nos da un control más avanzado sobre nuestras tareas.

...
private void MyMethod()
{
     ...
     //Fire and forget long running task using Task.Factory.StartNew(()=>{}, TaskCreationOptions.LongRunning)
     //Reference: https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcreationoptions?view=netframework-4.8
     Task.Factory.StartNew(() => FireAndForgetLongTask(), TaskCreationOptions.LongRunning);
     ...
}
...

En este caso, usamos TaskCreationOptions para obtener el control de cómo se comporta la tarea. Cuando establecemos este opción en LongRunning, estamos diciendo «Permito que esta tarea cree más subprocesos que el número disponible para que se complete».

TaskCompletionSource

TaskCompletionSource nos permite hacer que el código sincrónico se ejecute de forma asincrónica. Básicamente, es una buena manera de crear tareas manualmente con un control preciso a lo largo de su vida útil.

Aquí lo que necesita saber es que TaskCompletionSource<T> representa un resultado futuro y da la capacidad de establecer el estado final de la tarea subyacente manualmente llamando los métodos SetExceptionSetResult o SetCanceled.

Echemos un vistazo al ejemplo siguiente con una mala implementación que he visto recientemente.

Como puede ver, estamos llamando a la TaskService pero la tarea, antes de que se complete, producirá una excepción por método ThrowAnExeption, pero este método no se tiene en cuenta porque se olvida en segundo plano. Aunque se produce la excepción, no se puede ver. Entonces, ¿cómo manejamos eso?

...
/* VM Context */
...
private async Task MyMethod()
{
     ...
     try
     {
        var taskResult = await service.TaskService();
     }
     catch (Exception ex)
     {
         //This block will never be reached
     }
     ...
}
...
...
/* Service Context */
...
public Task<string> TaskService()
{
     var taskCompletionSource = new TaskCompletionSource<string>();
     var tcsTask = taskCompletionSource.Task;

     var internalTask = Task.Run(() =>
     {
         var taskResult = ThrowAnException(); //This doesn't take into account method can throw an exception so it appears to be "swallowed"
         taskCompletionSource.TrySetResult(taskResult);
     });

     return internalTask; //Bad! Example purpose.
}
...

Aquí tenemos un Task.Run donde establecemos el resultado. Esto es casi lo más importante con TaskCompletionSource porque si nunca establece el resultado, la tarea nunca se completa porque nunca la establecemos para producir una excepción, por lo que podría ejecutarse para siempre si no tiene errores.

Por otro lado, en el ejemplo anterior estamos devolviendo el internalTask, la tarea directamente. Esto significa que  TaskCompletionSource.Task es inútil en el código porque no la está utilizando.

Echemos un vistazo a cómo solucionar esto.

...
/* VM Context */
...
private async Task MyMethod()
{
     ...
     try
     {
         var taskResult = await service.TaskService();
     }
     catch (Exception ex)
     {
         //This block will be reached
     }     
     ...
}
...
...
/* Service Context */
...
public Task<string> TaskService()
{
     var taskCompletionSource = new TaskCompletionSource<string>();
     var tcsTask = taskCompletionSource.Task;

     var internalTask = Task.Run(() =>
     {
         try
         {        
              var taskResult = ThrowAnException();
              taskCompletionSource.TrySetResult(taskResult);
          }
          catch (Exception ex)
          {
              // Do something here...
              taskCompletionSource.TrySetException(ex);
          }
     });

     return tcsTask ; //Good!
}
...

Para solucionarlo, lo que hacemos es en nuestro método TaskService en el propio Task.Run, en lugar de simplemente establecer el resultado, agregamos un try/catch que establece «TrySetException» que controlan los casos en los que puede salir mal. De esta manera, estamos totalmente a salvo.

Xamarin.Forms

Actualizando propiedades de UI

Para este ejemplo, imagine que estamos en el código detrás (code behind) porque queremos actualizar «StatusLabel.Text» directamente. Estamos esperando con ConfigureAwait(false) porque queremos salir del hilo principal de interfaz de usuario.

🚨 Nota: Esto es sólo para fines de ejemplo. No use ConfigureAwait(false) y, a continuación, BeginInvokeOnMainThread en el mismo contexto. Y, por supuesto, usa tu VM para administrar sus servicios, comandos y enlaces de propiedades.

...
private async void MyButtonClicked(object sender, EventArgs e)
{
     ...
     //If there was no configure await, execution would continue on the UI thread
     var taskResult = await service.LongTaskService().ConfigureAwait(false);

     Device.BeginInvokeOnMainThread(() =>
     {
         StatusLabel.Text += "Updated StatusLabel from UI thread!";
     });
     ...
}
...

Si usamos este código sin BeginInvokeOnMainThread, ¿qué sucede después si actualizamos StatusLabel.Text directamente? obtenemos una excepción que dice«Sólo el hilo original que creó la jerarquía de vistas puede tocar sus vistas.».

¿Cómo arreglar esto? Todo lo que hacemos es usar  Device.BeginInvokeOnMainThread y luego poner nuestro código allí. Y eso es todo. Cualquier cosa dentro se ejecutará en el hilo principal.

💡 Tip: Asegúrese de estar en el hilo principal si desea actualizar la interfaz de usuario directamente.

Tarea asincrónica al iniciar tu app

El constructor de App.xaml.cs se ejecutará antes de que la aplicación se muestre en la pantalla al iniciar el Xamarin.Forms Application. Como sabes, los constructores actualmente no tienen la capacidad de esperar métodos asincrónicos.

Veamos cómo podemos manejar esto dependiendo de su situación exacta.

...
public App()
{
     ...
     Task.Run(async ()=> { await MyMethod(); });
     ...
}
...
//OR
...
protected override async void OnStart()
{
    // Handle when your app starts
}
...

Si no le importa el resultado de tu tarea asincrónica, solo desea ejecutarla, puede hacerlo en el constructor como se muestra en el primer ejemplo anterior. Lo que esto hace es insertar tu tarea en un hilo en segundo plano.

Sin embargo, se recomienda colocarlo realmente en el método OnStart . Agregue la palabra clave async allí. Dado que OnStart es solo un evento y no hay nada que espere a su retorno, el uso del async void es aceptable aquí.

💡 Tip: Puede aplicar el mismo concepto a un constructor de una Page o usar el método OnAppearing para empezar a cargar datos en la página cuando el usuario está allí. De la misma manera, seguir buenas prácticas con algunos Frameworks MVVM que ayudan con algunas abstracciones con el fin de hacerlo desde la ViewModel.

Plugins, extensiones y mucho más

Xamarin.Essentials: MainThread

La clase MainThread de Xamarin.Essentials permite a las aplicaciones ejecutar código en el subproceso principal de ejecución y determinar si un bloque determinado de código se está ejecutando actualmente en el hilo principal.

Para obtener más información, consulte el siguiente enlace.

AsyncAwaitBestPractices

Las extensiones para System.Threading.Tasks.Task que le ayudan a lanzar y olvidar de forma segura Task ValueTask y asegura que la Task volverá a generar un Exception.

También le ayuda a evitar pérdidas de memoria cuando los eventos no se cancelan la suscripción y le permite utilizar AsyncCommand para trabajar con una tarea asincrónica de forma segura.

⚠ Nota: Muchos de los Frameworks MVVM administran su propia implementación de ICommand, asegúrese de que no manejen tareas asincrónica con él antes de usar este.

Para obtener más información, consulte el siguiente enlace.

Sharpnado.TaskMonitor

TaskMonitor es una librería de contenedor de tareas que le ayuda a lidiar con tareas «lanzar y olvidar» mediante la implementación de las mejores prácticas async/await.

Ofrece:

  • Ejecución segura de todas sus tareas asincrónicas: hecho para escenarios async voidasync Task
  • Devoluciones de llamada para cualquier estado (cancelado, éxito, completado, error)
  • Controlador de errores predeterminado o personalizado
  • Registrador de estadísticas de tareas predeterminado o personalizado

Para obtener más información, consulte el siguiente enlace.

Más para leer

  • [Video] Best Practices – Async / Await | The Xamarin Show
  • [Blog Post] Long Story Short: Async/Await Best Practices in .NET
  • [Blog Post] Getting Started with Async / Await
  • [Blog Post] C# Developers: Stop Calling .Result
  • [Blog Post] C# Async fire and forget
  • [Blog Post] Task.Run vs Task.Factory.StartNew
  • [Docs] Async/Await – Best Practices in Asynchronous Programming
  • [Source Code] async-await Xamarin Scenarios

Espero que esto te pueda ser útil. Si conoces otras prácticas o plugins que puedas recomendar, puedes dejarlo en los comentarios.😉

Para obtener más contenido actualizado, sígueme en Instagram LinkedIn! ¡Gracias por leer!🚀

¿Qué opinas de este contenido?
 
Luis Matos

Luis Matos

I help professionals and companies to create value solutions. I am a Systems Engineer, blockchain executive, and international mobile application speaker. Founder of the Malla Consulting Agency and several international technology communities.
Suscribirte
Notificar de
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x

Buscar en el sitio