Isolates y compute en Flutter: ejercicio resuelto

Isolates y compute en Flutter: ejercicio resuelto

Dart es de un único hilo. Todo código que bloquea el hilo principal (parsing de JSON largo, criptografía, compresión de imágenes) provoca jank — fotogramas perdidos que el usuario percibe como tartamudeo. La solución son los Isolates: hilos separados con su propia memoria, que se comunican solo mediante mensajes.

Enunciado

Implementa tres escenarios de procesamiento pesado:

  1. compute: parseo de una lista de 50 000 usuarios en JSON con un solo mensaje.
  2. Isolate bidireccional: cálculo de primos con progreso en tiempo real.
  3. Isolate.run (Flutter 3.7+): forma moderna y simplificada para tareas únicas.

Dependencias

Solo Flutter SDK.

Solución completa

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
import 'dart:async';
import 'dart:isolate';
import 'dart:math';
import 'dart:convert';
import 'package:flutter/foundation.dart';  // compute
import 'package:flutter/material.dart';

void main() => runApp(const MaterialApp(home: _DemoPage()));

// ── Modelos ────────────────────────────────────────────────────────────────────
class User {
  final int id;
  final String name;
  final String email;

  User({required this.id, required this.name, required this.email});

  factory User.fromJson(Map<String, dynamic> json) => User(
        id: json['id'] as int,
        name: json['name'] as String,
        email: json['email'] as String,
      );
}

// ── Funciones top-level (obligatorio para Isolate) ─────────────────────────────
// Las funciones pasadas a compute/Isolate.spawn DEBEN ser top-level o static
List<User> _parseUsers(String jsonStr) {
  final List<dynamic> raw = jsonDecode(jsonStr) as List;
  return raw.map((e) => User.fromJson(e as Map<String, dynamic>)).toList();
}

// Genera JSON de ejemplo con N usuarios
String _generateUsersJson(int count) {
  final rng = Random();
  const names = ['Ana', 'Luis', 'María', 'Carlos', 'Elena', 'Pedro', 'Sara'];
  final list = List.generate(count, (i) => {
    'id': i + 1,
    'name': names[rng.nextInt(names.length)],
    'email': 'user$i@example.com',
  });
  return jsonEncode(list);
}

// Función para el Isolate de primos — recibe SendPort para reportar progreso
void _findPrimesIsolate(List<dynamic> args) {
  final sendPort = args[0] as SendPort;
  final limit = args[1] as int;

  final primes = <int>[];
  for (int n = 2; n <= limit; n++) {
    bool isPrime = true;
    for (int i = 2; i <= sqrt(n); i++) {
      if (n % i == 0) { isPrime = false; break; }
    }
    if (isPrime) primes.add(n);

    // Reportar progreso cada 1000 números
    if (n % 1000 == 0) {
      sendPort.send({'progress': n / limit, 'count': primes.length});
    }
  }
  // Mensaje final con resultado completo
  sendPort.send({'done': true, 'primes': primes.length, 'largest': primes.last});
}

// ── Pantalla principal ─────────────────────────────────────────────────────────
class _DemoPage extends StatelessWidget {
  const _DemoPage();

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 3,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('Isolates demo'),
          bottom: const TabBar(tabs: [
            Tab(text: 'compute'),
            Tab(text: 'Isolate'),
            Tab(text: 'Isolate.run'),
          ]),
        ),
        body: const TabBarView(children: [
          _ComputePage(),
          _IsolatePage(),
          _IsolateRunPage(),
        ]),
      ),
    );
  }
}

// ── 1. compute — parseo de 50 000 usuarios ─────────────────────────────────────
class _ComputePage extends StatefulWidget {
  const _ComputePage();
  @override
  State<_ComputePage> createState() => _ComputePageState();
}

class _ComputePageState extends State<_ComputePage> {
  String _status = 'Pulsa el botón para parsear 50 000 usuarios';
  List<User>? _users;
  bool _loading = false;

  Future<void> _parse() async {
    setState(() { _loading = true; _status = 'Generando JSON…'; });

    // Generar JSON en main isolate (rápido)
    final json = _generateUsersJson(50000);

    setState(() => _status = 'Parseando en isolate separado…');
    final sw = Stopwatch()..start();

    // compute envía la función + argumento a un isolate temporal
    // El hilo principal NUNCA se bloquea
    final users = await compute(_parseUsers, json);

    sw.stop();
    setState(() {
      _users = users;
      _loading = false;
      _status = '✅ ${users.length} usuarios en ${sw.elapsedMilliseconds} ms';
    });
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(_status, textAlign: TextAlign.center),
            const SizedBox(height: 24),
            if (_loading) const CircularProgressIndicator(),
            if (!_loading)
              FilledButton(onPressed: _parse, child: const Text('Parsear JSON')),
            if (_users != null) ...[
              const SizedBox(height: 16),
              Text('Primeros 3: ${_users!.take(3).map((u) => u.name).join(', ')}'),
            ],
          ],
        ),
      ),
    );
  }
}

// ── 2. Isolate bidireccional con progreso ──────────────────────────────────────
class _IsolatePage extends StatefulWidget {
  const _IsolatePage();
  @override
  State<_IsolatePage> createState() => _IsolatePageState();
}

class _IsolatePageState extends State<_IsolatePage> {
  double _progress = 0;
  String _status = 'Inactivo';
  Isolate? _isolate;
  bool _running = false;

  Future<void> _start() async {
    if (_running) return;
    setState(() { _running = true; _progress = 0; _status = 'Calculando…'; });

    final receivePort = ReceivePort();

    _isolate = await Isolate.spawn(
      _findPrimesIsolate,
      [receivePort.sendPort, 100000],  // buscar primos hasta 100 000
    );

    receivePort.listen((message) {
      final msg = message as Map<String, dynamic>;
      if (mounted) {
        if (msg.containsKey('done')) {
          setState(() {
            _running = false;
            _progress = 1;
            _status = '✅ ${msg['primes']} primos. Mayor: ${msg['largest']}';
          });
          receivePort.close();
        } else {
          setState(() {
            _progress = msg['progress'] as double;
            _status = 'Primos encontrados: ${msg['count']}';
          });
        }
      }
    });
  }

  void _cancel() {
    _isolate?.kill(priority: Isolate.immediate);
    setState(() { _running = false; _status = 'Cancelado'; _progress = 0; });
  }

  @override
  void dispose() {
    _isolate?.kill(priority: Isolate.immediate);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(_status, textAlign: TextAlign.center),
            const SizedBox(height: 16),
            LinearProgressIndicator(value: _progress),
            const SizedBox(height: 24),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                FilledButton(
                    onPressed: _running ? null : _start,
                    child: const Text('Calcular primos')),
                const SizedBox(width: 12),
                OutlinedButton(
                    onPressed: _running ? _cancel : null,
                    child: const Text('Cancelar')),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

// ── 3. Isolate.run — Flutter 3.7+ ─────────────────────────────────────────────
class _IsolateRunPage extends StatefulWidget {
  const _IsolateRunPage();
  @override
  State<_IsolateRunPage> createState() => _IsolateRunPageState();
}

class _IsolateRunPageState extends State<_IsolateRunPage> {
  String _result = 'Pulsa para calcular SHA-256 de 1 MB de datos';
  bool _loading = false;

  Future<void> _run() async {
    setState(() { _loading = true; _result = 'Calculando…'; });

    // Isolate.run: crea un isolate, ejecuta la función, lo destruye al terminar
    // Más simple que Isolate.spawn para tareas únicas sin progreso
    final result = await Isolate.run(() {
      // Simulamos trabajo pesado: calcular hash de un millón de iteraciones
      var hash = 0;
      for (int i = 0; i < 1000000; i++) {
        hash = (hash + i * 31) & 0xFFFFFFFF;
      }
      return 'Hash simulado: 0x${hash.toRadixString(16).padLeft(8, '0').toUpperCase()}';
    });

    setState(() { _loading = false; _result = result; });
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(_result, textAlign: TextAlign.center),
            const SizedBox(height: 24),
            if (_loading) const CircularProgressIndicator(),
            if (!_loading)
              FilledButton(onPressed: _run, child: const Text('Ejecutar en Isolate')),
          ],
        ),
      ),
    );
  }
}

Comparativa de APIs

APICuándo usarlaProgresoCancelable
compute(fn, arg)Tarea única con un argumentoNoNo
Isolate.run(() {})Tarea única con closure (Flutter 3.7+)NoNo
Isolate.spawn(fn, msg)Larga duración o progreso incremental
IsolateNameServerCompartir ports por nombre entre isolates

Reglas de los Isolates

  1. No se comparte memoria: los datos se copian al enviarse por SendPort.
  2. Solo tipos primitivos y listas/mapas de primitivos pueden cruzar el puerto sin error (en general). Para objetos complejos, serializa a JSON o usa TransferableTypedData.
  3. Las funciones pasadas a spawn/compute deben ser top-level o static: no pueden capturar el contexto de un widget o un closure con referencias al árbol.

Errores frecuentes

  • Función no top-level en compute: compute solo acepta funciones que Dart puede serializar. Una función anónima o un método de instancia lanza error. Define la función fuera de cualquier clase.
  • Olvido de receivePort.close(): el port mantiene el Isolate vivo. Ciérralo cuando ya no lo necesites.
  • Usar setState desde el isolate: imposible, los isolates no tienen acceso al árbol de widgets. Usa SendPort para pasar el resultado al hilo principal.

Aplicación práctica

Isolates se usan para: parseo de JSON masivo (listas de miles de objetos), compresión/descompresión de archivos, procesamiento de imágenes (filtros, redimensionado), cálculos matemáticos intensivos (ML inference en CPU), cifrado y hashing.

Siguiente ejercicio recomendado

Práctica guiada y siguiente paso

FAQ

¿compute es simplemente un Isolate.run más antiguo?

Sí, conceptualmente. Isolate.run (Flutter 3.7 / Dart 2.19) es la versión moderna que acepta closures. compute sigue disponible por compatibilidad pero internamente hace lo mismo.

¿Cuándo NO usar un Isolate?

Cuando la tarea tarda menos de ~1-2 ms. Lanzar un Isolate tiene un overhead de ~1 ms. Para operaciones rápidas el overhead supera el beneficio.

¿Los Isolates funcionan en Flutter Web?

No de la misma forma. En Flutter Web, los Isolates se mapean a Web Workers, con limitaciones: no todos los tipos de datos son transferibles y la API difiere. Usa compute que abstrae las diferencias.