Para detectar fugas de memoria en Flutter lo más conveniente es analizar el heap del Dart VM en modo Profile. Aunque DevTools ofrece una interfaz más moderna, Observatory permite examinar el perfil de asignaciones directamente.
Conexión con Observatory
Al ejecutar la aplicación, la consola muestra un mensaje similar a:
listening on ws://127.0.0.1:64673/hXsWR_ZOsGk=/ws
Abre esa URL en el navegador. En la esquina inferior derecha aparece allocation profile.
Perfil de asignaciones y GC manual
Entra en la página que deseas analizar, vuelve atrás y pulsa el botón GC. Si en la lista de clases persisten instancias que solo deberían existir mientras la página está visible, es muy probable que haya una retención accidental.
Selecciona la clase y accede a una instancia concreta para ver su retaining path: el árbol de referencias que impide que el recolector la elimine.
Diferencia entre el heap de Dart y la memoria total
El valor que muestra Observatory solo cubre la memoria gestionada por el Dart VM. Recursos nativos como objetos Skia, texturas o capas del motor pueden consumir mucho más y no se contabilizan con precisión; por ejemplo, el motor reporta un tamaño fijo aproximado para cada EngineLayer:
size_t EngineLayer::GetAllocationSize() {
return 3000;
}
Por eso conviene complementar con Android Profiler o Instruments cuando se investiga el consumo real.
Escenarios típicos de fuga
1. Oyentes o callbacks no eliminados
Conservar una lista de oyentes sin retirarlos provoca que los objetos que implementan esos callbacks permanezcan en memoria.
abstract class EventListener {
void onEvent(Event event);
}
class EventBroker {
static final List<EventListener> _listeners = [];
static void subscribe(EventListener listener) {
_listeners.add(listener);
}
static void notify(Event event) {
for (final listener in _listeners) {
listener.onEvent(event);
}
}
}
Solución: proporcionar un método de baja explícito.
class EventBroker {
static final List<EventListener> _listeners = [];
static void subscribe(EventListener listener) {
_listeners.add(listener);
}
static void unsubscribe(EventListener listener) {
_listeners.remove(listener);
}
static void notify(Event event) {
for (final listener in _listeners) {
listener.onEvent(event);
}
}
}
2. Flujos de imagen sin cerrar
Si un State escucha un ImageStream y no cancela la suscripción en dispose, el ImageInfo puede quedar retenido.
class ImagePreview extends StatefulWidget {
const ImagePreview({super.key});
@override
State<ImagePreview> createState() => _ImagePreviewState();
}
class _ImagePreviewState extends State<ImagePreview> {
ImageStream? _stream;
ImageStreamListener? _listener;
ImageInfo? _frame;
void _loadImage(ImageProvider provider) {
_stream = provider.resolve(const ImageConfiguration());
_listener = ImageStreamListener((info, _) {
setState(() => _frame = info);
});
_stream!.addListener(_listener!);
}
@override
void dispose() {
if (_stream != null && _listener != null) {
_stream!.removeListener(_listener!);
}
_frame?.image.dispose();
_frame = null;
_stream = null;
_listener = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container();
}
}
3. AssetBundle con cacheado permanente
CachingAssetBundle mantiene en memoria todo lo leído durante la vida de la app. Si se cargan JSON grandes desde assets, usa cache: false o una instancia que no almacene datos.
class AssetReader {
static AssetReader? _instance;
final AssetBundle _bundle;
AssetReader._(this._bundle);
factory AssetReader() {
_instance ??= AssetReader._(PlatformAssetBundle());
return _instance!;
}
Future<Map<String, dynamic>> readJson(String path) async {
final content = await _bundle.loadString(path, cache: false);
return jsonDecode(content) as Map<String, dynamic>;
}
}
4. Listas largas con imágenes
En versiones antiguas de Flutter, desplazar listas con muchas imágenes podía aumentar el consumo de memoria. Desde Flutter 1.17 el motor mejora la liberación de recursos gráficos. Para minimizar el impacto, usa ListView.builder, limita las dimensiones de las imágenes con cacheWidth/cacheHeight y evita retener manualmente proveedores de imágenes fuera del árbol de widgets.