home / skills / kaakati / rails-enterprise-dev / getx-patterns

This skill helps you implement GetX state management patterns with reactive variables, bindings, navigation, and lifecycle best practices for scalable apps.

npx playbooks add skill kaakati/rails-enterprise-dev --skill getx-patterns

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
5.4 KB
---
name: "GetX State Management Patterns"
description: "GetX controllers, reactive state, dependency injection, bindings, navigation, and best practices"
version: "1.0.0"
---

# GetX State Management Patterns

## Reactive State Management

### Reactive Variables
```dart
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
```dart
// 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
```dart
// 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
```dart
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
```dart
// 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
```dart
// Send arguments
Get.toNamed('/profile', arguments: {'id': '123'});

// Receive arguments
final args = Get.arguments as Map<String, dynamic>;
final id = args['id'];
```

### Route Configuration
```dart
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

```dart
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

```dart
// 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

```dart
// ❌ 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,
    );
  }
}
```

Overview

This skill documents GetX state management patterns for Flutter apps, covering reactive state, dependency injection, bindings, navigation, lifecycle hooks, and UI feedback. It focuses on practical, enterprise-ready approaches that keep controllers lightweight and business logic in use cases. Clear examples and recommended anti-patterns help teams build predictable, testable apps.

How this skill works

It inspects common GetX building blocks: reactive variables (.obs, Rx<T>), reactive widgets (Obx, GetX, GetBuilder), dependency injection (Get.put, Get.lazyPut, fenix), and Bindings for route-scoped wiring. It also covers navigation (Get.to, named routes, arguments), controller lifecycle (onInit, onReady, onClose), and standard UI feedback (snackbars, dialogs, bottom sheets). The guidance shows where to place business logic and how to manage resources safely.

When to use it

  • When you need a lightweight, reactive state solution without heavy boilerplate
  • When route-scoped dependency wiring and lazy initialization improve startup and memory usage
  • When you want clear separation of UI controllers and business use cases for testability
  • When you need predictable navigation with arguments and route bindings
  • When you must manage subscriptions and resources tied to controller lifecycle

Best practices

  • Use Bindings to register providers, repositories, use cases, and controllers per route
  • Keep controllers thin: delegate business logic to use cases and repositories
  • Use private .obs variables with public getters to encapsulate state
  • Prefer Get.lazyPut and fenix for memory-efficient lifecycle behavior
  • Clean up streams and subscriptions in onClose() to avoid leaks
  • Avoid global state and circular dependencies; prefer route-scoped DI

Example use cases

  • User profile flow: Bind repository, use case, and UserController per profile route
  • List and pagination: Use reactive lists (.obs) and Obx for lightweight UI updates
  • Authentication: Route-level bindings with middleware to guard routes and lazy services
  • Form submission: Controller triggers use case; show Get.snackbar on success or error
  • Dialog flows: Use Get.dialog/Get.bottomSheet to return results to calling route

FAQ

When should I use Obx vs GetBuilder?

Use Obx or GetX for reactive variables (.obs / Rx<T>) for minimal rebuilds. Use GetBuilder when you don't use reactive variables and prefer explicit update() calls for performance control.

How do I avoid memory leaks with GetX controllers?

Register controllers via Bindings and clean up streams/subscriptions in onClose(). Use Get.lazyPut with fenix only when you want recreation behavior and ensure no lingering references remain.