# 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? _refreshing; ``` > **📱 原生对比** > Kotlin 有完全相同的空安全语法(`?`、`!!`、`?.`、`?:`),Dart 的设计明显借鉴了它。 > Swift 用 `Optional` / `T?` 实现相同语义,`guard let` 和 `if let` 等价于 Dart 的 `if (x != null)`。 --- ### 1.2 异步编程(Future / Stream / async-await) Dart 是**单线程**的(与 JavaScript 相同),通过事件循环(Event Loop)处理并发。 `Future` 代表"将来某时的一个值",`Stream` 代表"将来某时的一系列值"。 ```dart // Future:一次性异步结果 Future 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 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? _refreshing; // 第一个进来的 401 final completer = Completer(); _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` 等价于 `Stream`,`Deferred` 等价于 `Completer`。 > iOS:`async/await`(Swift 5.5+)语法与 Dart 几乎相同,`AsyncStream` 等价于 `Stream`,Swift 的 `actor` 提供类似 `Completer` 的互斥能力。 --- ### 1.3 Sealed Class、Extension、Mixin #### Sealed Class(Dart 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 { int _seconds = 0; void startCountdown() { /* 60s 倒计时逻辑 */ } } class _LoginPageState extends ConsumerState 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` 时会被覆盖。 > **📱 原生对比** > Android:KSP(Kotlin Symbol Processing)+ Room/Hilt/Retrofit 注解处理器,与 build_runner 机制完全相同。 > iOS:Swift 宏(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 树
(配置描述,不可变,轻量)"] E["Element 树
(实例持有者,持久化,生命周期载体)"] R["RenderObject 树
(真正测量/绘制,昂贵)"] 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 中变化的部分。 > **📱 原生对比** > Android:View 树只有一层,View 对象同时承担"配置"和"渲染"两个职责,这也是 Android View 系统性能问题的根源之一。Jetpack Compose 的设计与 Flutter 更接近(声明式、不可变配置 + 运行时状态树)。 > iOS UIKit:UIView 同样是"配置+渲染"合一;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 // 向上查找最近的 InheritedWidget(MediaQuery、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 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() 只调用一次
可以启动 Animation、订阅 Stream note right of Disposed : dispose() 必须释放资源
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` | --- ## 第三章:生态库逐一解析 > **学习建议**:本章库的讲解顺序经过设计——**Freezed(3.1)先于 Riverpod(3.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 foo(Ref)` | `FutureProvider` | 异步一次性数据(网络请求) | | `@riverpod Stream foo(Ref)` | `StreamProvider` | 持续数据流(WebSocket) | | `@riverpod T foo(Ref)` | `Provider` | 同步只读计算值 | | `class Foo extends _$Foo` | `NotifierProvider` | 可变状态 + 方法 | | `@Riverpod(keepAlive: true)` | 永不销毁的 Provider | 全局单例(路由、DB、Dio 实例) | > **📱 原生对比** > **Android**:ViewModel(AAC)提供类似"状态容器",`StateFlow` 对应 `AsyncValue`(Riverpod 的异步状态包装),`viewModels()` / Hilt `@Inject` 对应 `ref.watch/read`。Riverpod 的 `ProviderScope` ≈ Hilt 的 `@HiltAndroidApp`(应用级依赖注入容器)。 > **iOS**:`@StateObject` + `ObservableObject`/`@Published` 对应 Riverpod Notifier;`@EnvironmentObject` 对应 `keepAlive: true` 的全局 Provider;Combine `Publisher` 对应 `StreamProvider`。TCA(The Composable Architecture)是功能上更完整的对标方案。 --- ### 3.3 GoRouter —— 声明式路由 **是什么**:Flutter 官方推荐的路由库,支持深链接(Deep Link)、Web URL 路由、类型安全路由、权限守卫(redirect)。 **解决什么问题**:原生 `Navigator.push/pop` 无法用 URL 描述路由状态,深链接处理复杂,无法做全局路由守卫。 ```dart // routes.dart — 类型安全路由定义 @TypedGoRoute(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 with _$UsersDaoMixin { Future findById(String userId) => (select(users)..where((t) => t.userId.equals(userId))) .getSingleOrNull(); // 编译期类型安全 Future 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、同样禁止主线程 IO(Room 默认在 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 使用 Keychain,Android 使用 EncryptedSharedPreferences(AES-256 + Android Keystore)。 **解决什么问题**:普通 `SharedPreferences` 存储 Token 在 Android 上明文可读(rooted 设备),Secure Storage 的加密存储符合安全规范。 ```dart // 零容忍规则:Token 只能从 SecureStorage 读写,禁止存 SharedPreferences class SecureStorageService { Future getToken() => _storage.read(key: _kToken); Future setToken(String token) => _storage.write(key: _kToken, value: token); Future 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**:dp(density-independent pixels)+ sp(scale-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 裸库 ↓ 替换为 SQLCipher(libsqlcipher.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 原生加密 API;Android 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()
业务代码发起请求"]) subgraph Chain["拦截器链(onRequest 从上到下,onError 从下到上)"] direction TB I1["[1] CertPinningInterceptor
🔐 TLS 指纹校验
PINNED_FINGERPRINTS 为空 → 跳过
不匹配 → NetworkFailure(badCertificate)"] I2["[2] AuthInterceptor
🔑 Token 注入
skip_auth=true → 跳过
否则读 SecureStorage → header[tokenName]=token"] I3["[3] TokenRefreshInterceptor(onError)
🔄 401 自动刷新
业务码分类 → 不消耗非 token 类错误
Completer 互斥防并发重复消耗"] I4["[4] RetryInterceptor(onError)
🔁 指数退避重试
408/429/5xx → 最多3次
200ms→400ms→800ms"] I5["[5] ErrorInterceptor(onError)
🗺️ 异常映射
DioException → sealed Failure
写入 err.error 供下游读取"] I6["[6] AuthLogoutInterceptor(onError)
🚪 强制登出
err.error is AuthFailure → clearAll()
→ markLoggedOut() → GoRouter 跳 /login"] I7["[7] LogInterceptor
📋 请求日志
Dev/Internal → TalkerDioLogger
Release → noop(零日志泄漏)"] end NET(["🌐 服务器"]) RESP(["📥 Response
成功响应返回业务代码"]) 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 异常
DioException
DriftException
RsaEncryptionException"] EM["ExceptionMapper
.fromUnknown(e, st)"] subgraph SF["sealed Failure(编译期强制穷举)"] NF["NetworkFailure
kind: timeout/noNetwork
/badCertificate/cancelled"] AF["AuthFailure
kind: unauthorized/forbidden
/refreshFailed/cryptoFailed"] SF2["ServerFailure
code: 业务错误码
statusCode: HTTP 状态码"] CF["CacheFailure
本地数据库 IO 异常"] UF["UnknownFailure
兜底"] end UI1["UI 层
failure.userMessage
→ AppToast"] UI2["ErrorLogger
failure.message(技术细节)
→ Drift error_logs 表"] UI3["Sentry
failure.toString()
→ 云端聚合"] 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 { <> +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 { <> -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 { <> +userId: String +username: String? +token: String? +tokenName: String? +refreshToken: String? +toEntity() UserEntity } class UserEntity { <> +userId: String +username: String? +phone: String? +avatar: String? } class AuthState { <> 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()
确保 Java 先加载 libsqlcipher.so"] B -- No --> D C --> D["_sqlCipherIsolateSetup()
主 Isolate 注册 SQLCipher open override"] D --> E["DbKeyProvider.key
从 SecureStorage 读取/生成 AES-256 密钥
(RandomKeyStrategy:首次随机 32 字节)"] E --> F{"app.db 文件已存在?"} F -- Yes --> G["_canOpenWithKey()
用当前密钥探测是否可打开"] G -- 失败 --> H["file.deleteSync()
删除损坏/密钥不匹配的旧文件"] G -- 成功 --> I F -- No --> I H --> I["NativeDatabase.createInBackground()
后台 Isolate:isolateSetup=_sqlCipherIsolateSetup(顶层函数!)
setup: PRAGMA key = 'escaped_key'"] I --> J["AppDatabase 就绪
所有读写透明 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()
→ markLoggedOut()
→ GoRouter 重新 redirect LoggedOut --> LoggedIn : login 成功
→ markLoggedIn()
→ 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 CrashGate(UI) M->>CR: preInit()(创建崩溃文件目录) CR-->>M: pendingCrash(读取上次遗留的崩溃记录) Note over M,CG: pendingCrash 注入 ProviderScope,CrashGate 自动弹窗 M->>CR: installHooks() CR->>FH: 接管 FlutterError.onError(Widget 层异常) CR->>ZH: 接管 runZonedGuarded(Dart 层未捕获异常) 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
'theme_scheme'
'theme_mode'"] TN["ThemeNotifier
@Riverpod keepAlive
→ themeProvider
(AppColorScheme)"] TMN["ThemeModeNotifier
@Riverpod keepAlive
→ themeModeProvider
(ThemeMode)"] AT["AppTheme
.light(scheme)
.dark(scheme)
→ ThemeData"] APP["App(ConsumerWidget)
ref.watch(themeProvider)
ref.watch(themeModeProvider)
→ 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 1:Domain 层** ```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> getOrders({int page = 1}); } ``` **Step 2:Data 层** ```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 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> getOrders({int page = 1}) async { final models = await _remote.getOrders(page: page); return models.map((m) => m.toEntity()).toList(); } } ``` **Step 3:Presentation 层** ```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 orders) = _Loaded; const factory OrderState.error(String message) = _Error; } @riverpod class OrderNotifier extends _$OrderNotifier { @override OrderState build() => const OrderState.initial(); Future 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(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 还未 build,Provider 依赖图未稳定 | 用 `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) 完成新项目配置。