关于 Advanced GetX Patterns
Advanced GetX Patterns 是面向使用 GetX 构建响应式 Dart 应用的开发者指南。内容涉及状态变化副作用、依赖与生命周期管理、HTTP 通信、后台队列和测试。
主要功能
- 01响应式 Worker 与副作用
- 02常驻服务和生命周期控制
- 03GetConnect HTTP 客户端模式
- 04组合式依赖绑定
- 05Rx 转换与后台队列
- 06Controller 和 Widget 测试
适用场景
- 01延迟处理搜索输入变化
- 02管理应用级认证服务
- 03组织路由专属依赖
- 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>();
});
最佳实践
-
Workers:
- 搜索输入使用
debounce(延迟 800ms) - 副作用(分析、导航)使用
ever - 一次性操作(引导流程)使用
once - 定期更新(30s 或更长间隔)使用
interval - 始终在
onClose()中释放 Worker
- 搜索输入使用
-
GetxService:
- 用于应用级服务(Auth、Theme、Localization)
- 使用
permanent: true注册 - 在
init()方法中初始化异步操作 - 服务应尽可能保持无状态或不可变
-
SmartManagement:
- 大多数应用使用
SmartManagement.full(默认值) - 只有在有特定内存管理需求时才更改
- 说明使用非默认管理模式的原因
- 大多数应用使用
-
GetConnect:
- 每个 API 创建一个
GetConnect实例 - 使用拦截器处理身份验证、日志记录和错误
- 在
addAuthenticator中实现重试逻辑 - 在 Repository 中统一处理错误
- 每个 API 创建一个
-
Bindings:
- 每个功能或路由使用一个 Binding
- 依赖项使用
lazyPut(首次使用时加载) - 需要立即使用的单例使用
put - 组合 Bindings 以支持复杂功能
-
测试:
- 始终在
tearDown()中释放控制器 - 使用
Get.reset()清除测试之间的所有依赖项 - 模拟用例,而不是 Repository
- 使用
pumpAndSettle()测试响应式状态变化
- 始终在
评论
支持 Markdown