flutter-apply-architecture-best-practices

flutter-apply-architecture-best-practices

热门

采用推荐的分层架构(UI 层、逻辑层、数据层)构建 Flutter 应用。适用于搭建新项目架构或进行可扩展性重构。

2783Star
163Fork
更新于 2026/8/5
SKILL.md
只读
名称
flutter-apply-architecture-best-practices
描述

采用推荐的分层架构(UI 层、逻辑层、数据层)构建 Flutter 应用。适用于搭建新项目架构或进行可扩展性重构。

Flutter 应用架构最佳实践

目录

架构分层

严格遵循“关注点分离”原则,将应用程序划分为独立的架构层。切勿将 UI 渲染与业务逻辑或数据获取混在一起。

UI 层(表示层)

采用 MVVM(Model-View-ViewModel)模式管理 UI 状态和逻辑。

  • Views(视图): 编写高复用、轻量化的 Widget。View 内部逻辑仅限 UI 相关操作(如动画、布局约束、简单路由)。ViewModel 所需的所有数据均由外部传入。
  • ViewModels(视图模型): 管理 UI 状态并处理用户交互。继承 ChangeNotifier(或使用 Listenable)来暴露状态。向 View 暴露不可变的状态快照。通过构造函数将 Repository 注入到 ViewModel 中。

Data 层(数据层)

采用 Repository(存储库)模式隔离数据访问逻辑,打造单一数据源(Single Source of Truth)。

  • Services(服务): 创建无状态类来封装外部 API(HTTP 客户端、本地数据库、平台插件)。返回原始 API 模型或 Result 包装对象。
  • Repositories(存储库): 调用一个或多个 Service。将原始 API 模型转换为干净的 Domain Model(领域模型)。负责处理缓存、离线同步和重试逻辑。向 ViewModel 暴露 Domain Model。

Logic 层(Domain 领域层 - 可选)

  • Use Cases(用例): 仅当应用包含较为复杂的业务逻辑(导致 ViewModel 过度膨胀),或者逻辑需要在多个 ViewModel 之间复用时,才需要引入该层。将这些逻辑提取到独立的 Use Case(交互器)类中,位于 ViewModel 与 Repository 之间。

项目结构

采用混合方式组织代码库:UI 组件按功能模块(feature)分组,Data/Domain 组件按类型分组。

lib/
├── data/
│   ├── models/         # API 模型
│   ├── repositories/   # Repository 实现
│   └── services/       # API 客户端、本地存储封装
├── domain/
│   ├── models/         # 干净的领域模型
│   └── use_cases/      # 可选的业务逻辑类
└── ui/
    ├── core/           # 共享 Widget、主题、排版
    └── features/
        └── [feature_name]/
            ├── view_models/
            └── views/

工作流:开发新功能

为应用添加新功能时,请遵循以下顺序工作流。可以复制此清单来跟踪进度。

任务进度清单

  • [ ] 步骤 1:定义 Domain Model。 使用 freezedbuilt_value 为该功能创建不可变的数据类。
  • [ ] 步骤 2:实现 Service。 创建或更新 Service 类,负责处理与外部 API 的通信。
  • [ ] 步骤 3:实现 Repository。 创建 Repository 类,调用 Service 并返回 Domain Model。
  • [ ] 步骤 4:根据条件选择逻辑(Domain 层)。
    • 如果该功能涉及复杂的数据转换或跨 Repository 逻辑: 创建 Use Case 类。
    • 如果该功能仅为简单的 CRUD 操作: 直接跳至步骤 5。
  • [ ] 步骤 5:实现 ViewModel。 创建继承自 ChangeNotifier 的 ViewModel。注入所需的 Repository/Use Case。暴露不可变状态与命令方法。
  • [ ] 步骤 6:实现 View。 创建 UI Widget。使用 ListenableBuilderAnimatedBuilder 监听 ViewModel 的变化。
  • [ ] 步骤 7:注入依赖。 在依赖注入容器(如 providerget_it)中注册新的 Service、Repository 和 ViewModel。
  • [ ] 步骤 8:运行校验。 为 ViewModel 和 Repository 执行单元测试。
    • 反馈循环: 运行测试 -> 检查失败项 -> 修复逻辑 -> 重新运行直到全部通过。

示例

Data 层:Service 与 Repository

// 1. Service(负责与原始 API 交互)
class ApiClient {
  Future<UserApiModel> fetchUser(String id) async {
    // HTTP GET 实现...
  }
}

// 2. Repository(单一数据源,返回 Domain Model)
class UserRepository {
  UserRepository({required ApiClient apiClient}) : _apiClient = apiClient;
  
  final ApiClient _apiClient;
  User? _cachedUser;

  Future<User> getUser(String id) async {
    if (_cachedUser != null) return _cachedUser!;
    
    final apiModel = await _apiClient.fetchUser(id);
    _cachedUser = User(id: apiModel.id, name: apiModel.fullName); // 转换为 Domain Model
    return _cachedUser!;
  }
}

UI 层:ViewModel 与 View

// 3. ViewModel(状态管理与表示层逻辑)
class ProfileViewModel extends ChangeNotifier {
  ProfileViewModel({required UserRepository userRepository}) 
      : _userRepository = userRepository;

  final UserRepository _userRepository;

  User? _user;
  User? get user => _user;

  bool _isLoading = false;
  bool get isLoading => _isLoading;

  Future<void> loadProfile(String id) async {
    _isLoading = true;
    notifyListeners();

    try {
      _user = await _userRepository.getUser(id);
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }
}

// 4. View(纯 UI 展示组件)
class ProfileView extends StatelessWidget {
  const ProfileView({super.key, required this.viewModel});

  final ProfileViewModel viewModel;

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: viewModel,
      builder: (context, _) {
        if (viewModel.isLoading) {
          return const Center(child: CircularProgressIndicator());
        }
        
        final user = viewModel.user;
        if (user == null) {
          return const Center(child: Text('User not found'));
        }

        return Column(
          children: [
            Text(user.name),
            ElevatedButton(
              onPressed: () => viewModel.loadProfile(user.id),
              child: const Text('Refresh'),
            ),
          ],
        );
      },
    );
  }
}