flutter
/

GetX Controller: Lifecycle, Methods & Best Practices

Last Sync: Today

On this page

13
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

GetX Controller: Lifecycle, Methods & Best Practices

What is a GetX Controller?

A GetX controller is a class that extends GetxController and holds your business logic, state, and side effects. It is the heart of GetX's architecture, providing lifecycle management, reactive state, and easy dependency injection. Controllers are automatically disposed when not needed, preventing memory leaks.

Creating a Controller

DARTRead-only
1
class MyController extends GetxController {
  // Your logic and state here
  var count = 0.obs;

  void increment() => count++;
}

Controller Lifecycle

GetX controllers have three main lifecycle methods that you can override to execute code at specific moments.

  • onInit() – Called immediately after the controller is created. Perfect for initializing data, setting up workers, or calling APIs.
  • onReady() – Called after the widget tree is fully rendered. Ideal for tasks that require the UI to be ready (e.g., showing dialogs, starting animations).
  • onClose() – Called when the controller is about to be destroyed. Clean up resources like streams, timers, or dispose subscriptions.
MethodTimingUse Cases
`onInit()`Immediately after creationInitialize data, setup workers, call APIs
`onReady()`After UI is fully renderedShow dialogs, start animations, focus fields
`onClose()`Before disposalClose streams, cancel timers, dispose subscriptions
DARTRead-only
1
class MyController extends GetxController {
  @override
  void onInit() {
    super.onInit();
    print('Controller initialized');
    // Initialize data, start workers
  }

  @override
  void onReady() {
    super.onReady();
    print('UI is ready');
    // Show welcome dialog, start animations
  }

  @override
  void onClose() {
    print('Controller disposed');
    // Clean up resources
    super.onClose();
  }
}

Dependency Injection with Controllers

Controllers are typically managed by GetX's dependency injection system. You can inject them in several ways.

DARTRead-only
1
// 1. Using Get.put - immediate creation
final controller = Get.put(MyController());

// 2. Using Get.lazyPut - created when first used
Get.lazyPut(() => MyController());

// 3. Using Get.create - new instance each time
Get.create(() => MyController());

// 4. Using bindings (recommended for large apps)
class HomeBinding extends Bindings {
  @override
  void dependencies() {
    Get.lazyPut(() => HomeController());
  }
}

Accessing Controllers

Once registered, you can access a controller anywhere in your app using Get.find() or by using GetView.

DARTRead-only
1
// Using Get.find
final controller = Get.find<MyController>();

// Using GetView (recommended for views)
class MyView extends GetView<MyController> {
  @override
  Widget build(BuildContext context) {
    // controller is automatically available
    return Text('${controller.count}');
  }
}

State Management in Controllers

Controllers can hold both reactive and simple state, and decide how to notify the UI.

DARTRead-only
1
class MyController extends GetxController {
  // Reactive state (auto UI updates with Obx)
  var reactiveCount = 0.obs;

  // Simple state (manual updates with update())
  int simpleCount = 0;

  void incrementReactive() => reactiveCount++;

  void incrementSimple() {
    simpleCount++;
    update(); // triggers GetBuilder rebuilds
  }
}

Communication Between Controllers

Controllers can communicate using GetX's DI system. One controller can find another and call its methods or listen to its state.

DARTRead-only
1
class AuthController extends GetxController {
  var user = User().obs;

  void login(String email, String password) { ... }
}

class ProfileController extends GetxController {
  final AuthController auth = Get.find();

  void updateProfile() {
    // Access auth controller's state
    if (auth.user.value.isLoggedIn) { ... }
  }
}

Using Workers in Controllers

Workers are typically set up in onInit() to react to state changes.

DARTRead-only
1
class SearchController extends GetxController {
  var query = ''.obs;

  @override
  void onInit() {
    super.onInit();
    debounce(query, (_) => performSearch(), time: Duration(milliseconds: 500));
  }

  void performSearch() => print('Searching: ${query.value}');
}

Permanent vs Non-Permanent Controllers

By default, GetX disposes controllers when the route that uses them is removed. You can make a controller permanent to keep it alive for the entire app lifecycle.

DARTRead-only
1
// Permanent controller (never disposed)
Get.put(MyController(), permanent: true);

// With lazyPut, fenix allows recreation after disposal
Get.lazyPut(() => MyController(), fenix: true);

Comparison: GetxController vs GetxService

FeatureGetxControllerGetxService
DisposalAuto-disposed when route is removed (unless permanent)Never disposed automatically
Best forScreen/feature-specific logicApp-wide services (API client, storage, auth)
UsageOne per screen or featureSingleton across app
Lifecycle methodsonInit, onReady, onCloseSame, but never called onClose unless manually deleted

Use GetxService for services that should live the entire app lifetime. Use GetxController for disposable logic tied to a screen or module.

Best Practices

  • Single Responsibility – Each controller should handle one feature or screen. Avoid giant controllers with unrelated logic.
  • Use onInit for initialization – Never put async code directly in the constructor.
  • Use onClose for cleanup – Dispose streams, timers, and any listeners.
  • Prefer bindings for injection – Keeps the UI clean and avoids scattered Get.put calls.
  • Use GetView for cleaner UI code – Eliminates repetitive Get.find calls.
  • Avoid accessing controllers in UI before they're registered – Ensure Get.put or bindings are called first.
  • Use Get.isRegistered<T>() – Check if a controller exists before accessing.

Common Mistakes

  • ❌ Creating controllers inside build method – Causes multiple instances on rebuilds. ✅ Use Get.put or bindings outside build.
  • ❌ Forgetting to call super.onInit() – Breaks internal GetX setup. ✅ Always call super.onInit() in overridden methods.
  • ❌ Using onReady for heavy work that blocks UI – It's called after UI is ready but can still block if not async. ✅ Use async methods or delay heavy operations.
  • ❌ Not disposing resources – Leads to memory leaks. ✅ Clear streams, timers, and dispose listeners in onClose.
  • ❌ Hardcoding Get.find in many places – Makes refactoring harder. ✅ Use GetView or pass dependencies via constructor.

Conclusion

GetX controllers provide a robust foundation for managing state, logic, and lifecycle in Flutter apps. By understanding their lifecycle, injection options, and best practices, you can build maintainable, scalable applications with minimal boilerplate.

Try it yourself

import 'package:flutter/material.dart';
import 'package:get/get.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      home: HomePage(),
    );
  }
}

// A simple controller
class CounterController extends GetxController {
  var count = 0.obs;

  @override
  void onInit() {
    super.onInit();
    print('Controller initialized');
  }

  @override
  void onReady() {
    super.onReady();
    print('UI is ready');
    // Show a welcome message
    Get.snackbar('Welcome', 'The UI is ready!');
  }

  @override
  void onClose() {
    print('Controller disposed');
    super.onClose();
  }

  void increment() => count++;
}

class HomePage extends StatelessWidget {
  final controller = Get.put(CounterController());

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('GetX Controller Demo')),
      body: Center(
        child: Obx(() => Text(
          'Count: ${controller.count}',
          style: TextStyle(fontSize: 32),
        )),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: controller.increment,
        child: Icon(Icons.add),
      ),
    );
  }
}

Test Your Knowledge

Q1
of 3

Which lifecycle method is called immediately after the controller is created?

A
onStart
B
onInit
C
onReady
D
onClose
Q2
of 3

What should you do in the onClose method?

A
Initialize data
B
Show dialogs
C
Clean up resources (streams, timers)
D
Call update()
Q3
of 3

How do you make a controller permanent (never disposed)?

A
Use `permanent: true` in Get.put
B
Use `Get.lazyPut`
C
Use `Get.create`
D
It's permanent by default

Frequently Asked Questions

What's the difference between `GetxController` and `GetxService`?

GetxService is a controller that is never disposed automatically. Use it for app-wide services like API clients, theme managers, etc. GetxController is disposed when the route is removed (unless marked permanent).

Can I have multiple controllers per screen?

Yes, it's common to have one controller per logical feature. For complex screens, split into multiple smaller controllers.

How do I test a controller?

You can instantiate it normally and call its methods. Use Get.reset() to clear registrations between tests. Mock dependencies if needed.

Can controllers listen to each other's state?

Yes, one controller can Get.find another and observe its Rx variables using workers or Obx in the UI.

Do I need to dispose controllers manually?

No, GetX automatically disposes them when the associated route is removed. Only use Get.delete if you need to remove it earlier.

What is the order of lifecycle methods?

Constructor -> onInit -> the view is built -> onReady -> (when route is popped) -> onClose.

How do I make a controller permanent?

Use Get.put(MyController(), permanent: true) or in bindings set permanent: true. Alternatively, use GetxService.

Previous

getx workers

Next

getx lifecycle

Related Content

Need help?

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