Flutter Mixin 架构模式:如何优雅拆分 2000+ 行的复杂 Controller

当你的 Controller 膨胀到 2000 行以上,代码变得难以维护时,Mixin 是一个优雅的解决方案。本文分享如何使用 Mixin 模式将复杂的 GetX Controller 拆分为多个职责单一的模块。

问题背景

在开发相册整理应用时,PhotoCleanController 承担了太多职责:

  • 照片列表管理和分组
  • 滑动手势处理
  • 撤销/重做功能
  • 缓存管理
  • 删除操作
  • 相似照片检测
  • 各种工具方法

最初这些代码都写在一个文件里,导致:

  1. 文件过长:超过 2000 行,难以阅读
  2. 职责混乱:不同功能的代码交织在一起
  3. 难以测试:无法单独测试某个功能模块
  4. 协作困难:多人修改同一文件容易冲突

为什么选择 Mixin?

在 Dart 中,拆分大类有几种方案:

方案 优点 缺点
继承 简单直接 单继承限制,耦合度高
组合 解耦彻底 需要大量委托代码,状态共享麻烦
Mixin 多重混入,状态共享方便 需要理解 Mixin 机制

Mixin 的优势在于:

  1. 可以混入多个:一个类可以 with 多个 Mixin
  2. 共享状态:Mixin 可以访问主类的属性和方法
  3. 职责分离:每个 Mixin 负责一个功能领域
  4. 渐进式重构:可以逐步将代码迁移到 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: 逐个迁移

从最独立的模块开始,逐个迁移:

  1. 创建 Mixin 骨架
  2. 声明依赖(需要的属性和方法)
  3. 迁移相关代码
  4. 在主控制器中 with 这个 Mixin
  5. 测试功能是否正常
  6. 重复以上步骤

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 的有效方案:

  1. 职责分离:每个 Mixin 负责一个功能领域
  2. 状态共享:通过主控制器共享响应式状态
  3. 依赖明确:Mixin 开头声明所有依赖
  4. 渐进重构:可以逐步迁移,不需要一次性重写
  5. 易于维护:代码组织清晰,定位问题更快

这种模式特别适合:

  • GetX Controller 超过 500 行
  • 功能模块之间有明确边界
  • 需要在多个 Controller 间复用某些功能

本文基于「回留」相册整理应用的实际重构经验。

分类: 标签:

评论

-- 评论已关闭 --

全部评论