Dart Isolate y Flutter compute
No voy a hacerte un curso sobre el async/await y sobre como Flutter gestiona las llamadas asíncronas, ya que si has encontrado este post es porque sabes como funciona esto y solo quieres saber más sobre este tema y sobre como añadir un “thread” aparte del mainThread de Flutter para quitar estos “Jank” de tu app.
El Event Loop
Por si a caso, te voy a comentar un poco sobre el Event Loop de Flutter, ya que es un concepto importante que tienes que entender para saber si es necesario usar un Isolate.
El event loop es esto:
Básicamente, todos los eventos de tu app, que sean UI, I/O u otros están procesados por ese Event-loop uno por uno.
Un ejemplo concreto:
El objetivo de ese gif, es demostrar que cuando hago el tap sobre el FAB de la derecha, todo el cálculo se hará en el mainThread. Ese cálculo necesita tantos recursos y tiempo que toda la app (botón, animación, etc.) se quedan parados hasta que el cálculo se haya acabado:
Multithreading o computación en paralelo
No voy a entrar en el detalle de sobre que pasa si hay solamente un procesador o varios procesadores con el multithreading, solo te voy a decir que el multithreading permite, en el caso de Flutter, tener 2 (o varios) threads, o sea, 2 (o varios) event-loop, totalmente aislados (Isolate) el uno del otro.
Ya hemos visto con el ejemplo anterior que si hacemos un cálculo grande en el mainThread todo se queda bloqueado hasta que el cálculo haya terminado, entonces al crear un nuevo thread, todo ese cálculo se ejecuta con un otro event-loop, con otro “chunck de memoria” sin bloquear el event-loop del mainThread.
Con el siguiente gif, voy a usar la función compute() de Flutter y vamos a ver que pasa:
Pues va genial!, la animación no se bloquea y la app sigue funcionando como debería.
Existe otra posibilidad para crear un Isolate, y es usando Isolate de Dart, así que os dejo otro ejemplo usando Isolate.spawn():
Funciona igual de bien!
Ahora estás pensando, “vale, muy bien, pero como si implementa esto?” Y tienes razón, así que vamos a por el código:
Implementar un Isolate
Con Isolate:
Empezamos por el más complicado, así entenderemos mejor el increíble trabajo que han hecho en Flutter para facilitarnos las cosas:
Ante todo, te quiero recordar que es un Isolate, está totalmente aislada en otra parte de la memoria, entonces este Isolate no tiene acceso a tus instancias de clases del mainThread, entonces se necesita algo para que los 2 isolates puedan comunicar entre ellos y por esto esta: ReceivePort con su getter sendPort.
ReceivePort es un Stream y sendPort es quien manda el mensaje a ReceivePort.
Ahora que tenemos esto claro, solo nos queda crear el isolate con Isolate.spawn(), pero para crear este isolate, tenemos que tener en cuenta que la tarea tiene que ser una función Top-level o un método static (un isolate no puede tener acceso a tus instancias de clases, te acuerdas?) y pasamos en argumento la tarea y el sendPort para que el ReceivePort declarado en tu widget pueda recibir el mensaje.
Y si has estado atento hasta ahora, entonces sabes que solo nos queda escuchar el Stream receivePort y esperar de recibir los datos.
En este caso, una vez hemos recibido los datos, podemos hacer el kill del isolate, ya que no lo voy a necesitar más. Con compute:
Con compute, es mucho más fácil, pero no tenemos el control de elegir cuando queremos hacer el kill del isolate:
Como lo ves, es mucho más simple, pero el mismo se aplica que para Isolate.spawn(), la tarea tiene que ser una función top-level.
Y ya esta todo, ahora si ves que hay algún “Jank” en tu app, ya sabes que el mainThread está haciendo demasiado trabajo y está perdiendo algunas “frames”, y sabes que puedes arreglar esto usando un Isolate.
Os dejo el enlace del github que contiene el código de la app demo que he usado para esta story: https://github.com/arnaudelub/isolate_demo
Happy coding a todos!