Files
flutter-template/docs/flutter-guide.md
T
SkyJourneyandClaude Sonnet 4.6 f194ac7d3f docs: 新增 flutter-guide 小白指南,深化 architecture 架构文档
- 新建 docs/flutter-guide.md(约 1600 行)
  - 第一章 Dart 核心:空安全、async/await、sealed class、代码生成
  - 第二章 Flutter 框架:三棵树、Widget 类型、生命周期、布局系统
    各节附 Android / iOS 原生等价对照
  - 第三章 12 个生态库逐一解析(Freezed 先于 Riverpod 的依赖顺序)
    含「平台生态对比总览」表 + 每库「📱 原生对比」callout
  - 第四章 项目核心组件深读,含「参见 3.x」交叉引用
  - 第五章 新增 Feature 标准流程与常见坑

- 增强 docs/architecture.md
  - 新增「架构设计原则」首节(DIP / SSOT / SoC),各附 Android / iOS 对比表
  - 全文 10 张 Mermaid 深度图(flowchart / sequenceDiagram / classDiagram)
  - 6 处「📱 原生对比」callout(OkHttp / Alamofire / Room / CoreData / actor 等)
  - 修复 Mermaid 语法:\n → <br/>,flowchart 中非法 <|..| → -.->,
    HTML 实体 &lt;T&gt; → «T»

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 15:56:50 +08:00

1269 lines
49 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Flutter 企业级开发指南(小白入门)
> 本文档面向**初次接触 Flutter / Dart 的开发者**,系统讲解语言基础、框架原理、生态库,
> 以及 sunny_mochi 脚手架中每个核心组件的设计原理和使用方式。
>
> **阅读建议**:按章顺序读一遍建立心智模型,之后按需跳阅第三、四章查阅具体库和组件。
> 如果你有 Android 或 iOS 原生开发背景,每节的"📱 原生对比"可以帮你快速建立映射关系。
---
## 目录
- [第一章:Dart 语言核心](#第一章dart-语言核心)
- [第二章:Flutter 框架原理](#第二章flutter-框架原理)
- [第三章:生态库逐一解析](#第三章生态库逐一解析)
- [第四章:项目核心组件深度解读](#第四章项目核心组件深度解读)
- [第五章:新增 Feature 的标准流程](#第五章新增-feature-的标准流程)
---
## 第一章:Dart 语言核心
> Dart 是 Flutter 的唯一编程语言。Flutter 框架的 Widget 系统、Riverpod 的响应式机制、
> Drift 的类型安全查询,底层都是 Dart 特性在发挥作用。读懂本章,后续所有库的用法
> 自然就能理解。
### 1.1 类型系统与空安全(Null Safety
Dart 是**强类型、静态类型**语言,从 Dart 2.12 起引入**空安全(Sound Null Safety**。
空安全意味着编译器在编译期就能捕获空指针错误,而不是等到运行时崩溃。
```dart
// 不允许为 null 的类型(默认)
String name = 'Alice';
// name = null; // ❌ 编译错误
// 允许为 null 的类型(加 ?)
String? nickname;
nickname = null; // ✅ 合法
// 使用前必须判空
if (nickname != null) {
print(nickname.length); // 自动 smart cast,不需要 !
}
// 非空断言(确信不为 null 时用,运行时失败会抛异常)
print(nickname!.length);
// 空值合并运算符
final display = nickname ?? '匿名用户';
// 可选链(null 时短路,不崩溃)
final len = nickname?.length;
```
**项目中的典型用法**
```dart
// failures.dart — cause 可为 null(某些错误没有原因对象)
const Failure({required this.message, this.cause, this.stackTrace});
final Object? cause;
// token_refresh_interceptor.dart — Completer 可为 null(未在刷新中)
Completer<bool>? _refreshing;
```
> **📱 原生对比**
> Kotlin 有完全相同的空安全语法(`?`、`!!`、`?.`、`?:`),Dart 的设计明显借鉴了它。
> Swift 用 `Optional<T>` / `T?` 实现相同语义,`guard let` 和 `if let` 等价于 Dart 的 `if (x != null)`。
---
### 1.2 异步编程(Future / Stream / async-await
Dart 是**单线程**的(与 JavaScript 相同),通过事件循环(Event Loop)处理并发。
`Future` 代表"将来某时的一个值",`Stream` 代表"将来某时的一系列值"。
```dart
// Future:一次性异步结果
Future<String> fetchToken() async {
await Future.delayed(Duration(seconds: 1)); // 模拟网络延迟
return 'my_token_abc123';
}
// await:等待 Future 完成(只能在 async 函数内使用)
final token = await fetchToken();
print(token); // my_token_abc123
// 错误处理
try {
final token = await fetchToken();
} on NetworkException catch (e) {
// 处理特定类型异常
} on Object catch (e, st) {
// 兜底(本项目标准写法)
}
// Stream:持续的数据流
Stream<int> counter() async* {
for (int i = 0; i < 3; i++) {
await Future.delayed(Duration(seconds: 1));
yield i; // 逐个发出值
}
}
// 监听 Stream
await for (final value in counter()) {
print(value); // 0, 1, 2
}
```
**`unawaited`**:启动 Future 但不等待它——通常用于"fire-and-forget"操作:
```dart
// main.dart — 不 await sync 启动,不阻塞 UI 渲染
unawaited(ref.read(syncServiceProvider).start());
```
**`Completer`**:手动控制 Future 的完成时机,本项目用于 Token 刷新互斥:
```dart
// token_refresh_interceptor.dart 精简版
Completer<bool>? _refreshing;
// 第一个进来的 401
final completer = Completer<bool>();
_refreshing = completer;
final result = await _runRefresh(token);
_refreshing = null;
completer.complete(result); // 通知所有等待者
// 后来的 401 直接等已有的 Future
final ok = await _refreshing!.future;
```
> **📱 原生对比**
> Android`suspend fun`Kotlin Coroutines)等价于 `async`/`await``Flow<T>` 等价于 `Stream<T>``Deferred<T>` 等价于 `Completer`。
> iOS`async/await`Swift 5.5+)语法与 Dart 几乎相同,`AsyncStream` 等价于 `Stream`Swift 的 `actor` 提供类似 `Completer` 的互斥能力。
---
### 1.3 Sealed Class、Extension、Mixin
#### Sealed ClassDart 3.0+
`sealed` 关键字让编译器知道子类集合是**封闭的**,在 `switch` 时强制穷举所有情况:
```dart
// failures.dart
sealed class Failure implements Exception {
const Failure({required this.message, this.cause, this.stackTrace});
final String message;
final Object? cause;
final StackTrace? stackTrace;
}
class NetworkFailure extends Failure { /* ... */ }
class AuthFailure extends Failure { /* ... */ }
class ServerFailure extends Failure { /* ... */ }
class CacheFailure extends Failure { /* ... */ }
class UnknownFailure extends Failure { /* ... */ }
```
使用时编译器确保你处理了所有情况:
```dart
switch (failure) {
case NetworkFailure(:final kind):
showToast('网络错误:$kind');
case AuthFailure():
router.go('/login');
case ServerFailure(:final code):
showToast('服务器错误 $code');
case CacheFailure():
showToast('本地数据异常');
case UnknownFailure():
showToast('未知错误');
}
// 如果少写任何一个 case → 编译错误!
```
> **📱 原生对比**
> Kotlin 有完全相同的 `sealed class`Dart 的 `sealed` 语义来自 Kotlin)。
> Swift 用带关联值的 `enum` 实现同等效果:`enum Failure { case network(kind: NetworkKind), case auth, ... }``switch` 同样强制穷举。
#### Extension(扩展方法)
为**已有类型**添加方法,无需继承,无需修改源码:
```dart
extension ContextX on BuildContext {
void showError(WidgetRef ref, Object error, StackTrace st) {
// 统一展示错误 + 写日志
}
}
// 使用时就像原生方法一样
context.showError(ref, failure, st);
```
> **📱 原生对比**
> Kotlin 有同名的 `extension function`,语法略有不同。iOS Swift 的 `extension` 关键字功能完全相同,是 Swift 的一等公民特性。
#### Mixin
在不同类之间复用方法,比继承更灵活(一个类可以 mixin 多个):
```dart
mixin SmsCountdownMixin on State<StatefulWidget> {
int _seconds = 0;
void startCountdown() { /* 60s 倒计时逻辑 */ }
}
class _LoginPageState extends ConsumerState<LoginPage> with SmsCountdownMixin {
// 直接使用 startCountdown() 和 _seconds
}
```
> **📱 原生对比**
> Kotlin 用 `interface` + `default implementation` 实现类似效果(Kotlin 没有 mixin 关键字)。
> Swift 的 `protocol` + `extension` 组合是完整等价物:在 `extension` 里为 protocol 提供默认实现,`class` 遵循 protocol 即可复用。
---
### 1.4 代码生成(build_runner / @riverpod / @freezed
Dart 通过**注解 + 代码生成**消除样板代码。项目用到三套:
| 注解 | 生成器 | 生成文件 | 作用 |
|------|--------|---------|------|
| `@riverpod` | riverpod_generator | `.g.dart` | Provider 工厂函数 |
| `@freezed` | freezed | `.freezed.dart` | 值对象 + copyWith + JSON |
| `@TypedGoRoute` | go_router_builder | `.g.dart` | 类型安全路由类 |
| `@DriftDatabase` | drift_dev | `.g.dart` | SQL 查询代码 |
**运行代码生成**
```bash
# 一次性生成(修改 .dart 文件后必跑)
make gen
# 等价于
flutter pub run build_runner build --delete-conflicting-outputs
```
> **黄金法则**:永远不要手动修改 `.g.dart` 或 `.freezed.dart` 文件——它们在下次 `make gen` 时会被覆盖。
> **📱 原生对比**
> AndroidKSPKotlin Symbol Processing+ Room/Hilt/Retrofit 注解处理器,与 build_runner 机制完全相同。
> iOSSwift 宏(Swift 5.9+)提供类似能力;传统方案是 Sourcery(代码生成工具)或 Codegen 脚本。
---
## 第二章:Flutter 框架原理
> **从原生视角理解 Flutter**
> Android 开发者:Flutter 的 Widget 树对应 Android 的 View 树,但 Widget 是**不可变的配置描述**
> 而不是 View 实例本身。Flutter 自己调用 Skia/Impeller 绘制,不复用平台原生控件。
> iOS 开发者:Flutter 的 Widget 对应 SwiftUI 的 `View`(结构体,不可变,声明式)。
> UIKit 的 `UIViewController` 生命周期对应 `StatefulWidget` 的 `State`。
### 2.1 三棵树:Widget / Element / RenderObject
Flutter 渲染界面靠三层树协同工作:
```mermaid
flowchart TD
W["Widget 树<br/>(配置描述,不可变,轻量)"]
E["Element 树<br/>(实例持有者,持久化,生命周期载体)"]
R["RenderObject 树<br/>(真正测量/绘制,昂贵)"]
W -->|"首次构建: createElement()"| E
E -->|"首次挂载: createRenderObject()"| R
W -->|"rebuild: didUpdateWidget()"| E
E -->|"布局/绘制变化: markNeedsLayout()"| R
style W fill:#4A90D9,color:#fff
style E fill:#27AE60,color:#fff
style R fill:#E67E22,color:#fff
```
- **Widget**:描述"我想要什么"(配置),是不可变对象,每次 `setState` / `ref.watch` 都可能重建
- **Element**Widget 的实例,负责"连接新旧 Widget"并决定是否需要更新 RenderObject
- **RenderObject**:实际执行测量(`performLayout`)和绘制(`paint`),创建/销毁成本高
**关键洞察**Widget 重建(rebuild**≠** 界面重绘。Flutter 足够聪明,当新旧 Widget 类型和 key 相同时,只更新 Element 和 RenderObject 中变化的部分。
> **📱 原生对比**
> AndroidView 树只有一层,View 对象同时承担"配置"和"渲染"两个职责,这也是 Android View 系统性能问题的根源之一。Jetpack Compose 的设计与 Flutter 更接近(声明式、不可变配置 + 运行时状态树)。
> iOS UIKitUIView 同样是"配置+渲染"合一;SwiftUI 的 `View` 协议(结构体)则和 Flutter Widget 概念完全对齐——轻量不可变的配置,框架自己管理渲染树。
---
### 2.2 三种 Widget 类型
| Widget 类型 | 何时使用 | 本项目用法 | Android 类比 | iOS 类比 |
|-------------|---------|-----------|------------|--------|
| `StatelessWidget` | 纯展示,无状态 | `AppRoot`、大多数展示组件 | `@Composable` fun(无状态)| SwiftUI `View`(纯 `let` 属性)|
| `StatefulWidget` | 有本地状态(动画、表单) | `_SyncBootstrap` | `@Composable` + `remember {}` | `@State` 属性 |
| `ConsumerWidget` | 需要读取 Riverpod Provider | `App`、功能页面 | `@Composable` + `viewModel()` | `@ObservedObject` / `@EnvironmentObject` |
| `ConsumerStatefulWidget` | 同时需要本地状态和 Provider | `LoginPage` | `@Composable` + `remember` + `viewModel()` | `@StateObject` + `@State` |
```dart
// ConsumerWidget 是最常用的 Widget 类型(来自 flutter_riverpod
class App extends ConsumerWidget {
const App({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// ref 用于读取/监听 Provider
final router = ref.watch(appRouterProvider);
final themeMode = ref.watch(themeModeProvider);
final scheme = ref.watch(themeProvider);
return MaterialApp.router(
routerConfig: router,
themeMode: themeMode,
theme: AppTheme.light(scheme: scheme),
darkTheme: AppTheme.dark(scheme: scheme),
);
}
}
```
---
### 2.3 BuildContext 与 InheritedWidget
`BuildContext` 是 Widget 在 Element 树中的位置标识,通过它可以:
```dart
// 向上查找最近的 InheritedWidgetMediaQuery、Theme 都是 InheritedWidget
final mediaQuery = MediaQuery.of(context);
final theme = Theme.of(context);
// 导航(GoRouter
context.go('/home');
context.push('/detail');
// 显示 SnackBar / Dialog
ScaffoldMessenger.of(context).showSnackBar(SnackBar(...));
showDialog(context: context, builder: (_) => AlertDialog(...));
```
**注意**`BuildContext` 只在 `build()` 方法内可靠使用。在异步回调中使用时,需先检查 `mounted`
```dart
Future<void> onTap() async {
await someAsyncOp();
if (!mounted) return; // Widget 可能已卸载
context.go('/next');
}
```
> **📱 原生对比**
> Android`Context` 是 Android 的核心概念,用于访问资源、启动 Activity、获取系统服务——与 `BuildContext` 功能重叠但范围更广(Android Context 贯穿整个 App 生命周期,BuildContext 只在 Widget 树范围内有效)。
> iOS:没有直接等价物;SwiftUI 的 `Environment` 和 `EnvironmentValues` 实现了类似的"向上查找"机制(`@Environment(\.colorScheme) var colorScheme`)。
---
### 2.4 生命周期
```mermaid
stateDiagram-v2
[*] --> Created : new Widget()
Created --> Mounted : createState() + initState()
Mounted --> Built : build()
Built --> Rebuilt : setState() / ref.watch 变化
Rebuilt --> Built : build()
Built --> Disposed : 离开 Widget 树
Disposed --> [*]
note right of Mounted : initState() 只调用一次<br/>可以启动 Animation、订阅 Stream
note right of Disposed : dispose() 必须释放资源<br/>Timer、StreamSubscription、Controller
```
> **📱 原生对比**
> Android Activity/Fragment`onCreate → onStart → onResume → onPause → onStop → onDestroy`。`initState()` ≈ `onCreate()``dispose()` ≈ `onDestroy()`。
> iOS UIViewController`viewDidLoad → viewWillAppear → viewDidAppear → viewWillDisappear → viewDidDisappear → deinit`。`initState()` ≈ `viewDidLoad()``dispose()` ≈ `deinit`。
---
### 2.5 布局系统
Flutter 布局的核心约束是:**父 Widget 向子 Widget 传入约束(constraints),子 Widget 在约束范围内决定自己的尺寸**。
```
父约束向下传递:
BoxConstraints(minW=0, maxW=375, minH=0, maxH=812)
子 Widget 决定尺寸:
Size(200, 50)
子尺寸向上返回
```
| Widget | 用途 | Android 类比 | iOS 类比 |
|--------|------|------------|--------|
| `Column` / `Row` | 垂直/水平排列 | `LinearLayout` (vertical/horizontal) | `VStack` / `HStack` |
| `Expanded` / `Flexible` | 按比例分配空间 | `layout_weight` | `.frame(maxWidth: .infinity)` |
| `Stack` / `Positioned` | 绝对定位层叠 | `FrameLayout` / `ConstraintLayout` | `ZStack` / `.offset` |
| `SizedBox` | 固定尺寸/间距 | `Space` / `View(match_parent)` | `Spacer` / `.frame(width:height:)` |
| `ListView` / `GridView` | 可滚动列表/网格 | `RecyclerView` | `List` / `LazyVGrid` |
| `CustomScrollView` + `Sliver` | 复杂滚动(吸顶、动态 Header)| `CoordinatorLayout` + `NestedScrollView` | `ScrollView` + `LazyVStack` |
---
## 第三章:生态库逐一解析
> **学习建议**:本章库的讲解顺序经过设计——**Freezed3.1)先于 Riverpod3.2**
> 因为 Riverpod 的状态类(`sealed AuthState`)都是 Freezed 生成的数据类,
> 理解 Freezed 后再看 Riverpod 代码会豁然开朗。
### 平台生态对比总览
| 功能 | Flutter 库 | Android 官方/主流 | iOS 官方/主流 |
|------|-----------|-----------------|-------------|
| 值对象/数据类 | Freezed | `data class` + kotlinx.serialization | `Codable` (Decodable/Encodable) |
| 状态管理 | Riverpod | ViewModel + StateFlow (AAC) | `@StateObject` + Combine |
| 依赖注入 | Riverpod ProviderScope | Hilt / Dagger 2 | Swinject / Factory(三方)|
| 路由 | GoRouter | Jetpack Navigation Component | NavigationPath (SwiftUI) / Coordinator |
| HTTP 客户端 | Dio | OkHttp + Retrofit | URLSession + Alamofire(三方)|
| ORM / 数据库 | Drift | Room | Core Data / GRDB(三方)|
| 数据库加密 | SQLCipher | SQLCipher for Android / EncryptedRoom | SQLCipher for iOS |
| 安全存储 | flutter_secure_storage | EncryptedSharedPreferences + Keystore | Keychain Services |
| 日志 | Talker | Timber + Logcat | OSLog / CocoaLumberjack(三方)|
| 错误追踪 | Sentry | Firebase Crashlytics / Sentry Android | Firebase Crashlytics / Sentry iOS |
| 国际化 | Slang | String Resources (res/values/strings.xml) | Localizable.strings + SwiftGen(三方)|
| 屏幕适配 | flutter_screenutil | dp/sp + ConstraintLayout 百分比 | Auto Layout + Size Classes |
| RSA 加密 | pointycastle + asn1lib | JCE (`javax.crypto`) / Android Keystore | Security framework / CryptoKit |
---
### 3.1 Freezed —— 值对象与联合类型
> **为什么先讲 Freezed**:本项目里 Riverpod 的状态类(`AuthState`)、所有实体(`UserEntity`)、
> 所有网络模型(`UserModel`)都是 Freezed 生成的。理解 Freezed 是理解后续所有 `@riverpod` 代码的前提。
**是什么**:代码生成库,为 Dart 类自动生成 `==``hashCode``copyWith``toString``toJson`/`fromJson`,并支持联合类型(Union Types / Discriminated Union)。
**解决什么问题**Dart 原生没有 `data class`,手写 `==``copyWith` 极其繁琐且容易出错。
```dart
// user_entity.dart — 值对象
@freezed
class UserEntity with _$UserEntity {
const factory UserEntity({
required String userId,
String? username,
String? phone,
String? avatar,
}) = _UserEntity;
}
// 自动生成的能力
final user = UserEntity(userId: 'u1', username: 'Alice');
final updated = user.copyWith(username: 'Bob'); // 不变性
print(user == updated); // false(值比较,不是引用比较)
// auth_state.dart — 联合类型(状态机)
@freezed
sealed class AuthState with _$AuthState {
const factory AuthState.initial() = _Initial;
const factory AuthState.loading() = _Loading;
const factory AuthState.authenticated(UserEntity user)= _Authenticated;
const factory AuthState.unauthenticated() = _Unauthenticated;
const factory AuthState.error(String message) = _Error;
}
// 使用时穷举所有状态(编译期安全)
switch (state) {
case AuthState.loading():
return CircularProgressIndicator();
case AuthState.authenticated(:final user):
return Text('欢迎,${user.username}');
case AuthState.error(:final message):
return Text('错误:$message');
// ...
}
```
> **📱 原生对比**
> **Android**Kotlin 的 `data class` 原生提供 `equals`/`hashCode`/`copy`(对应 `copyWith`),`sealed class` 原生支持穷举 `when`——Freezed 的功能在 Kotlin 里几乎是语言内置的。
> **iOS**Swift 的 `struct` + `Codable` 提供值语义和 JSON 序列化;`enum` with associated values 提供联合类型。Swift 没有内置 `copyWith`,通常手写或用 `@dynamicMemberLookup` 变通。
---
### 3.2 Riverpod —— 响应式状态管理
**是什么**:Flutter 社区最主流的状态管理方案,基于"Provider"概念,解决了原 Provider 包的诸多痛点(类型安全、测试隔离、自动处理异步等)。它同时承担**状态管理**和**依赖注入**两个职责。
**解决什么问题**:在多个 Widget 之间共享状态,避免"状态提升"导致的层层传递 `callback`,同时保证 UI 响应式更新。
```dart
// 定义 Provider(以主题为例)
@Riverpod(keepAlive: true)
class ThemeNotifier extends _$ThemeNotifier {
@override
AppColorScheme build() => AppColorScheme.blue; // 初始值
void setScheme(AppColorScheme s) => state = s;
}
// 使用 Provider(在 ConsumerWidget 中)
class MyPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// watch:监听变化,变化时重建 Widget
final scheme = ref.watch(themeProvider);
// read:只读一次,不监听变化(用于事件回调)
ref.read(themeNotifierProvider.notifier).setScheme(AppColorScheme.red);
// listen:监听变化,执行副作用(导航、Toast)
ref.listen(themeProvider, (prev, next) {
print('主题变了: $next');
});
}
}
```
**Provider 类型对照**
| 注解写法 | 生成的 Provider | 适合场景 |
|---------|----------------|---------|
| `@riverpod Future<T> foo(Ref)` | `FutureProvider<T>` | 异步一次性数据(网络请求) |
| `@riverpod Stream<T> foo(Ref)` | `StreamProvider<T>` | 持续数据流(WebSocket |
| `@riverpod T foo(Ref)` | `Provider<T>` | 同步只读计算值 |
| `class Foo extends _$Foo` | `NotifierProvider` | 可变状态 + 方法 |
| `@Riverpod(keepAlive: true)` | 永不销毁的 Provider | 全局单例(路由、DB、Dio 实例) |
> **📱 原生对比**
> **Android**ViewModelAAC)提供类似"状态容器"`StateFlow<T>` 对应 `AsyncValue<T>`Riverpod 的异步状态包装),`viewModels()` / Hilt `@Inject` 对应 `ref.watch/read`。Riverpod 的 `ProviderScope` ≈ Hilt 的 `@HiltAndroidApp`(应用级依赖注入容器)。
> **iOS**`@StateObject` + `ObservableObject`/`@Published` 对应 Riverpod Notifier`@EnvironmentObject` 对应 `keepAlive: true` 的全局 ProviderCombine `Publisher` 对应 `StreamProvider`。TCAThe Composable Architecture)是功能上更完整的对标方案。
---
### 3.3 GoRouter —— 声明式路由
**是什么**:Flutter 官方推荐的路由库,支持深链接(Deep Link)、Web URL 路由、类型安全路由、权限守卫(redirect)。
**解决什么问题**:原生 `Navigator.push/pop` 无法用 URL 描述路由状态,深链接处理复杂,无法做全局路由守卫。
```dart
// routes.dart — 类型安全路由定义
@TypedGoRoute<LoginRoute>(path: '/login')
class LoginRoute extends GoRouteData with $LoginRoute {
const LoginRoute();
@override
Widget build(BuildContext context, GoRouterState state) => const LoginPage();
}
// 跳转(类型安全,不再用字符串)
LoginRoute().go(context); // 替换当前页面
LoginRoute().push(context); // 压栈
// app_router.dart — 全局路由守卫
GoRouter(
refreshListenable: auth.listenable, // 登录态变化时重新执行 redirect
redirect: (context, state) {
final loggedIn = auth.value;
if (loggedIn == null) return null; // 启动中,不动
if (!loggedIn) return '/login'; // 未登录,跳登录
if (loggedIn && state.matchedLocation == '/login') return '/home';
return null;
},
)
```
> **📱 原生对比**
> **Android**Jetpack Navigation Component + Safe Args(类型安全参数传递)是直接对标;`NavController.navigate()` ≈ `context.go()`Navigation Graph XML 的全局 action ≈ GoRouter `redirect`。
> **iOS**SwiftUI 的 `NavigationPath` + `.navigationDestination(for:destination:)` 是声明式路由的原生方案;UIKit 的 `UINavigationController` + Coordinator pattern 是更成熟的命令式替代。
---
### 3.4 Dio —— HTTP 客户端
**是什么**Flutter 最流行的 HTTP 客户端,支持拦截器链、文件上传、取消请求、FormData 等。
**解决什么问题**:原生 `http` 包功能简单,Dio 提供完整的中间件(拦截器)机制,可以统一处理 Token 注入、错误映射、重试等横切关注点。
**Options.extra 信号机制**(本项目独特设计):
拦截器之间通过 `RequestOptions.extra` Map 传递信号,避免拦截器之间直接耦合:
```dart
// 登录接口调用时设置,告知 AuthInterceptor 跳过 Token 注入
dio.post('/auth/login', options: Options(extra: {'skip_auth': true}));
// TokenRefreshInterceptor 防止无限重试:
// 重放请求时打标记
err.requestOptions.extra['_token_refreshed'] = true;
// 下一次进入拦截器时检查
final retried = err.requestOptions.extra['_token_refreshed'] == true;
if (retried) return handler.next(err); // 不再重试
```
> **📱 原生对比**
> **Android**OkHttp 的 `Interceptor` 接口与 Dio 拦截器机制几乎完全相同(连"应用层拦截器 vs 网络层拦截器"的分类思路都一致)。Retrofit 基于 OkHttp 提供注解式 API 定义。`OkHttpClient.Builder().addInterceptor()` 对应 `dio.interceptors.add()`。
> **iOS**URLSession 原生没有拦截器链;Alamofire 的 `RequestInterceptor``adapt` + `retry`)和 `EventMonitor` 提供等价能力;Moya(基于 Alamofire)提供更高层的抽象。
---
### 3.5 Drift —— 类型安全 ORM
**是什么**Flutter/Dart 的类型安全 SQLite ORM,通过代码生成将 Dart 类映射为数据库表,查询语句在编译期检查。
**解决什么问题**:直接写 SQL 字符串没有类型检查,容易出现拼写错误和运行时崩溃。Drift 让你用 Dart 类型化的 API 写查询。
```dart
// 表定义(users_table.dart
class Users extends Table {
TextColumn get userId => text()(); // TEXT NOT NULL
TextColumn get username => text().nullable()(); // TEXT NULL
IntColumn get gender => integer().nullable()();
}
// DAO(数据访问对象)
@DriftAccessor(tables: [Users])
class UsersDao extends DatabaseAccessor<AppDatabase> with _$UsersDaoMixin {
Future<User?> findById(String userId) =>
(select(users)..where((t) => t.userId.equals(userId)))
.getSingleOrNull(); // 编译期类型安全
Future<void> upsert(UsersCompanion companion) =>
into(users).insertOnConflictUpdate(companion);
}
```
**后台 Isolate**Drift 的 `NativeDatabase.createInBackground` 在独立 Isolate 中运行数据库操作,不阻塞 UI 线程:
```dart
// app_database.dart
NativeDatabase.createInBackground(
file,
isolateSetup: _sqlCipherIsolateSetup, // 必须是顶层函数!
setup: (db) {
db.execute("PRAGMA key = '$escaped';"); // 解锁加密
},
)
```
> **📱 原生对比**
> **Android**:Room 是直接对标——同样注解式、同样代码生成 DAO、同样禁止主线程 IORoom 默认在 IO Dispatcher 执行)。`@Entity` ≈ Drift `Table``@Dao` ≈ `@DriftAccessor``@Database` ≈ `@DriftDatabase`。
> **iOS**Core Data 是官方 ORM(基于 NSManagedObject,面向对象风格);GRDB.swift(三方)更接近 Drift 的风格(类型安全 SQL)。两者都支持后台 `managedObjectContext` 避免主线程阻塞。
---
### 3.6 Flutter Secure Storage —— 安全存储
**是什么**:跨平台安全键值存储,iOS 使用 KeychainAndroid 使用 EncryptedSharedPreferencesAES-256 + Android Keystore)。
**解决什么问题**:普通 `SharedPreferences` 存储 Token 在 Android 上明文可读(rooted 设备),Secure Storage 的加密存储符合安全规范。
```dart
// 零容忍规则:Token 只能从 SecureStorage 读写,禁止存 SharedPreferences
class SecureStorageService {
Future<String?> getToken() => _storage.read(key: _kToken);
Future<void> setToken(String token) => _storage.write(key: _kToken, value: token);
Future<void> clearAll() => _storage.deleteAll(); // 退出登录时调用
}
```
> **📱 原生对比**
> **Android**`EncryptedSharedPreferences`Jetpack Security)直接对标,底层用 Android Keystore + AES-256-GCM;也可直接操作 Android Keystore 存证书/密钥。
> **iOS**Keychain Services 是原生安全存储,`SecItemAdd` / `SecItemCopyMatching` 是底层 API;大多数项目用 `KeychainAccess`(三方)简化调用。
---
### 3.7 Talker —— 结构化日志
**是什么**:Flutter 的结构化日志库,支持按级别过滤(verbose/debug/info/warning/error)、颜色输出、集成 Dio 和 Riverpod 的自动日志。
**解决什么问题**`print()` 没有级别、没有颜色、Release 包无法关闭,会泄露敏感信息。
```dart
// 使用(零容忍:禁止 print/debugPrint
appTalker.info('App 启动');
appTalker.warning('[TokenRefresh] 刷新失败', error, stackTrace);
appTalker.error('[Database] 打开失败', error, stackTrace);
appTalker.verbose('[Router] path=/home loggedIn=true');
// Release 包自动关闭,Dev Panel 中可查看历史日志
```
> **📱 原生对比**
> **Android**Timber(三方)是最流行的日志库,`Timber.d/w/e()` 对应 Talker 的各级别,`DebugTree` vs `ReleaseTree` 实现 Release 环境静默——与 Talker 设计理念完全一致。
> **iOS**`OSLog`(官方,iOS 14+)是结构化日志的现代方案;`CocoaLumberjack`(三方)提供更丰富的级别过滤和文件输出,功能上更接近 Talker。
---
### 3.8 Sentry —— 云端错误追踪
**是什么**:错误监控 SaaS,将 App 崩溃和异常上报到云端,提供错误聚合、影响用户数统计、堆栈还原(ProGuard / dSYM)。
**本项目重点:PII 脱敏**
```dart
// sentry_setup.dart — beforeSend 钩子过滤敏感信息
SentryFlutter.init(
(options) {
options.beforeSend = (event, hint) => _sanitize(event);
// 手机号脱敏、Token 完全移除、身份证号脱敏
},
);
// DSN 留空 = 不上报(dev 环境默认不上报)
```
> **📱 原生对比**
> **Android / iOS**Firebase Crashlytics 是最主流的替代(Google 出品,免费,与 Firebase 生态集成深);Sentry 同样提供 Android SDK 和 iOS SDK,功能对等。两者都支持 `beforeSend` 钩子脱敏。
---
### 3.9 Slang —— 编译期 i18n
**是什么**:基于代码生成的国际化方案,翻译文件(JSON)在编译期转为类型安全的 Dart 类,拼错翻译键名会在编译期报错。
```dart
// 使用(context.t 是类型安全的)
Text(context.t.auth.login) // "登录"
Text(context.t.auth.phoneHint) // "请输入手机号"camelCase 自动转换)
// 修改翻译后:make gen-i18n
```
> **📱 原生对比**
> **Android**String Resources`res/values/strings.xml`)是官方方案,通过 `R.string.xxx` 访问(编译期检查)。无需三方库,但键名是字符串常量,IDE 支持弱于 Slang 的类型链式访问。
> **iOS**`Localizable.strings` + `NSLocalizedString("key", comment:)` 是传统方案;SwiftGen(三方)与 Slang 最接近,从资源文件生成类型安全的 Swift 访问器。
---
### 3.10 flutter_screenutil —— 屏幕适配
**是什么**:基于设计稿尺寸自动缩放 UI 元素,确保在不同屏幕尺寸的设备上界面比例一致。
**本项目配置**:设计基准 375×812(与 iOS 6s 逻辑分辨率相同),`minTextAdapt: true` 防止文字在小屏上过小。
```dart
// 使用
Container(
width: 200.w, // 200 / 375 * 屏幕宽度
height: 50.h, // 50 / 812 * 屏幕高度
padding: EdgeInsets.all(16.r), // 按最小边缩放
child: Text('登录', style: TextStyle(fontSize: 16.sp)),
)
```
> **📱 原生对比**
> **Android**dpdensity-independent pixels+ spscale-independent pixels)是系统内置适配方案;ConstraintLayout 的百分比约束和 `guideline` 实现比例布局。无需三方库。
> **iOS**Auto Layout + Size Classes 是官方适配方案(设置 leading/trailing/top/bottom 约束相对于 Safe Area);SnapKit(三方)简化约束书写,功能上更贴近 ScreenUtil 的开发体验。
---
### 3.11 SQLCipher —— 数据库加密
**是什么**SQLite 的加密扩展,在 SQLite 基础上增加 AES-256 全库加密,文件在文件系统层面不可读。
**与 Drift 集成**
```
普通 SQLite 裸库
↓ 替换为
SQLCipherlibsqlcipher.so / SQLCipher.xcframework
↓ 在 setup 回调中解锁
PRAGMA key = 'your_key';
↓ 之后所有操作透明加密
Drift ORM(正常使用,感知不到加密层)
```
**关键坑**Drift 后台 Isolate 的 setup 函数必须是**顶层函数**(不能是 lambda),否则 Isolate 间序列化失败,加密静默不生效。
> **📱 原生对比**
> **Android**`SQLCipher for Android`(同一家公司的 SDK)可直接替换 `android.database.sqlite`Jetpack Security 的 `EncryptedFile` + Room 组合提供另一种加密路径(实验性,不如 SQLCipher 成熟)。
> **iOS**`SQLCipher for iOS`(同一 SDK 体系);Core Data 不原生支持加密,需要通过 SQLCipher 替换底层存储或使用 SQLite 层加密。
---
### 3.12 pointycastle + asn1lib —— RSA 加密传输
**是什么**:纯 Dart 密码学库,`pointycastle` 提供 RSA/AES 等算法实现,`asn1lib` 解析 ASN.1/DER 格式的密钥。
**本项目用途**:登录密码在客户端用服务端公钥加密后传输,防止明文密码在网络中传输(即使 HTTPS 中间人攻击场景下也有额外保护层)。
```dart
// rsa_helper.dart
// 失败时抛 RsaEncryptionException(禁止 fallback 明文,零容忍)
static String encrypt(String plainText, String spkiBase64) {
// Base64 解码 → DER → ASN.1 解析 → RSA 公钥 → PKCS#1 v1.5 加密 → Base64 输出
}
```
> **📱 原生对比**
> **Android**`javax.crypto`JCE+ `java.security`JCA)是 JVM 原生加密 APIAndroid Keystore System 提供硬件级密钥保护。Bouncy Castle 是最流行的三方密码学库(pointycastle 是其 Dart 移植版)。
> **iOS**`Security` framework`SecKey`、`SecKeyCreateWithData`)处理 RSA 操作;Swift 5.5+ 的 `CryptoKit` 提供更现代的 API(但 RSA PKCS#1 v1.5 不在 CryptoKit 支持范围,需用 Security framework)。
---
## 第四章:项目核心组件深度解读
> 本章是第三章各库的**项目级集成讲解**——从单个库的"怎么用"升级到"为什么这样组合"。
> 每节会标注"参见第三章 X.X"以便回查库的基础知识。
### 4.1 启动序列(main.dart
(参见:3.7 Talker、3.8 Sentry、3.5 Drift、3.2 Riverpod
```mermaid
sequenceDiagram
participant M as main()
participant CR as CrashReporter
participant S as SentrySetup
participant DB as AppDatabase
participant RS as ProviderScope
participant SB as _SyncBootstrap
M->>CR: preInit()(准备崩溃写入路径)
CR-->>M: pendingCrash(上次崩溃数据)
M->>S: init(appRunner)(空 DSN 直接执行 appRunner
S->>CR: installHooks()(接管 FlutterError + Zone 异常)
S->>DB: open()AES-256 解锁 SQLite
DB-->>S: AppDatabase 实例
S->>RS: runApp(ProviderScope(...))
RS->>SB: initState()
SB->>SB: syncService.start()(后台同步,unawaited
SB->>SB: errorLogger.pruneOld()(清理旧日志,unawaited
```
**为什么顺序不能颠倒**
1. `CrashReporter.preInit()` 必须最早——它准备崩溃文件写入路径,后续任何阶段崩溃都能被捕获
2. `SentrySetup.init()` 包裹整个 `appRunner`——确保 Sentry 捕获到启动期异常
3. `AppDatabase.open()` 必须在 `ProviderScope` 之前——`appDatabaseProvider``overrideWithValue` 注入,需要实例准备好
---
### 4.2 七层拦截器网络栈
(参见:3.4 Dio 拦截器链)
```mermaid
flowchart TD
REQ(["📤 Dio.request()<br/>业务代码发起请求"])
subgraph Chain["拦截器链(onRequest 从上到下,onError 从下到上)"]
direction TB
I1["[1] CertPinningInterceptor<br/>🔐 TLS 指纹校验<br/>PINNED_FINGERPRINTS 为空 → 跳过<br/>不匹配 → NetworkFailure(badCertificate)"]
I2["[2] AuthInterceptor<br/>🔑 Token 注入<br/>skip_auth=true → 跳过<br/>否则读 SecureStorage → header[tokenName]=token"]
I3["[3] TokenRefreshInterceptoronError<br/>🔄 401 自动刷新<br/>业务码分类 → 不消耗非 token 类错误<br/>Completer 互斥防并发重复消耗"]
I4["[4] RetryInterceptoronError<br/>🔁 指数退避重试<br/>408/429/5xx → 最多3次<br/>200ms→400ms→800ms"]
I5["[5] ErrorInterceptoronError<br/>🗺️ 异常映射<br/>DioException → sealed Failure<br/>写入 err.error 供下游读取"]
I6["[6] AuthLogoutInterceptoronError<br/>🚪 强制登出<br/>err.error is AuthFailure → clearAll()<br/>→ markLoggedOut() → GoRouter 跳 /login"]
I7["[7] LogInterceptor<br/>📋 请求日志<br/>Dev/Internal → TalkerDioLogger<br/>Release → noop(零日志泄漏)"]
end
NET(["🌐 服务器"])
RESP(["📥 Response<br/>成功响应返回业务代码"])
REQ --> I1 --> I2 --> NET
NET --> RESP
NET -- 错误 --> I7 --> I6 --> I5 --> I4 --> I3
style I1 fill:#E74C3C,color:#fff
style I2 fill:#3498DB,color:#fff
style I3 fill:#9B59B6,color:#fff
style I4 fill:#F39C12,color:#fff
style I5 fill:#1ABC9C,color:#fff
style I6 fill:#E67E22,color:#fff
style I7 fill:#7F8C8D,color:#fff
```
---
### 4.3 sealed Failure 错误体系
(参见:1.3 Sealed Class、3.1 Freezed
```mermaid
flowchart LR
NET["网络/DB 异常<br/>DioException<br/>DriftException<br/>RsaEncryptionException"]
EM["ExceptionMapper<br/>.fromUnknown(e, st)"]
subgraph SF["sealed Failure(编译期强制穷举)"]
NF["NetworkFailure<br/>kind: timeout/noNetwork<br/>/badCertificate/cancelled"]
AF["AuthFailure<br/>kind: unauthorized/forbidden<br/>/refreshFailed/cryptoFailed"]
SF2["ServerFailure<br/>code: 业务错误码<br/>statusCode: HTTP 状态码"]
CF["CacheFailure<br/>本地数据库 IO 异常"]
UF["UnknownFailure<br/>兜底"]
end
UI1["UI 层<br/>failure.userMessage<br/>→ AppToast"]
UI2["ErrorLogger<br/>failure.message(技术细节)<br/>→ Drift error_logs 表"]
UI3["Sentry<br/>failure.toString()<br/>→ 云端聚合"]
NET --> EM
EM --> NF & AF & SF2 & CF & UF
NF & AF & SF2 & CF & UF --> UI1
NF & AF & SF2 & CF & UF --> UI2
UF --> UI3
style NF fill:#E74C3C,color:#fff
style AF fill:#9B59B6,color:#fff
style SF2 fill:#E67E22,color:#fff
style CF fill:#F39C12,color:#fff
style UF fill:#7F8C8D,color:#fff
```
---
### 4.4 认证模块 Clean Architecture
(参见:3.1 Freezed、3.2 Riverpod、3.3 GoRouter、3.4 Dio、3.6 SecureStorage
```mermaid
classDiagram
class AuthRepository {
<<interface>>
+loginWithPhone(phone, code) Future~UserEntity~
+loginWithPassword(phone, pwd) Future~UserEntity~
+logout() Future~void~
+getCurrentUser() Future~UserEntity?~
+sendSmsCode(phone) Future~String?~
+fetchUserProfile() Future~UserEntity~
}
class AuthRepositoryImpl {
-AuthRemoteDatasource _remote
-UsersDao _dao
-SecureStorageService _storage
+loginWithPhone(phone, code) Future~UserEntity~
+loginWithPassword(phone, pwd) Future~UserEntity~
-_persistUser(UserModel) Future~void~
}
class AuthRemoteDatasource {
-Dio _dio
+loginWithPhone(phone, code) Future~UserModel~
+loginWithPassword(phone, pwd) Future~UserModel~
+sendSmsCode(phone) Future~String?~
+logout() Future~void~
+getUserProfile() Future~UserModel~
}
class AuthNotifier {
<<Riverpod Notifier>>
-int _generation
+state: AuthState
+loginWithPhone(phone, code) Future~void~
+loginWithPassword(phone, pwd) Future~void~
+logout() Future~void~
+sendSmsCode(phone) Future~void~
+refreshUserInfo() Future~void~
}
class UserModel {
<<Freezed + JSON>>
+userId: String
+username: String?
+token: String?
+tokenName: String?
+refreshToken: String?
+toEntity() UserEntity
}
class UserEntity {
<<Freezed, Pure Dart>>
+userId: String
+username: String?
+phone: String?
+avatar: String?
}
class AuthState {
<<sealed Freezed>>
initial()
loading()
authenticated(UserEntity)
unauthenticated()
error(String)
}
AuthRepository <|.. AuthRepositoryImpl : implements
AuthRepositoryImpl --> AuthRemoteDatasource : uses
AuthRepositoryImpl --> UserModel : receives
UserModel --> UserEntity : toEntity()
AuthNotifier --> AuthRepository : calls
AuthNotifier --> AuthState : state
```
**分层原则**
- **Domain 层**`UserEntity``AuthRepository`):纯 Dart,无框架依赖,可在服务端/命令行复用
- **Data 层**`UserModel``AuthRepositoryImpl`):负责 JSON 解析和持久化,`UserModel.toEntity()` 是跨层桥梁
- **Presentation 层**`AuthNotifier``LoginPage`):只依赖 `AuthRepository` 接口,测试时可 Mock
---
### 4.5 SQLCipher 加密数据库启动流程
(参见:3.5 Drift、3.11 SQLCipher、3.6 SecureStorage
```mermaid
flowchart TD
A["AppDatabase.open()"] --> B{"Platform.isAndroid?"}
B -- Yes --> C["applyWorkaroundToOpenSqlCipherOnOldAndroidVersions()<br/>确保 Java 先加载 libsqlcipher.so"]
B -- No --> D
C --> D["_sqlCipherIsolateSetup()<br/>主 Isolate 注册 SQLCipher open override"]
D --> E["DbKeyProvider.key<br/>从 SecureStorage 读取/生成 AES-256 密钥<br/>RandomKeyStrategy:首次随机 32 字节)"]
E --> F{"app.db 文件已存在?"}
F -- Yes --> G["_canOpenWithKey()<br/>用当前密钥探测是否可打开"]
G -- 失败 --> H["file.deleteSync()<br/>删除损坏/密钥不匹配的旧文件"]
G -- 成功 --> I
F -- No --> I
H --> I["NativeDatabase.createInBackground()<br/>后台 IsolateisolateSetup=_sqlCipherIsolateSetup(顶层函数!)<br/>setup: PRAGMA key = 'escaped_key'"]
I --> J["AppDatabase 就绪<br/>所有读写透明 AES-256 加密"]
```
---
### 4.6 GoRouter 三态认证跳转
(参见:3.3 GoRouter、1.2 Completer 与异步)
```mermaid
stateDiagram-v2
[*] --> Bootstrapping : App 启动
Bootstrapping --> CheckStorage : authStatusProvider 创建
CheckStorage --> LoggedIn : SecureStorage 有 token
CheckStorage --> LoggedOut : SecureStorage 无 token
CheckStorage --> LoggedOut : 读取异常
state LoggedIn {
[*] --> Home
Home --> [*]
}
state LoggedOut {
[*] --> Login
Login --> [*]
}
state Bootstrapping {
[*] --> Waiting : auth.value == null
Waiting --> [*] : redirect 返回 null(不跳转)
}
LoggedIn --> LoggedOut : logout()<br/>→ markLoggedOut()<br/>→ GoRouter 重新 redirect
LoggedOut --> LoggedIn : login 成功<br/>→ markLoggedIn()<br/>→ GoRouter 重新 redirect
```
---
### 4.7 崩溃日志系统
(参见:3.8 Sentry、3.5 Drift
```mermaid
sequenceDiagram
participant M as main()
participant CR as CrashReporter
participant FH as FlutterError.onError
participant ZH as Zone onError
participant CG as CrashGateUI
M->>CR: preInit()(创建崩溃文件目录)
CR-->>M: pendingCrash(读取上次遗留的崩溃记录)
Note over M,CG: pendingCrash 注入 ProviderScopeCrashGate 自动弹窗
M->>CR: installHooks()
CR->>FH: 接管 FlutterError.onErrorWidget 层异常)
CR->>ZH: 接管 runZonedGuardedDart 层未捕获异常)
Note over FH,ZH: 崩溃发生时
FH->>CR: 同步写入 crash.json(不依赖 async
ZH->>CR: 同步写入 crash.json
Note over CR,CG: 下次启动
CR->>CR: consumePending()(读 crash.json 并删除)
CR-->>CG: pendingCrash 非 null → 弹对话框询问上报
```
**"同步写入"是关键设计**:崩溃发生时 Dart 运行时处于不稳定状态,异步操作(`await`)可能无法完成。`CrashReporter` 使用同步文件写入,确保崩溃信息在进程退出前落盘。
---
### 4.8 主题系统
(参见:3.2 Riverpod、3.10 flutter_screenutil
```mermaid
flowchart LR
SP["SharedPreferences<br/>'theme_scheme'<br/>'theme_mode'"]
TN["ThemeNotifier<br/>@Riverpod keepAlive<br/>→ themeProvider<br/>AppColorScheme"]
TMN["ThemeModeNotifier<br/>@Riverpod keepAlive<br/>→ themeModeProvider<br/>ThemeMode"]
AT["AppTheme<br/>.light(scheme)<br/>.dark(scheme)<br/>→ ThemeData"]
APP["AppConsumerWidget<br/>ref.watch(themeProvider)<br/>ref.watch(themeModeProvider)<br/>→ MaterialApp.router"]
SP -->|"启动读取"| TN
SP -->|"启动读取"| TMN
TN -->|"setScheme() → 写入"| SP
TMN -->|"setMode() → 写入"| SP
TN --> AT
AT --> APP
TMN --> APP
```
**5 套色板切换**(运行时热切换,无需重启):
```dart
ref.read(themeNotifierProvider.notifier).setScheme(AppColorScheme.red);
// → SharedPreferences 持久化
// → themeProvider 通知
// → App 重建(只有 App widget,成本极低)
// → MaterialApp.router 使用新 ThemeData
```
---
## 第五章:新增 Feature 的标准流程
### 5.1 目录结构模板
```
lib/features/{feature_name}/
├── domain/
│ ├── entities/{name}_entity.dart # @freezed,纯 Dart,无框架依赖
│ └── repositories/{name}_repository.dart # abstract interface,定义业务契约
├── data/
│ ├── models/{name}_model.dart # @freezed + fromJson + toEntity()
│ ├── datasources/{name}_remote_datasource.dart # @riverpod,只调用 Dio
│ └── repositories/{name}_repository_impl.dart # @riverpod,实现接口
└── presentation/
├── notifiers/{name}_notifier.dart # @riverpod class XxxNotifier
└── pages/{name}_page.dart # ConsumerWidget 或 ConsumerStatefulWidget
```
### 5.2 完整示例(以"订单列表"为例)
**Step 1Domain 层**
```dart
// lib/features/order/domain/entities/order_entity.dart
@freezed
class OrderEntity with _$OrderEntity {
const factory OrderEntity({
required String orderId,
required String title,
required double amount,
required String status,
}) = _OrderEntity;
}
// lib/features/order/domain/repositories/order_repository.dart
abstract interface class OrderRepository {
Future<List<OrderEntity>> getOrders({int page = 1});
}
```
**Step 2Data 层**
```dart
// lib/features/order/data/models/order_model.dart
@freezed
class OrderModel with _$OrderModel {
const factory OrderModel({
@JsonKey(name: 'order_id') required String orderId,
required String title,
required double amount,
required String status,
}) = _OrderModel;
factory OrderModel.fromJson(Map<String, dynamic> json) =>
_$OrderModelFromJson(json);
}
extension OrderModelX on OrderModel {
OrderEntity toEntity() => OrderEntity(
orderId: orderId,
title: title,
amount: amount,
status: status,
);
}
// lib/features/order/data/repositories/order_repository_impl.dart
@riverpod
OrderRepository orderRepository(Ref ref) => OrderRepositoryImpl(
ref.read(orderRemoteDatasourceProvider),
);
class OrderRepositoryImpl implements OrderRepository {
OrderRepositoryImpl(this._remote);
final OrderRemoteDatasource _remote;
@override
Future<List<OrderEntity>> getOrders({int page = 1}) async {
final models = await _remote.getOrders(page: page);
return models.map((m) => m.toEntity()).toList();
}
}
```
**Step 3Presentation 层**
```dart
// lib/features/order/presentation/notifiers/order_notifier.dart
@freezed
sealed class OrderState with _$OrderState {
const factory OrderState.initial() = _Initial;
const factory OrderState.loading() = _Loading;
const factory OrderState.loaded(List<OrderEntity> orders) = _Loaded;
const factory OrderState.error(String message) = _Error;
}
@riverpod
class OrderNotifier extends _$OrderNotifier {
@override
OrderState build() => const OrderState.initial();
Future<void> load() async {
state = const OrderState.loading();
try {
final orders = await ref.read(orderRepositoryProvider).getOrders();
state = OrderState.loaded(orders);
} on Object catch (e, st) {
final failure = ExceptionMapper().fromUnknown(e, st);
state = OrderState.error(failure.message);
}
}
}
```
**Step 4:注册路由**
```dart
// lib/core/router/routes.dart 新增
@TypedGoRoute<OrderRoute>(path: '/orders')
class OrderRoute extends GoRouteData with $OrderRoute {
const OrderRoute();
@override
Widget build(BuildContext context, GoRouterState state) => const OrderPage();
}
```
**Step 5:代码生成**
```bash
make gen # 生成 order_model.g.dart / order_notifier.g.dart 等
```
---
### 5.3 常见坑与最佳实践
| 坑 | 原因 | 正确做法 |
|----|------|---------|
| `initState``ref.watch` 后立即报错 | `initState` 时 Widget 还未 buildProvider 依赖图未稳定 | 用 `addPostFrameCallback` 延迟到 build 后 |
| Drift 后台 Isolate 不生效(数据明文)| `isolateSetup` 传了 lambda 而非顶层函数 | 必须是**顶层函数**(定义在类外层)|
| `context.go('/login')``async` 后崩溃 | 异步操作后 Widget 已卸载 | `await` 后加 `if (!mounted) return;` |
| `ref.watch` 在非 `build` 方法调用 | `watch` 只在 `build` 里有效 | 事件回调用 `ref.read`,监听用 `ref.listen` |
| `print()` 被 Lint 报错 | `very_good_analysis` 禁止 `print` | 用 `appTalker.info/warning/error()` |
| `Navigator.push()` 破坏路由栈 | GoRouter 管理整个导航栈 | 用 `XxxRoute().go(context)` |
| 手动修改 `.g.dart` 文件被覆盖 | `make gen` 覆盖所有生成文件 | 只修改手写源文件,重新 `make gen` |
| `ApiPaths` 中路径为空字符串 | 脚手架占位,未配置 | 接手时填入真实 API 路径 |
---
> **下一步**:阅读 [docs/architecture.md](architecture.md) 了解整体架构决策,
> 阅读 [docs/setup-project.md](setup-project.md) 完成新项目配置。