Mar 22nd, 2025
This is the second blog post in the
Flutter – Detecting memory leaks
series. If you missed the first one and want to learn about the inner workings of Dart’s Garbage Collector, you can do so
.
In this article, I will provide code examples of how to manually dispose of objects that Dart’s GC won’t handle automatically. I will keep it as short as possible, one example per section.
The examples cover all of the referenced non-collectable objects from the first article, but the last, as the disposal of third-party libraries is tied to the selected library’s documentation.
Plenty of Flutter objects have the
dispose
or
cancel
method on them. Be sure first to check if your potential leaking object has one. The AnimationController used in this example is one of them and is tied to the framework’s tickers. If these resources are not properly released, it can lead to memory leaks.
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 1),
)..repeat();
}
@override
void dispose() {
_controller.dispose(); // Dispose the AnimationController
super.dispose();
}
...
}
Global variables are accessible throughout the app and have no specific end-of-lifecycle defined. That’s why you must set them to
null
when their use is no longer needed.
class AppState {
static var largeData = List<int>.filled(1000000, 0);
}
void cleanupGlobalVariables() {
AppState.largeData = null; // Free up memory if it is no longer needed
}
The stream's sole purpose is to pipe down the stream data when and where it is needed. It is not tied to a single use, and Dart's GC cannot know when the stream's data is no longer needed. Cancel subscriptions to streams when they are no longer needed to prevent memory leaks.
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
StreamSubscription<int>? _subscription;
@override
void initState() {
super.initState();
_subscription = someStream.listen((data) {
print('Stream sent data: $data');
});
}
@override
void dispose() {
_subscription?.cancel(); // Cancel the subscription
super.dispose();
}
...
}
Flutter’s build context (
BuildContext
) is a short-lived and large object. Never use
BuildContext
inside closures if the closure OUTLIVES the widget. If you need context inside a closure, assign the context to a variable and then use that variable inside the closure. Dart’s GC automatically collects variables.
// BAD
Widget build(BuildContext context) {
final printer = () => print(someFunctionUsingContext(context));
usePrinter(printer);
…
}
// GOOD
Widget build(BuildContext context) {
final result = someFunctionUsingContext(context); // variable gets GCed
final printer = () => print(result);
usePrinter(printer);
…
}
The situation with caches and maps is a bit similar to the global variables. They hold onto objects even when you no longer need them by keeping strong references to them. Ensuring caches and maps won’t cause unwanted memory leaks includes several approaches, all viable and dependent on your use case.
cache.remove('key'); // Remove an object
cache.clear(); // Clear the entire cache
final weakCache = WeakMap<Key, Value>();
weakCache[key] = BigObject(); // Doesn’t prevent garbage collection
final cache = LinkedHashMap<String, BigObject>();
void addToCache(String key, BigObject value) {
if (cache.length >= 100) {
cache.remove(cache.keys.first); // Remove the oldest object
}
cache[key] = value;
}
final imageCache = PaintingBinding.instance.imageCache;
imageCache.clear(); // Clear the image cache
When a timer is created, the objects in its callback form a strong reference to it. When the timer’s purpose is outlived, these strong references still exist, and without manual disposal, they will cause memory leaks.
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
Timer _timer;
@override
void initState() {
super.initState();
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
print("Tick-tock!");
});
}
@override
void dispose() {
_timer.cancel(); // Cancel the timer when the widget is disposed
super.dispose();
}
...
Targeted platform-specific methods need to be handled/disposed of properly. A developer must manually handle acquiring and then releasing the resource to prevent memory leaks.
class WakeLockScreen extends StatefulWidget {
@override
_WakeLockScreenState createState() => _WakeLockScreenState();
}
class _WakeLockScreenState extends State<WakeLockScreen> {
static const platform = MethodChannel('com.example.wakelock');
@override
void initState() {
super.initState();
_acquireWakeLock();
}
Future<void> _acquireWakeLock() async {
try {
await platform.invokeMethod('acquireWakeLock');
} on PlatformException catch (e) {
print("Failed to acquire WakeLock: ${e.message}");
}
}
Future<void> _releaseWakeLock() async {
try {
await platform.invokeMethod('releaseWakeLock');
} on PlatformException catch (e) {
print("Failed to release WakeLock: ${e.message}");
}
}
@override
void dispose() {
_releaseWakeLock(); // Release the WakeLock when the widget is disposed
super.dispose();
}
...
If you believe you disposed of all non-collectable objects, but your app is still leaking, it is time to use tools that help you detect where your app is leaking. First, let’s go over the simplest one,
.