Claude-skill-registry Advanced GetX Patterns
Advanced GetX features including Workers, GetxService, SmartManagement, GetConnect, GetSocket, bindings composition, and testing patterns
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/advanced-getx-patterns" ~/.claude/skills/majiayu000-claude-skill-registry-advanced-getx-patterns && rm -rf "$T"
skills/data/advanced-getx-patterns/SKILL.mdAdvanced GetX Patterns
Advanced GetX patterns for building sophisticated reactive applications with proper state management, dependency injection, and network communication.
Workers - Reactive Side Effects
Workers allow you to execute callbacks when observable values change.
ever - Execute on Every Change
class UserController extends GetxController { final user = Rx<User?>(null); final isAuthenticated = false.obs; @override void onInit() { super.onInit(); // Execute callback every time user changes ever(user, (User? userData) { if (userData != null) { print('User logged in: ${userData.name}'); isAuthenticated.value = true; } else { print('User logged out'); isAuthenticated.value = false; } }); } }
once - Execute Only Once
class OnboardingController extends GetxController { final hasCompletedOnboarding = false.obs; @override void onInit() { super.onInit(); // Execute only the first time value becomes true once(hasCompletedOnboarding, (_) { Get.offAllNamed(AppRoutes.home); // This won't run again even if value changes back to false and then true }); } }
debounce - Delay Execution
class SearchController extends GetxController { final searchQuery = ''.obs; final searchResults = <Product>[].obs; final isSearching = false.obs; @override void onInit() { super.onInit(); // Wait 800ms after user stops typing before searching debounce( searchQuery, (_) => performSearch(), time: const Duration(milliseconds: 800), ); } Future<void> performSearch() async { if (searchQuery.value.isEmpty) { searchResults.clear(); return; } isSearching.value = true; final result = await repository.search(searchQuery.value); result.fold( (failure) => searchResults.clear(), (products) => searchResults.value = products, ); isSearching.value = false; } }
interval - Execute Periodically
class DashboardController extends GetxController { final stats = Rx<DashboardStats?>(null); @override void onInit() { super.onInit(); // Refresh stats every 30 seconds while value changes interval( stats, (_) => refreshStats(), time: const Duration(seconds: 30), ); // Initial load refreshStats(); } Future<void> refreshStats() async { final result = await repository.getStats(); result.fold( (failure) => {}, (data) => stats.value = data, ); } }
Worker Best Practices
class MyController extends GetxController { final count = 0.obs; Worker? _countWorker; @override void onInit() { super.onInit(); // Store worker reference for manual disposal _countWorker = ever(count, (value) { print('Count changed to: $value'); }); } @override void onClose() { // Dispose worker manually if needed _countWorker?.dispose(); super.onClose(); } }
GetxService - Permanent Services
GetxService instances are never disposed, perfect for app-wide services.
Creating a Service
import 'package:get/get.dart'; class AuthService extends GetxService { final _isAuthenticated = false.obs; bool get isAuthenticated => _isAuthenticated.value; final _currentUser = Rx<User?>(null); User? get currentUser => _currentUser.value; // Called when service is first created @override Future<void> onInit() async { super.onInit(); await _loadSavedAuth(); } Future<void> _loadSavedAuth() async { final storage = Get.find<GetStorage>(); final token = storage.read<String>('auth_token'); if (token != null) { await validateToken(token); } } Future<void> login(String email, String password) async { final result = await repository.login(email, password); result.fold( (failure) => throw Exception(failure.message), (user) { _currentUser.value = user; _isAuthenticated.value = true; Get.find<GetStorage>().write('auth_token', user.token); }, ); } void logout() { _currentUser.value = null; _isAuthenticated.value = false; Get.find<GetStorage>().remove('auth_token'); } @override void onClose() { // GetxService onClose is never called // Service persists throughout app lifecycle super.onClose(); } }
Registering Services
// In main.dart or dependency injection setup Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); await GetStorage.init(); // Register permanent services Get.putAsync(() => AuthService().init(), permanent: true); Get.put(ThemeService(), permanent: true); Get.put(LocalizationService(), permanent: true); runApp(MyApp()); } // With async initialization class AuthService extends GetxService { Future<AuthService> init() async { await _loadSavedAuth(); return this; } }
SmartManagement - Lifecycle Control
Control how GetX manages controller lifecycle.
SmartManagement Modes
void main() { runApp( GetMaterialApp( smartManagement: SmartManagement.full, // Default home: HomePage(), ), ); }
SmartManagement.full (Default):
- Disposes controllers when routes are closed
- Most memory efficient
- Recommended for most apps
SmartManagement.onlyBuilder:
- Only disposes controllers created with
GetBuilder
instances persistGet.find()- Use when you need manual control
SmartManagement.keepFactory:
- Keeps factory instances
- Controllers can be recreated with same dependencies
- Use for complex dependency graphs
Manual Controller Disposal
class ManualController extends GetxController { // This controller won't auto-dispose } // Register without auto-dispose Get.put(ManualController(), permanent: true); // Manually dispose when needed Get.delete<ManualController>(); // Or with tag Get.put(ManualController(), tag: 'unique-tag'); Get.delete<ManualController>(tag: 'unique-tag');
GetConnect - HTTP Client
GetConnect provides a powerful HTTP client with interceptors and base URL configuration.
Basic Setup
class ApiProvider extends GetConnect { @override void onInit() { // Base URL httpClient.baseUrl = 'https://api.example.com'; // Default timeout httpClient.timeout = const Duration(seconds: 30); // Request interceptor httpClient.addRequestModifier<dynamic>((request) { // Add auth token to all requests final token = Get.find<AuthService>().token; if (token != null) { request.headers['Authorization'] = 'Bearer $token'; } request.headers['Content-Type'] = 'application/json'; return request; }); // Response interceptor httpClient.addResponseModifier((request, response) { // Log responses in debug mode if (kDebugMode) { print('Response: ${response.statusCode} ${response.bodyString}'); } return response; }); // Auth interceptor httpClient.addAuthenticator<dynamic>((request) async { // Refresh token if 401 final token = await refreshToken(); request.headers['Authorization'] = 'Bearer $token'; return request; }); } // GET request Future<Response<List<Product>>> getProducts() { return get<List<Product>>( '/products', decoder: (data) => (data as List) .map((item) => Product.fromJson(item)) .toList(), ); } // POST request Future<Response<User>> createUser(User user) { return post<User>( '/users', user.toJson(), decoder: (data) => User.fromJson(data), ); } // PUT request Future<Response<User>> updateUser(String id, User user) { return put<User>( '/users/$id', user.toJson(), decoder: (data) => User.fromJson(data), ); } // DELETE request Future<Response> deleteUser(String id) { return delete('/users/$id'); } }
Using GetConnect in Repository
class UserRepositoryImpl implements UserRepository { final ApiProvider apiProvider; UserRepositoryImpl({required this.apiProvider}); @override Future<Either<Failure, List<User>>> getUsers() async { try { final response = await apiProvider.get<List<User>>( '/users', decoder: (data) => (data as List) .map((json) => User.fromJson(json)) .toList(), ); if (response.hasError) { return Left(ServerFailure(response.statusText ?? 'Unknown error')); } return Right(response.body!); } catch (e) { return Left(ServerFailure(e.toString())); } } }
Bindings Composition
Combine multiple bindings for complex features.
Creating Bindings
// Feature binding class ProductBinding extends Bindings { @override void dependencies() { Get.lazyPut(() => ProductRemoteDataSource(api: Get.find())); Get.lazyPut(() => ProductLocalDataSource(storage: Get.find())); Get.lazyPut<ProductRepository>( () => ProductRepositoryImpl( remoteDataSource: Get.find(), localDataSource: Get.find(), ), ); Get.lazyPut(() => GetProducts(repository: Get.find())); Get.lazyPut(() => ProductController(getProducts: Get.find())); } } // Another feature binding class CartBinding extends Bindings { @override void dependencies() { Get.lazyPut(() => CartLocalDataSource(storage: Get.find())); Get.lazyPut<CartRepository>( () => CartRepositoryImpl(localDataSource: Get.find()), ); Get.lazyPut(() => AddToCart(repository: Get.find())); Get.lazyPut(() => RemoveFromCart(repository: Get.find())); Get.lazyPut(() => CartController( addToCart: Get.find(), removeFromCart: Get.find(), )); } } // Combine bindings class ProductDetailsBinding extends Bindings { @override void dependencies() { // Register product dependencies ProductBinding().dependencies(); // Register cart dependencies CartBinding().dependencies(); // Register page-specific controller Get.lazyPut(() => ProductDetailsController( product: Get.find(), cart: Get.find(), )); } }
Using BindingsBuilder
GetPage( name: '/checkout', page: () => CheckoutPage(), binding: BindingsBuilder(() { // Quick inline bindings Get.lazyPut(() => CheckoutController()); Get.lazyPut(() => PaymentService()); Get.lazyPut(() => ShippingService()); }), ) // Or with multiple bindings GetPage( name: '/checkout', page: () => CheckoutPage(), bindings: [ CartBinding(), PaymentBinding(), ShippingBinding(), ], )
Rx Advanced Patterns
Custom Rx Classes
class RxUser extends Rx<User> { RxUser(User initial) : super(initial); String get fullName => value.firstName + ' ' + value.lastName; bool get isAdmin => value.role == 'admin'; void updateEmail(String email) { value = value.copyWith(email: email); } } // Usage final user = RxUser(User(firstName: 'John', lastName: 'Doe')); user.updateEmail('john@example.com');
Rx Transformations
class DataController extends GetxController { final rawData = <Item>[].obs; // Computed observable List<Item> get filteredData => rawData.where((item) => item.isActive).toList(); // Or use Rx.map late final activeItems = rawData.map((data) => data.where((item) => item.isActive).toList() ).obs; }
GetQueue - Background Tasks
class UploadQueue extends GetxService { final _queue = <UploadTask>[].obs; void addTask(UploadTask task) { _queue.add(task); processQueue(); } Future<void> processQueue() async { while (_queue.isNotEmpty) { final task = _queue.first; try { await _uploadFile(task); _queue.removeAt(0); } catch (e) { // Retry logic task.retryCount++; if (task.retryCount >= 3) { _queue.removeAt(0); // Give up after 3 retries } else { await Future.delayed(Duration(seconds: task.retryCount * 2)); } } } } Future<void> _uploadFile(UploadTask task) async { // Upload logic } }
Testing GetX Controllers
Unit Testing
void main() { late ProductController controller; late MockGetProducts mockGetProducts; setUp(() { mockGetProducts = MockGetProducts(); controller = ProductController(getProducts: mockGetProducts); }); tearDown(() { controller.dispose(); }); test('loads products successfully', () async { // Arrange final products = [Product(id: '1', name: 'Product 1')]; when(() => mockGetProducts()) .thenAnswer((_) async => Right(products)); // Act await controller.loadProducts(); // Assert expect(controller.products, products); expect(controller.isLoading, false); verify(() => mockGetProducts()).called(1); }); test('handles failure when loading products', () async { // Arrange when(() => mockGetProducts()) .thenAnswer((_) async => Left(ServerFailure('Error'))); // Act await controller.loadProducts(); // Assert expect(controller.products, isEmpty); expect(controller.error, isNotNull); expect(controller.isLoading, false); }); }
Widget Testing with GetX
testWidgets('displays products when loaded', (tester) async { // Mock controller final controller = ProductController(getProducts: mockGetProducts); Get.put(controller); // Pump widget await tester.pumpWidget( GetMaterialApp( home: ProductListPage(), ), ); // Simulate loading controller.products.value = [Product(id: '1', name: 'Product 1')]; await tester.pumpAndSettle(); // Assert expect(find.text('Product 1'), findsOneWidget); // Cleanup Get.delete<ProductController>(); });
Best Practices
-
Workers:
- Use
for search inputs (800ms delay)debounce - Use
for side effects (analytics, navigation)ever - Use
for one-time actions (onboarding)once - Use
for periodic updates (30s+)interval - Always dispose workers in
onClose()
- Use
-
GetxService:
- Use for app-wide services (Auth, Theme, Localization)
- Register with
permanent: true - Initialize async operations in
methodinit() - Services should be stateless or immutable where possible
-
SmartManagement:
- Stick with
(default) for most appsSmartManagement.full - Only change if you have specific memory management needs
- Document why you're using non-default management
- Stick with
-
GetConnect:
- Create one
instance per APIGetConnect - Use interceptors for auth, logging, error handling
- Implement retry logic in
addAuthenticator - Handle errors consistently in repositories
- Create one
-
Bindings:
- One binding per feature/route
- Use
for dependencies (loaded when first used)lazyPut - Use
for singletons needed immediatelyput - Compose bindings for complex features
-
Testing:
- Always dispose controllers in
tearDown() - Use
to clear all dependencies between testsGet.reset() - Mock use cases, not repositories
- Test reactive state changes with
pumpAndSettle()
- Always dispose controllers in