Flutter之旅 -- 项目架构

本篇文章主要介绍以下几个知识点:

  • 分层架构设计
  • Provider vs Riverpod
  • Riverpod 在实际项目中的使用示例
Flutter之旅

1. 分层架构设计

Android 应用架构指南:https://developer.android.com/topic/architecture?hl=zh-cn
使用 Riverpod 的 Flutter 应用架构指南:https://codewithandrea.com/articles/flutter-app-architecture-riverpod-introduction/

1.1 Flutter 分层架构概述

分层架构

基于 Clean Architecture 原则,Flutter 应用采用分层架构确保关注点分离:

┌─────────────────────────────────────┐
│         Presentation Layer          │  ← UI 层
├─────────────────────────────────────┤
│         Application Layer           │  ← 业务逻辑层
├─────────────────────────────────────┤
│           Domain Layer              │  ← 领域层
├─────────────────────────────────────┤
│            Data Layer               │  ← 数据层
└─────────────────────────────────────┘

主要层次包括:

  • data layer:(数据层)

    • 职责:数据获取、存储、网络请求
    • 组件:Repositories, Data Sources, APIs
    • 特点:处理数据的 CRUD 操作、抽象数据来源(网络、本地数据库)、数据转换和缓存策略
  • domain layer:(领域层)

    • 职责:业务实体、业务规则(构造model模型)
    • 组件:Models, Entities, Value Objects
    • 特点:纯 Dart 代码(不依赖 Flutter)、定义数据结构和业务规则
  • application layer:(应用层)
    • 职责:状态管理、业务流程编排
    • 组件:StateNotifier, Provider, Use Cases
    • 特点:协调不同领域服务、管理应用状态、处理用户操作的业务逻辑
  • presentation layer:(表现层)
    • 职责:UI 组件、页面、用户交互
    • 组件:Widgets, Pages, Dialogs
    • 特点:只负责 UI 渲染和用户交互、通过 Riverpod Provider 获取状态、不包含业务逻辑

1.2 与 Android 架构对比

对比 Android 应用架构
层级 Flutter + Riverpod Android MVVM
表现层 Widget + Consumer View + Activity/Fragment
应用层 StateNotifier + Provider ViewModel + LiveData/Flow
领域层 Domain Models Use Cases(网域层,可选)
数据层 Repository + DataSource Repository + Room/Retrofit

相似点:

  • 都遵循单向数据流
  • 都有明确的层级分离
  • 都使用观察者模式进行状态管理

差异点:

  • Flutter 使用 Riverpod 的依赖注入更加简洁、响应式编程更加直观
  • Android 领(网)域层是可选的,主要用于封装复杂业务逻辑或多个 ViewModel 共享的业务逻辑,对应 Flutter 中的应用层(项目中采用了 Riverpod 自动生成代码的方式时,也可以省略)。
  • Flutter 的领域层更多用于定义数据模型和业务实体(只针对本文的架构)

2. Provider vs Riverpod

Provider 和 Riverpod 这两个库的作者都是 Remi Rousselet,新库命名是旧库的字母重排。

Provider 的优点是 简单易用,上手难度低,适用于应用规模较小,状态管理不太复杂的场景。

Provider 的局限如下:

  • 依赖 BuildContext
    Provider 是基于 InheritedWidget 封装,读取状态需要 BuildContext,所以 只能在Widget树中声明使用
    而在有些场景下不一定能直接拿到 BuildContext,如在 非UI层 (如业务逻辑层) 访问状态,只能通过某种方式传递 BuildContext 实例,繁琐之余还增加了代码的耦合度。
    使用不当,还可能导致 ProviderNotFoundException

  • 多个相同类型的 Provider,需要自己维护一个 Key 进行区分。
    如:Widget 树的同一层级,为相同类型的状态创建多个同类型的 Provider,子 Widget 无法确定使用哪个 Provider 的数据,需要指定一个特定的 Key 来进行区分:

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => Counter(1), key: ValueKey(1)),
        ChangeNotifierProvider(create: (_) => Counter(2), key: ValueKey(2)),
      ],
      child: MyApp(),
    ),
  );
}

// 通过key指定使用Counter实例
Provider.of<Counter>(context, listen: false, key: ValueKey(1)).increment();
  • 如果需要跨 Widget 共享状态,Provider 就没法弄成局部私有的,只能是全局可访问的。

Riverpod 在 Provider 的基础上进行重构,解决上述问题之余,提供了 更灵活/精细的状态管理机制,状态不可变,编译时类型安全、易于测试等特性,更清晰的代码组织和维护方式 (注解代码生成),可以有效的组织和管理大规模的状态。

选择 Riverpod 的理由:

  1. 类型安全: 编译时检查,减少运行时错误
  2. 更好的性能: 精确的重建控制,避免不必要的 Widget 重建
  3. 简化的 API: 更直观的语法,减少样板代码
  4. 强大的开发工具: 更好的调试和开发体验
  5. 测试友好: 更容易进行单元测试和集成测试

3. Riverpod 基本使用与核心原理

3.1 Riverpod 基本使用

Riverpod 使用详解可参考:https://juejin.cn/post/7359402114018689076
官方文档:https://riverpod.dev/docs/introduction/why_riverpod

Riverpod 提供了多种状态管理模式,适用于不同的场景:

模式 适用场景 优点 缺点
StateProvider 简单状态 语法简单,直接修改 不适合复杂逻辑
StateNotifier 复杂状态 强类型,业务逻辑封装好 需要更多代码
FutureProvider 一次性异步 自动处理加载状态 不适合可变异步操作
StreamProvider 持续数据流 自动处理流状态 需要管理流的生命周期
ChangeNotifier 兼容旧代码 兼容性好 性能相对较差
Notifier 现代化状态管理 语法简洁,性能好 需要代码生成

3.2 Riverpod 核心原理

把应用的“数据源”和“依赖关系”想象成一张河网:上游的水质变化,会沿着支流层层传导到下游。
Riverpod 做的事情,就是把这张“依赖河网”用代码表达出来,并且自动完成“缓存、传导、重算、清理”。
这让复杂异步场景变得简单,正如官方所说:它是一个“响应式缓存框架”,专注于缓存与自动刷新。

  • 核心对象
    • ProviderContainer:一座“水库”,保存所有 provider 的缓存与依赖图,支持覆盖与作用域。
    • Provider/Family:一段“取水逻辑”的声明,描述 value 如何被计算,是否带参数(family)。
    • Ref(WidgetRef/Ref):提供读依赖、注册监听、生命周期回调(onDispose/keepAlive)的“水工”。
    • Listener:下游订阅者,只有当感兴趣的上游发生变化时才会被通知(select 精准过滤)。

一个极简(伪)实现:

// 极简容器:保存缓存与依赖
class MiniContainer {
  final Map<Object, dynamic> _cache = {};
  final Map<Object, Set<Object>> _deps = {}; // provider -> its dependencies

  T read<T>(MiniProvider<T> provider) {
    if (_cache.containsKey(provider)) return _cache[provider] as T;

    final tracker = _DependencyTracker(this, provider);
    final value = provider.create(tracker);
    _cache[provider] = value;
    _deps[provider] = tracker.dependencies;
    return value;
  }

  // 当依赖变更时,向下游传播“需要重算”的信号
  void markDirty(Object changed) {
    for (final entry in _deps.entries) {
      if (entry.value.contains(changed)) {
        _cache.remove(entry.key); // 使下游失效,下一次 read 时重算
        markDirty(entry.key);
      }
    }
  }
}

class _DependencyTracker {
  final MiniContainer container;
  final Object owner;
  final Set<Object> dependencies = {};
  _DependencyTracker(this.container, this.owner);

  T watch<T>(MiniProvider<T> dep) {
    dependencies.add(dep);
    return container.read(dep);
  }
}

class MiniProvider<T> {
  final T Function(_DependencyTracker ref) create;
  const MiniProvider(this.create);
}

上面伪代码展示了 Riverpod 的核心:

  • read 第一次会计算并缓存 value;
  • watch 让容器记录“谁依赖了谁”;
  • 当上游变化,容器递归失效下游缓存;
  • 下游在下次被读取/监听时自动重算。

4. 工作流实现

下面展示在实际项目中的工作流:

  • 模块目录结构
lib/
├── api/
│   └── api_service.dart                      # api 相关
|
├── features/
│   ├── air/
│       ├── application/                      # 应用层
│       │   └── providers.dart
│       ├── data/                             # 数据层
│       │   └── history_repository.dart       # 数据仓库
│       ├── domain/                           # 领域层
│       │   ├── history.dart                  # 数据模型
│       │   └── history.g.dart
│       └── presentation/                     # 表现层
│           ├── widget/                       # 通用组件
│           │   └── chart_bar.dart
|           ├── dialog/                       # 对话框组件
│           └── statistics_page.dart          # 统计UI页面
  • 定义 API 接口
/// lib/api/api.dart
class Api {
  static const String _path = '/dev/v1/';

  // 获取指定时间段的历史数据:/user/nodes/tsdata
  static const String historyData = "${_path}user/nodes/tsdata";
}

/// lib/api/api_service.dart
class ApiService {
  // 获取24小时平均值
  Future<Response> get24hoursData(
      {required String nodeId,
      required String paramName,
      String type = "float"}) async {
    var response = await _httpUtil
        .request(Method.get, Api.historyData, queryParameters: {
      "node_id": nodeId,
      "param_name": "$paramIdentifier.$paramName",
      "type": type,
      "aggregate": "avg",
      "aggregation_interval": "hour",
      "num_intervals": "24"
    });
    return response;
  }
}
  • 定义数据仓库(data layer)
/// lib/features/air/data/history_repository.dart
class HistoryRepository {
  final ApiService apiService;

  HistoryRepository({required this.apiService});

  // 获取24小时平均值
  Future<History> get24hoursData(
      {required String nodeId,
      required String paramName,
      String type = "float"}) async {
    var response = await apiService.get24hoursData(
        nodeId: nodeId, paramName: paramName, type: type);
    Log.d("get24hoursData: ${response.data}");
    return History.fromJson(response.data);
  }
}

/// Providers
final historyRepositoryProvider = Provider<HistoryRepository>((ref) {
  return HistoryRepository(apiService: ApiService.instance());
});
  • 定义数据模型(domain layer)
import 'package:json_annotation/json_annotation.dart';

part 'history.g.dart';

/// lib/features/air/domain/history.dart
@JsonSerializable()
class History {
  @JsonKey(name: "ts_data")
  final List<TsDatum> tsData;

  History({
    required this.tsData,
  });

  factory History.fromJson(Map<String, dynamic> json) => _$HistoryFromJson(json);

  Map<String, dynamic> toJson() => _$HistoryToJson(this);
}
  • 实现状态管理(application layer)
/// 状态定义
class AirState {
  final AsyncValue<History> data; 

  const AirState({
    this.data = const AsyncLoading(),
  });

  AirState copyWith({AsyncValue<History>? data}) {
    return AirState(data: data ?? this.data);
  }
}

/// 状态管理器
class AirNotifier extends StateNotifier<AirState> {
  final HistoryRepository _repository;
  final String _nodeId;

  AirNotifier(this._repository, this._nodeId) : super(const AirState());

  // 获取历史数据
  Future<History> getHistoryData() async {
    final result =  _repository.get24hoursData(_nodeId);
    if (mounted) {
        state = state.copyWith(data: result);
    }
    return result;
  }
}

// 历史数据状态管理器 provider
final airNotifierProvider = StateNotifierProvider.autoDispose
    .family<AirNotifier, AirState, String>((ref, nodeId) {
  final repository = ref.watch(historyRepositoryProvider);
  return AirNotifier(repository, nodeId);
});
  • UI 层实现(presentation layer)
/// lib/features/air/presentation/statistics_page.dart
class StatisticsPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(airNotifierProvider('node_id'));
    return state.when(
      data: (data) => HistoryView(state),
      loading: () => CircularProgressIndicator(),
      error: (err, stack) => Text('Error: $err'),
    );
  }
}

小结

这个工作流展示了完整的数据流向:

  1. API 定义 → 定义网络请求接口
  2. Repository 实现 → 处理数据获取、缓存、错误处理
  3. Domain 模型 → 定义业务数据结构
  4. StateNotifier → 管理复杂业务状态
  5. Provider 定义 → 提供依赖注入和状态访问
  6. UI 消费 → 响应式 UI 更新

数据流向:

UI (Consumer) 
  ↓ ref.watch()
Provider 
  ↓ StateNotifier
Application Layer 
  ↓ Repository
Data Layer 
  ↓ API/Database
External Data Source

5. 注意事项

  • Provider 生命周期管理
// ❌ 错误:不必要的长期持有
final expensiveProvider = Provider<ExpensiveService>((ref) {
  return ExpensiveService(); // 会一直存在内存中
});

// ✅ 正确:使用 autoDispose
final expensiveProvider = Provider.autoDispose<ExpensiveService>((ref) {
  final service = ExpensiveService();
  
  // 清理资源
  ref.onDispose(() {
    service.dispose();
  });
  
  return service;
});

// 需要时保持活跃
final cacheProvider = Provider.autoDispose<CacheService>((ref) {
  final cache = CacheService();
  
  // 在有数据时保持活跃
  if (cache.hasData) {
    ref.keepAlive();
  }
  
  return cache;
});
  • 避免过度监听
// ❌ 错误:监听整个复杂状态
class MyWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userState = ref.watch(userProvider); // 整个状态变化都会重建
    
    return Text(userState.user?.name ?? '');
  }
}

// ✅ 正确:只监听需要的部分
final userNameProvider = Provider<String?>((ref) {
  return ref.watch(userProvider.select((state) => state.user?.name));
});

class MyWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userName = ref.watch(userNameProvider); // 只有名字变化才重建
    
    return Text(userName ?? '');
  }
}

// 或者使用 select
class MyWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userName = ref.watch(
      userProvider.select((state) => state.user?.name)
    );
    
    return Text(userName ?? '');
  }
}
  • 正确使用 Family Provider
// ❌ 错误:Family Provider 参数过于复杂
final userProvider = StateNotifierProvider.family<UserNotifier, UserState, Map<String, dynamic>>((ref, params) {
  return UserNotifier(params['id'], params['config']);
});

// ✅ 正确:使用简单参数或自定义类
class UserParams {
  final String id;
  final UserConfig config;
  
  UserParams(this.id, this.config);
  
  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is UserParams &&
          runtimeType == other.runtimeType &&
          id == other.id &&
          config == other.config;

  @override
  int get hashCode => id.hashCode ^ config.hashCode;
}

final userProvider = StateNotifierProvider.family<UserNotifier, UserState, UserParams>((ref, params) {
  return UserNotifier(params.id, params.config);
});
  • 性能优化
// 使用 select 避免不必要的重建
final isLoadingProvider = Provider<bool>((ref) {
  return ref.watch(userProvider.select((state) => state.isLoading));
});

// 使用 Consumer 局部重建
class UserProfile extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // 静态内容不会重建
        const Text('用户信息'),
        
        // 只有这部分会根据状态变化重建
        Consumer(
          builder: (context, ref, child) {
            final user = ref.watch(userProvider.select((state) => state.user));
            return Text(user?.name ?? '未登录');
          },
        ),
        
        // 其他静态内容
        const SizedBox(height: 20),
      ],
    );
  }
}

6. 总结

  • 架构优势
  1. 清晰的分层结构: 每一层都有明确的职责,便于维护和测试
  2. 强类型安全: Riverpod 提供编译时检查,减少运行时错误
  3. 优秀的性能: 精确的重建控制,避免不必要的 UI 更新
  4. 易于测试: 依赖注入和状态隔离使测试变得简单
  5. 可扩展性: 模块化设计便于功能扩展和团队协作
  • 最佳实践
  1. 遵循分层原则: 确保每层只处理自己的职责
  2. 合理使用 Provider: 根据需求选择合适的 Provider 类型
  3. 注意生命周期: 使用 autoDispose 避免内存泄漏
  4. 优化性能: 使用 select 和 Consumer 减少不必要的重建
  5. 统一错误处理: 在合适的层级处理和转换错误
  6. 编写测试: 利用 Riverpod 的测试友好特性编写单元测试
  • 适用场景
    • 中大型 Flutter 应用
    • 需要复杂状态管理的应用
    • 团队协作开发的项目
    • 对性能和可维护性要求较高的应用

通过合理运用 Riverpod 和分层架构,可以构建出高质量、可维护、可测试的 Flutter 应用。关键是要理解每一层的职责,正确使用 Riverpod 的各种特性,并遵循最佳实践。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容