mcpField
A
Advanced GetX Patterns

Advanced GetX Patterns

github.com/kaakati开发1054 次安装39 周前更新
代码仓库

关于 Advanced GetX Patterns

Advanced GetX Patterns 是面向使用 GetX 构建响应式 Dart 应用的开发者指南。内容涉及状态变化副作用、依赖与生命周期管理、HTTP 通信、后台队列和测试。

主要功能

  1. 01响应式 Worker 与副作用
  2. 02常驻服务和生命周期控制
  3. 03GetConnect HTTP 客户端模式
  4. 04组合式依赖绑定
  5. 05Rx 转换与后台队列
  6. 06Controller 和 Widget 测试

适用场景

  1. 01延迟处理搜索输入变化
  2. 02管理应用级认证服务
  3. 03组织路由专属依赖
  4. 04测试响应式 Controller 行为

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/kaakati/rails-enterprise-dev --skill advanced-getx-patterns

安装后在 Claude Code 中输入/advanced-getx-patterns确认已加载。

详细介绍

高级 GetX 模式

用于构建复杂响应式应用的高级 GetX 模式,涵盖恰当的状态管理、依赖注入和网络通信。

Workers——响应式副作用

当可观察值发生变化时,Worker 可用于执行回调。

ever——每次变化时执行
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——仅执行一次
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——延迟执行
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——定期执行
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 最佳实践
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——永久服务

GetxService 实例不会被释放,非常适合用作应用级服务。

创建服务
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();
  }
}
注册服务
// 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——生命周期控制

控制 GetX 如何管理控制器的生命周期。

SmartManagement 模式
void main() {
  runApp(
    GetMaterialApp(
      smartManagement: SmartManagement.full, // Default
      home: HomePage(),
    ),
  );
}

SmartManagement.full(默认):

  • 路由关闭时释放控制器
  • 内存效率最高
  • 推荐用于大多数应用

SmartManagement.onlyBuilder:

  • 仅释放通过 GetBuilder 创建的控制器
  • Get.find() 实例会保留
  • 需要手动控制时使用

SmartManagement.keepFactory:

  • 保留工厂实例
  • 可使用相同依赖项重新创建控制器
  • 适用于复杂的依赖关系图
手动释放控制器
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 客户端

GetConnect 提供功能强大的 HTTP 客户端,支持拦截器和基础 URL 配置。

基本设置
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');
  }
}
在 Repository 中使用 GetConnect
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

组合多个 Binding,以支持复杂功能。

创建 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(),
    ));
  }
}
使用 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 高级模式

自定义 Rx 类
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('[email protected]');
Rx 转换
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——后台任务

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
  }
}

测试 GetX 控制器

单元测试
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);
  });
}
使用 GetX 进行 Widget 测试
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>();
});

最佳实践

  1. Workers:

    • 搜索输入使用 debounce(延迟 800ms)
    • 副作用(分析、导航)使用 ever
    • 一次性操作(引导流程)使用 once
    • 定期更新(30s 或更长间隔)使用 interval
    • 始终在 onClose() 中释放 Worker
  2. GetxService:

    • 用于应用级服务(Auth、Theme、Localization)
    • 使用 permanent: true 注册
    • 在 init() 方法中初始化异步操作
    • 服务应尽可能保持无状态或不可变
  3. SmartManagement:

    • 大多数应用使用 SmartManagement.full(默认值)
    • 只有在有特定内存管理需求时才更改
    • 说明使用非默认管理模式的原因
  4. GetConnect:

    • 每个 API 创建一个 GetConnect 实例
    • 使用拦截器处理身份验证、日志记录和错误
    • 在 addAuthenticator 中实现重试逻辑
    • 在 Repository 中统一处理错误
  5. Bindings:

    • 每个功能或路由使用一个 Binding
    • 依赖项使用 lazyPut(首次使用时加载)
    • 需要立即使用的单例使用 put
    • 组合 Bindings 以支持复杂功能
  6. 测试:

    • 始终在 tearDown() 中释放控制器
    • 使用 Get.reset() 清除测试之间的所有依赖项
    • 模拟用例,而不是 Repository
    • 使用 pumpAndSettle() 测试响应式状态变化

评论

支持 Markdown