Mixin 架构模式 - 如何用 Mixin 拆分复杂 Controller
Flutter Mixin 架构模式:如何优雅拆分 2000+ 行的复杂 Controller
当你的 Controller 膨胀到 2000 行以上,代码变得难以维护时,Mixin 是一个优雅的解决方案。本文分享如何使用 Mixin 模式将复杂的 GetX Controller 拆分为多个职责单一的模块。
问题背景
在开发相册整理应用时,PhotoCleanController 承担了太多职责:
- 照片列表管理和分组
- 滑动手势处理
- 撤销/重做功能
- 缓存管理
- 删除操作
- 相似照片检测
- 各种工具方法
最初这些代码都写在一个文件里,导致:
- 文件过长:超过 2000 行,难以阅读
- 职责混乱:不同功能的代码交织在一起
- 难以测试:无法单独测试某个功能模块
- 协作困难:多人修改同一文件容易冲突
为什么选择 Mixin?
在 Dart 中,拆分大类有几种方案:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 继承 | 简单直接 | 单继承限制,耦合度高 |
| 组合 | 解耦彻底 | 需要大量委托代码,状态共享麻烦 |
| Mixin | 多重混入,状态共享方便 | 需要理解 Mixin 机制 |
Mixin 的优势在于:
- 可以混入多个:一个类可以
with多个 Mixin - 共享状态:Mixin 可以访问主类的属性和方法
- 职责分离:每个 Mixin 负责一个功能领域
- 渐进式重构:可以逐步将代码迁移到 Mixin
架构设计
2. Mixin 的依赖声明
每个 Mixin 需要声明它依赖的属性和方法:
/// 滑动手势处理 Mixin
mixin PhotoSwipeMixin on GetxController {
// ==================== 需要主控制器提供的属性 ====================
Rx<CardStackSwiperDirection?> get currentSwipeDirection;
RxDouble get swipeProgress;
RxInt get currentPhotoIndex;
RxList<AssetEntity> get currentGroupPhotos;
// 预加载器(用于滑动时暂停预加载)
PhotoPreloader get photoPreloader;
// 需要调用的方法
void executeSwipeAction(int index, SwipeAction action, CardStackSwiperDirection direction);
void handleEndOfStack();
// 服务依赖
dynamic get mainController;
// ==================== Mixin 自己的实现 ====================
/// 处理滑动事件
void handleSwipe(
int? previousIndex,
int? currentIndex,
CardStackSwiperDirection direction,
) {
if (previousIndex != null) {
final settingsController = Get.find<SettingsController>();
SwipeAction action;
switch (direction) {
case CardStackSwiperDirection.top:
action = settingsController.swipeUpAction.value;
executeSwipeAction(previousIndex, action, direction);
break;
// ... 其他方向
}
// 最后一张卡片被滑出
if (currentIndex == null) {
Future.delayed(const Duration(milliseconds: 300), handleEndOfStack);
}
}
// 重置滑动状态
currentSwipeDirection.value = null;
swipeProgress.value = 0.0;
// 滑动结束,恢复预加载
photoPreloader.resume();
}
/// 更新滑动进度(带节流优化)
void updateSwipeProgressFromPercentage(double horizontalPct, double verticalPct) {
// 节流控制,避免过于频繁的 UI 更新
final now = DateTime.now();
if (now.difference(_lastProgressUpdate) < _progressThrottle) return;
// ... 进度计算逻辑
if ((swipeProgress.value - newProgress).abs() > _progressUpdateThreshold) {
swipeProgress.value = newProgress;
_lastProgressUpdate = now;
}
}
}
这种模式的好处:
- 依赖明确:Mixin 开头就声明了所有依赖
- 类型安全:编译器会检查主控制器是否提供了这些属性
- 自包含:Mixin 内部逻辑完整,不需要了解主控制器的其他部分
3. 撤销功能 Mixin 示例
/// 撤销功能 Mixin
mixin PhotoUndoMixin on GetxController {
// ==================== 依赖声明 ====================
RxList<UndoRecord> get undoStack;
Rx<AssetEntity?> get undoingPhoto;
RxBool get isUndoAnimating;
RxList<AssetEntity> get currentGroupPhotos;
RxList<AssetEntity> get pendingDeletePhotos;
RxSet<String> get processedPhotoIds;
RxInt get deletedCount;
RxInt get cardStackVersion;
// 需要调用的方法
Future<void> batchUpdatePhotoStates(List<String> photoIds, Map<String, dynamic> updates);
// 服务
DatabaseService get databaseService;
SimilarPhotoService get similarPhotoService;
FeedbackService get feedbackService;
// ==================== 公开 API ====================
/// 是否可以撤销
bool get canUndo => undoStack.isNotEmpty && !isUndoAnimating.value;
/// 添加操作到撤销栈(优化版:延迟获取相似组数据)
void pushUndoRecord(
AssetEntity photo,
String actionType,
int originalIndex,
CardStackSwiperDirection swipeDirection, [
String? similarGroupId,
]) {
// 限制撤销栈大小
while (undoStack.length >= maxUndoCount) {
undoStack.removeAt(0);
}
// 先添加基础记录,不阻塞主线程
final record = UndoRecord(
photo: photo,
actionType: actionType,
originalIndex: originalIndex,
swipeDirection: swipeDirection,
similarGroupId: similarGroupId,
);
undoStack.add(record);
// 异步补充相似组数据(用于撤销时恢复)
if (similarGroupId != null) {
_enrichUndoRecordWithSimilarGroup(record, photo.id);
}
}
/// 执行撤销操作
Future<void> undo() async {
if (!canUndo) return;
feedbackService.triggerUndoFeedback();
final record = undoStack.removeLast();
if (record.isBatchOperation) {
await _undoBatchOperation(record);
return;
}
isUndoAnimating.value = true;
undoingPhoto.value = record.photo;
try {
await Future.delayed(const Duration(milliseconds: 320));
// 恢复状态
processedPhotoIds.remove(record.photo.id);
if (record.actionType == 'delete') {
pendingDeletePhotos.removeWhere((p) => p.id == record.photo.id);
deletedCount.value--;
}
// 批量更新数据库
await Future.wait([
databaseService.updatePhotoProcessedState(record.photo.id, false),
databaseService.updatePhotoPendingDeleteState(record.photo.id, false),
databaseService.updatePhotoKeepState(record.photo.id, false),
]);
// 恢复相似组缓存
if (record.similarGroupId != null) {
similarPhotoService.restorePhotoToGroup(record.photo, record.similarGroupId);
}
// 恢复到卡片堆
final insertIndex = record.originalIndex.clamp(0, currentGroupPhotos.length);
currentGroupPhotos.insert(insertIndex, record.photo);
cardStackVersion.value++;
} finally {
isUndoAnimating.value = false;
undoingPhoto.value = null;
}
}
}
4. 缓存管理 Mixin 示例
/// 缓存管理 Mixin
mixin PhotoCacheMixin on GetxController {
// ==================== 依赖声明 ====================
GenericLRUCache<String, Uint8List> get thumbnailCache;
GenericLRUCache<String, Uint8List> get photoDataCache;
PhotoPreloader get photoPreloader;
AdaptiveImageDecodeThrottler get throttler;
Map<String, String> get albumNameCache;
Map<String, LatLng?> get locationCache;
UnifiedCacheManager get cacheManager;
MediaService get mediaService;
// ==================== 公开 API ====================
/// 获取照片缩略图(带缓存)
Future<Uint8List?> getPhotoThumbnail(
AssetEntity photo, {
int width = 400,
int height = 400,
}) async {
final cacheKey = '${photo.id}_${width}x$height';
// 1. 先查缓存
final cached = thumbnailCache.get(cacheKey);
if (cached != null) return cached;
try {
// 2. 缓存未命中,从系统获取
final thumbnail = await mediaService.getThumbnail(photo, width: width, height: height);
// 3. 写入缓存
if (thumbnail != null) {
thumbnailCache.put(cacheKey, thumbnail, sizeBytes: thumbnail.length);
}
return thumbnail;
} catch (e) {
logger.e('获取缩略图失败: $e');
return null;
}
}
/// 获取照片数据(带智能尺寸优化)
Future<Uint8List?> getPhotoData(AssetEntity photo, {bool isForDisplay = true}) async {
// 1. 查缓存
final cachedData = photoDataCache.get(photo.id);
if (cachedData != null) return cachedData;
try {
Uint8List? data;
if (isForDisplay) {
// 显示用途:根据屏幕尺寸优化
data = await _getOptimizedPhotoData(photo);
} else {
// 分享等用途:获取原图
data = await mediaService.getOriginalImage(photo);
}
if (data != null) {
photoDataCache.put(photo.id, data, sizeBytes: data.length);
// 检查是否需要内存清理
cacheManager.checkAndCleanupIfNeeded();
}
return data;
} catch (e) {
logger.e('获取照片数据失败: $e');
return null;
}
}
/// 获取优化尺寸的照片数据
Future<Uint8List?> _getOptimizedPhotoData(AssetEntity photo) async {
final screenSize = Get.size;
final devicePixelRatio = Get.pixelRatio;
// 计算目标尺寸(不超过屏幕的 2 倍)
final maxWidth = (screenSize.width * devicePixelRatio).clamp(0, 2048).toInt();
final maxHeight = (screenSize.height * devicePixelRatio).clamp(0, 2048).toInt();
if (photo.width > maxWidth || photo.height > maxHeight) {
// 需要缩放
final aspectRatio = photo.width / photo.height;
int targetWidth, targetHeight;
if (aspectRatio > 1) {
targetWidth = maxWidth;
targetHeight = (maxWidth / aspectRatio).round();
} else {
targetHeight = maxHeight;
targetWidth = (maxHeight * aspectRatio).round();
}
return await throttler.decodeThumbnail(
photo: photo,
size: ThumbnailSize(targetWidth, targetHeight),
quality: 85,
);
}
return await mediaService.getOriginalImage(photo);
}
/// 清理并注销缓存(在 onClose 时调用)
void disposeCache() {
thumbnailCache.clear();
photoDataCache.clear();
albumNameCache.clear();
locationCache.clear();
photoPreloader.clearState();
cacheManager.unregisterCache('photo_thumbnail');
cacheManager.unregisterCache('photo_data');
}
/// 获取内存使用情况(调试用)
Map<String, dynamic> getMemoryUsage() {
return {
'thumbnailCacheCount': thumbnailCache.getStats()['count'],
'photoDataCacheCount': photoDataCache.getStats()['count'],
'photoDataCacheMemoryMB': photoDataCache.getStats()['memoryBytes'] / 1024 / 1024,
};
}
}
5. 统一导出文件
// mixins/mixins.dart
export 'photo_batch_mixin.dart';
export 'photo_swipe_mixin.dart';
export 'photo_undo_mixin.dart';
export 'photo_cache_mixin.dart';
export 'photo_delete_mixin.dart';
export 'photo_similar_mixin.dart';
export 'photo_utils_mixin.dart';
主控制器只需要一行导入:
import 'mixins/mixins.dart';
Mixin 间的协作
跨 Mixin 调用
有时一个 Mixin 需要调用另一个 Mixin 的方法。有两种处理方式:
方式一:通过主控制器声明抽象方法
// PhotoDeleteMixin 需要调用 PhotoBatchMixin 的方法
mixin PhotoDeleteMixin on GetxController {
// 声明为抽象方法,由主控制器提供实现
Future<void> batchUpdatePhotoStates(List<String> photoIds, Map<String, dynamic> updates);
String getPhotoAlbumName(AssetEntity photo);
Future<void> confirmDelete() async {
// ... 删除逻辑
await batchUpdatePhotoStates(deletedIds, {'deleted': 1});
}
}
方式二:直接在主控制器中实现
class PhotoCleanController extends GetxController
with PhotoBatchMixin, PhotoDeleteMixin {
// 实现 PhotoDeleteMixin 需要的方法
@override
Future<void> batchUpdatePhotoStates(List<String> photoIds, Map<String, dynamic> updates) async {
// 实际实现
}
}
状态共享
所有 Mixin 共享主控制器的状态:
class PhotoCleanController extends GetxController with PhotoBatchMixin, PhotoUndoMixin {
// 共享状态
@override
final RxList<AssetEntity> currentGroupPhotos = <AssetEntity>[].obs;
// PhotoBatchMixin 可以修改 currentGroupPhotos
// PhotoUndoMixin 也可以修改 currentGroupPhotos
// 它们操作的是同一个响应式列表
}
最佳实践
1. 单一职责
每个 Mixin 只负责一个功能领域:
// ✅ 好:职责单一
mixin PhotoCacheMixin { /* 只处理缓存 */ }
mixin PhotoDeleteMixin { /* 只处理删除 */ }
// ❌ 差:职责混乱
mixin PhotoOperationsMixin { /* 缓存 + 删除 + 分享 + ... */ }
2. 依赖最小化
Mixin 只声明它真正需要的依赖:
// ✅ 好:只声明需要的
mixin PhotoSwipeMixin on GetxController {
RxDouble get swipeProgress;
RxInt get currentPhotoIndex;
}
// ❌ 差:声明了不需要的
mixin PhotoSwipeMixin on GetxController {
RxDouble get swipeProgress;
RxInt get currentPhotoIndex;
RxList<AssetEntity> get pendingDeletePhotos; // 滑动不需要这个
DatabaseService get databaseService; // 滑动不需要这个
}
3. 私有方法前缀
Mixin 内部的私有方法使用下划线前缀:
mixin PhotoCacheMixin on GetxController {
// 公开 API
Future<Uint8List?> getPhotoData(AssetEntity photo) async { ... }
// 内部实现
Future<Uint8List?> _getOptimizedPhotoData(AssetEntity photo) async { ... }
void _cleanupOldCache() { ... }
}
4. 文档注释
每个 Mixin 开头写清楚职责:
/// 撤销功能 Mixin
///
/// 负责:
/// - 撤销栈管理
/// - 单次操作撤销
/// - 批量操作撤销
/// - 相似组状态恢复
mixin PhotoUndoMixin on GetxController {
// ...
}
5. 避免循环依赖
Mixin 之间不应该直接相互引用:
// ❌ 差:循环依赖
mixin MixinA on GetxController {
MixinB get mixinB; // 直接引用另一个 Mixin
}
// ✅ 好:通过抽象方法解耦
mixin MixinA on GetxController {
void someMethodFromB(); // 声明需要的方法,由主控制器提供
}
重构步骤
如果你有一个臃肿的 Controller,可以按以下步骤重构:
Step 1: 识别功能边界
分析现有代码,找出可以独立的功能模块:
PhotoCleanController (2000+ 行)
├── 照片加载和分组逻辑 → PhotoBatchMixin
├── 滑动手势处理 → PhotoSwipeMixin
├── 撤销功能 → PhotoUndoMixin
├── 缓存管理 → PhotoCacheMixin
├── 删除操作 → PhotoDeleteMixin
├── 相似照片处理 → PhotoSimilarMixin
└── 工具方法 → PhotoUtilsMixin
Step 2: 创建 Mixin 文件
mkdir -p lib/app/modules/photo/controllers/mixins
touch lib/app/modules/photo/controllers/mixins/photo_batch_mixin.dart
touch lib/app/modules/photo/controllers/mixins/photo_swipe_mixin.dart
# ... 其他 Mixin
touch lib/app/modules/photo/controllers/mixins/mixins.dart
Step 3: 逐个迁移
从最独立的模块开始,逐个迁移:
- 创建 Mixin 骨架
- 声明依赖(需要的属性和方法)
- 迁移相关代码
- 在主控制器中
with这个 Mixin - 测试功能是否正常
- 重复以上步骤
Step 4: 清理主控制器
迁移完成后,主控制器应该只剩下:
- 状态声明
- 服务依赖
- 生命周期方法
- 少量协调逻辑
效果对比
重构前
// photo_clean_controller.dart - 2000+ 行
class PhotoCleanController extends GetxController {
// 所有状态声明 (100+ 行)
// 所有服务依赖 (20+ 行)
// 照片加载逻辑 (300+ 行)
// 滑动处理逻辑 (200+ 行)
// 撤销逻辑 (250+ 行)
// 缓存逻辑 (200+ 行)
// 删除逻辑 (300+ 行)
// 相似照片逻辑 (400+ 行)
// 工具方法 (200+ 行)
}
重构后
// photo_clean_controller.dart - 约 500 行
class PhotoCleanController extends GetxController
with PhotoBatchMixin, PhotoSwipeMixin, PhotoUndoMixin,
PhotoCacheMixin, PhotoDeleteMixin, PhotoSimilarMixin,
PhotoUtilsMixin {
// 状态声明 (100+ 行)
// 服务依赖 (20+ 行)
// 生命周期 (50+ 行)
// 核心协调逻辑 (300+ 行)
}
// photo_batch_mixin.dart - 约 300 行
// photo_swipe_mixin.dart - 约 150 行
// photo_undo_mixin.dart - 约 250 行
// photo_cache_mixin.dart - 约 200 行
// photo_delete_mixin.dart - 约 200 行
// photo_similar_mixin.dart - 约 300 行
// photo_utils_mixin.dart - 约 150 行
收益:
| 指标 | 重构前 | 重构后 |
|---|---|---|
| 单文件最大行数 | 2000+ | ~500 |
| 功能定位时间 | 需要搜索 | 直接找对应 Mixin |
| 代码复用 | 困难 | 可单独复用 Mixin |
| 测试难度 | 高 | 可针对单个 Mixin 测试 |
| 协作冲突 | 频繁 | 减少(不同人改不同文件) |
总结
Mixin 架构模式是拆分复杂 Controller 的有效方案:
- 职责分离:每个 Mixin 负责一个功能领域
- 状态共享:通过主控制器共享响应式状态
- 依赖明确:Mixin 开头声明所有依赖
- 渐进重构:可以逐步迁移,不需要一次性重写
- 易于维护:代码组织清晰,定位问题更快
这种模式特别适合:
- GetX Controller 超过 500 行
- 功能模块之间有明确边界
- 需要在多个 Controller 间复用某些功能
本文基于「回留」相册整理应用的实际重构经验。
分类:
标签:
版权申明
本文系作者 @MrZhang1899 原创发布在Mixin 架构模式 - 如何用 Mixin 拆分复杂 Controller。未经许可,禁止转载。
评论
-- 评论已关闭 --
全部评论