flutter
/

GetX Memory Management: Prevent Leaks & Optimize Performance

Last Sync: Today

On this page

12
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

GetX Memory Management: Prevent Leaks & Optimize Performance

Introduction

Memory management is crucial for building smooth, long‑running Flutter apps. GetX helps by automatically disposing controllers when they are no longer needed, but you must still clean up custom resources like streams, timers, and listeners. This guide explains how GetX handles memory, how to properly implement onClose, and best practices to prevent memory leaks.

  1. How GetX Manages Controllers

GetX tracks controller usage through a reference count. When you call Get.put or Get.lazyPut, the controller is registered. Each time Get.find is called from a mounted widget, the reference count increases. When the widget is unmounted, the count decreases. When the count reaches zero, GetX calls onClose() and disposes the controller. This automatic disposal prevents most memory leaks, but you still need to clean up any resources you created (e.g., streams, timers, animations) inside onClose.

  1. The onClose Method

Override onClose to release any resources that are not automatically cleaned up by GetX. Common resources to dispose:

  • Stream subscriptions (e.g., from Firebase, StreamController, or StreamSubscription).
  • Timers (Timer, Ticker).
  • Controllers (TextEditingController, AnimationController).
  • Listeners (e.g., ScrollController, PageController).
  • Dio clients or other network connections.
DARTRead-only
1
class MyController extends GetxController {
  StreamSubscription? _subscription;
  Timer? _timer;
  final TextEditingController textController = TextEditingController();

  @override
  void onInit() {
    super.onInit();
    _subscription = someStream.listen((data) {});
    _timer = Timer.periodic(Duration(seconds: 1), (_) {});
  }

  @override
  void onClose() {
    _subscription?.cancel();
    _timer?.cancel();
    textController.dispose();
    super.onClose();
  }
}

  1. Avoiding Memory Leaks in Common Scenarios

Always cancel subscriptions in onClose. For broadcast streams, you must cancel each subscription individually.

DARTRead-only
1
// Good
StreamSubscription? sub;
void onInit() {
  sub = stream.listen(print);
}
void onClose() {
  sub?.cancel();
}

// Dangerous – anonymous subscription not stored
void onInit() {
  stream.listen(print); // cannot cancel – memory leak
}
DARTRead-only
1
Timer? _timer;
void onInit() {
  _timer = Timer.periodic(Duration(seconds: 1), (_) {});
}
void onClose() {
  _timer?.cancel();
}
DARTRead-only
1
final TextEditingController controller = TextEditingController();
@override
void onClose() {
  controller.dispose();
  super.onClose();
}
DARTRead-only
1
late AnimationController _controller;
@override
void onInit() {
  super.onInit();
  _controller = AnimationController(vsync: this);
}
@override
void onClose() {
  _controller.dispose();
  super.onClose();
}

  1. Permanent Controllers & Memory

Controllers marked permanent: true or extending GetxService are never disposed automatically. Use them only for truly global services (e.g., authentication, API client). For such controllers, you should still implement onClose to clean up resources when you eventually delete them (if ever). If you never delete them, resources like streams will stay alive – ensure they are either not used or are properly managed (e.g., using a single subscription that lives as long as the app).

  1. Detecting Memory Leaks

To check if controllers are being disposed correctly, add print statements in onClose. Use Flutter DevTools to inspect the widget tree and memory usage. Look for controllers that are not being disposed when you pop routes.

DARTRead-only
1
@override
void onClose() {
  print('Controller disposed');
  super.onClose();
}

If you suspect a leak, use the DevTools memory tab and force garbage collection. If a controller stays in memory after its route is popped, you may have a lingering reference.

  1. Common Pitfalls

  • Creating controllers inside build method – New controller on every rebuild, never disposed. ✅ Use bindings or inject once.
  • Not canceling stream subscriptions – Subscriptions keep the controller alive. ✅ Cancel in onClose.
  • Using Get.put in a widget that rebuilds often – Creates duplicate instances. ✅ Use Get.lazyPut or bindings.
  • Keeping permanent controllers that hold large data – Unnecessary memory usage. ✅ Only make essential controllers permanent.
  • Forgetting to call super.onClose() – GetX internal cleanup may be skipped. ✅ Always call super.onClose() at the end of your override.

  1. Testing Memory Management

In unit tests, you can verify that onClose is called by creating a controller, disposing it via Get.delete, and checking that the flag was set. For widget tests, use tester.pumpAndSettle and verify that onClose logs appear.

DARTRead-only
1
test('Controller disposes correctly', () {
  final controller = MyController();
  Get.put(controller);
  Get.delete<MyController>();
  // Expect that onClose was called (add a flag in controller to verify)
});

Best Practices

  • Always dispose custom resources in onClose – Streams, timers, controllers.
  • Avoid storing large data in controllers unnecessarily – Offload to services with caching.
  • Use bindings – Let GetX manage lifecycle automatically.
  • Prefer Get.lazyPut – Defer creation until needed.
  • Limit permanent controllers – Only for app‑wide services.
  • Test disposal – Use logs and DevTools to ensure cleanup.

Common Mistakes

  • ❌ Not canceling subscriptions – Memory leak. ✅ Cancel in onClose.
  • ❌ Storing BuildContext in a controller – Can cause leaks if context outlives the widget. ✅ Avoid storing contexts; use Get.context when needed.
  • ❌ Using Get.put inside a widget's build – Creates new instances on each rebuild. ✅ Register in initState or bindings.
  • ❌ Forgetting to call super.onClose() – Breaks internal cleanup. ✅ Always call super.onClose().

FAQ

  • Q: Does GetX automatically dispose controllers when using bindings?
    A: Yes, if the controller is registered inside a binding and the route is popped, GetX will dispose it (unless marked permanent).
  • Q: How do I dispose a controller manually?
    A: Call Get.delete<MyController>(). This triggers onClose and removes it from the registry.
  • Q: What happens if I don't call super.onClose()?
    A: GetX's internal cleanup may not run, causing memory leaks. Always call it.
  • Q: Can I use onClose to save data before the controller is destroyed?
    A: Yes, it's a great place to save user input or state to GetStorage.
  • Q: Why is my controller not being disposed?
    A: It might be permanent, or there may be a lingering reference (e.g., a stream subscription that wasn't cancelled). Check isRegistered and logs.

Conclusion

GetX automates most controller lifecycle management, but you still need to clean up custom resources. By properly implementing onClose and following the best practices in this guide, you can ensure your Flutter app remains memory‑efficient and leak‑free.

Test Your Knowledge

Q1
of 3

Where should you dispose resources like streams and timers in a GetX controller?

A
onInit
B
onReady
C
onClose
D
onStart
Q2
of 3

What should you always do at the end of your onClose override?

A
Call dispose()
B
Call super.onClose()
C
Call delete()
D
Call reset()
Q3
of 3

What is the main reason for not cancelling a stream subscription in a controller?

A
The app will crash
B
Memory leak
C
Performance improvement
D
Automatic cancellation

Previous

getx offline support

Next

getx logging debugging

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.