GetX State Management Patterns
Reactive State Management
Reactive Variables
class UserController extends GetxController { // Simple reactive variable final count = 0.obs;
// Complex reactive variable final user = Rx<User?>(null);
// Reactive list final users = <User>[].obs;
// Reactive map final settings = <String, dynamic>{}.obs;
// Getters (recommended) int get countValue => count.value; User? get currentUser => user.value; }
Reactive Widgets
// Option 1: Obx (lightweight) Obx(() => Text(controller.count.toString()))
// Option 2: GetX (with dependency injection) GetX<UserController>( builder: (controller) => Text(controller.user?.name ?? 'Loading...'), )
// Option 3: GetBuilder (no reactive variables needed) GetBuilder<UserController>( builder: (controller) => Text(controller.userName), )
Dependency Injection
Registration Methods
// Immediate instance Get.put(UserController());
// Lazy instance (created when first used) Get.lazyPut(() => UserController());
// Singleton (persists across routes) Get.putAsync(() => StorageService().init());
// Fenix (recreates when route is accessed again) Get.lazyPut(() => UserController(), fenix: true);
Bindings Pattern
class UserBinding extends Bindings { @override void dependencies() { // Data sources Get.lazyPut(() => UserProvider(Get.find())); Get.lazyPut(() => UserLocalSource(Get.find()));
// Repository
Get.lazyPut<UserRepository>(
() => UserRepositoryImpl(Get.find(), Get.find()),
);
// Use cases
Get.lazyPut(() => GetUser(Get.find()));
// Controller
Get.lazyPut(() => UserController(getUserUseCase: Get.find()));
} }
Navigation
Basic Navigation
// Navigate to route Get.to(() => ProfilePage());
// Navigate with name Get.toNamed('/profile');
// Navigate and remove previous Get.off(() => LoginPage());
// Navigate and remove all previous Get.offAll(() => HomePage());
// Go back Get.back();
// Go back with result Get.back(result: user);
Navigation with Arguments
// Send arguments Get.toNamed('/profile', arguments: {'id': '123'});
// Receive arguments final args = Get.arguments as Map<String, dynamic>; final id = args['id'];
Route Configuration
class AppRoutes { static const initial = '/splash';
static final routes = [ GetPage( name: '/splash', page: () => SplashPage(), binding: SplashBinding(), ), GetPage( name: '/login', page: () => LoginPage(), binding: AuthBinding(), transition: Transition.fadeIn, ), GetPage( name: '/home', page: () => HomePage(), binding: HomeBinding(), middlewares: [AuthMiddleware()], ), ]; }
Controller Lifecycle
class UserController extends GetxController { @override void onInit() { super.onInit(); // Called immediately after controller is allocated loadUser(); }
@override void onReady() { super.onReady(); // Called after widget is rendered analytics.logScreenView(); }
@override void onClose() { // Clean up resources _subscription.cancel(); super.onClose(); } }
Snackbars & Dialogs
// Snackbar Get.snackbar( 'Success', 'User created successfully', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.green, );
// Dialog Get.dialog( AlertDialog( title: Text('Confirm'), content: Text('Are you sure?'), actions: [ TextButton( onPressed: () => Get.back(), child: Text('Cancel'), ), TextButton( onPressed: () { deleteUser(); Get.back(); }, child: Text('Delete'), ), ], ), );
// Bottom sheet Get.bottomSheet( Container( child: Column( children: [ ListTile( title: Text('Option 1'), onTap: () => Get.back(result: 1), ), ], ), ), );
Best Practices
✅ DO
-
Use bindings for dependency injection
-
Use private reactive variables with public getters
-
Clean up resources in onClose()
-
Use meaningful controller names (e.g., UserController , not Controller1 )
-
Keep business logic in use cases, not controllers
❌ DON'T
-
Don't create controllers directly in widgets
-
Don't put business logic in controllers
-
Don't forget to dispose streams and subscriptions
-
Don't use global state excessively
-
Don't create circular dependencies
Anti-Patterns to Avoid
// ❌ BAD: Business logic in controller class UserController extends GetxController { Future<void> createUser(String name, String email) async { // Direct API call in controller final response = await http.post(...); // Business logic in controller if (response.statusCode == 201) { user.value = User.fromJson(response.body); } } }
// ✅ GOOD: Delegate to use case class UserController extends GetxController { final CreateUser createUserUseCase;
Future<void> createUser(String name, String email) async { final result = await createUserUseCase(name, email); result.fold( (failure) => _handleError(failure), (user) => user.value = user, ); } }