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

49 KiB
Raw Blame History

Flutter 企业级开发指南(小白入门)

本文档面向初次接触 Flutter / Dart 的开发者,系统讲解语言基础、框架原理、生态库, 以及 sunny_mochi 脚手架中每个核心组件的设计原理和使用方式。

阅读建议:按章顺序读一遍建立心智模型,之后按需跳阅第三、四章查阅具体库和组件。 如果你有 Android 或 iOS 原生开发背景,每节的"📱 原生对比"可以帮你快速建立映射关系。


目录


第一章:Dart 语言核心

Dart 是 Flutter 的唯一编程语言。Flutter 框架的 Widget 系统、Riverpod 的响应式机制、 Drift 的类型安全查询,底层都是 Dart 特性在发挥作用。读懂本章,后续所有库的用法 自然就能理解。

1.1 类型系统与空安全(Null Safety)

Dart 是强类型、静态类型语言,从 Dart 2.12 起引入空安全(Sound Null Safety。 空安全意味着编译器在编译期就能捕获空指针错误,而不是等到运行时崩溃。

// 不允许为 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;

项目中的典型用法

// 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 letif let 等价于 Dart 的 if (x != null)


1.2 异步编程(Future / Stream / async-await

Dart 是单线程的(与 JavaScript 相同),通过事件循环(Event Loop)处理并发。 Future 代表"将来某时的一个值",Stream 代表"将来某时的一系列值"。

// 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"操作:

// main.dart — 不 await sync 启动,不阻塞 UI 渲染
unawaited(ref.read(syncServiceProvider).start());

Completer:手动控制 Future 的完成时机,本项目用于 Token 刷新互斥:

// 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;

📱 原生对比
Androidsuspend funKotlin Coroutines)等价于 async/awaitFlow<T> 等价于 Stream<T>Deferred<T> 等价于 Completer
iOSasync/awaitSwift 5.5+)语法与 Dart 几乎相同,AsyncStream 等价于 StreamSwift 的 actor 提供类似 Completer 的互斥能力。


1.3 Sealed Class、Extension、Mixin

Sealed ClassDart 3.0+

sealed 关键字让编译器知道子类集合是封闭的,在 switch 时强制穷举所有情况:

// 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 { /* ... */ }

使用时编译器确保你处理了所有情况:

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 classDart 的 sealed 语义来自 Kotlin)。
Swift 用带关联值的 enum 实现同等效果:enum Failure { case network(kind: NetworkKind), case auth, ... }switch 同样强制穷举。

Extension(扩展方法)

已有类型添加方法,无需继承,无需修改源码:

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 多个):

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 查询代码

运行代码生成

# 一次性生成(修改 .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 生命周期对应 StatefulWidgetState

2.1 三棵树:Widget / Element / RenderObject

Flutter 渲染界面靠三层树协同工作:

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 都可能重建
  • ElementWidget 的实例,负责"连接新旧 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
// 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 树中的位置标识,通过它可以:

// 向上查找最近的 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

Future<void> onTap() async {
  await someAsyncOp();
  if (!mounted) return;  // Widget 可能已卸载
  context.go('/next');
}

📱 原生对比
AndroidContext 是 Android 的核心概念,用于访问资源、启动 Activity、获取系统服务——与 BuildContext 功能重叠但范围更广(Android Context 贯穿整个 App 生命周期,BuildContext 只在 Widget 树范围内有效)。
iOS:没有直接等价物;SwiftUI 的 EnvironmentEnvironmentValues 实现了类似的"向上查找"机制(@Environment(\.colorScheme) var colorScheme)。


2.4 生命周期

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/FragmentonCreate → onStart → onResume → onPause → onStop → onDestroyinitState()onCreate()dispose()onDestroy()
iOS UIViewControllerviewDidLoad → viewWillAppear → viewDidAppear → viewWillDisappear → viewDidDisappear → deinitinitState()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 类自动生成 ==hashCodecopyWithtoStringtoJson/fromJson,并支持联合类型(Union Types / Discriminated Union)。

解决什么问题Dart 原生没有 data class,手写 ==copyWith 极其繁琐且容易出错。

// 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');
  // ...
}

📱 原生对比
AndroidKotlin 的 data class 原生提供 equals/hashCode/copy(对应 copyWith),sealed class 原生支持穷举 when——Freezed 的功能在 Kotlin 里几乎是语言内置的。
iOSSwift 的 struct + Codable 提供值语义和 JSON 序列化;enum with associated values 提供联合类型。Swift 没有内置 copyWith,通常手写或用 @dynamicMemberLookup 变通。


3.2 Riverpod —— 响应式状态管理

是什么:Flutter 社区最主流的状态管理方案,基于"Provider"概念,解决了原 Provider 包的诸多痛点(类型安全、测试隔离、自动处理异步等)。它同时承担状态管理依赖注入两个职责。

解决什么问题:在多个 Widget 之间共享状态,避免"状态提升"导致的层层传递 callback,同时保证 UI 响应式更新。

// 定义 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 实例)

📱 原生对比
AndroidViewModelAAC)提供类似"状态容器"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 描述路由状态,深链接处理复杂,无法做全局路由守卫。

// 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;
  },
)

📱 原生对比
AndroidJetpack Navigation Component + Safe Args(类型安全参数传递)是直接对标;NavController.navigate()context.go()Navigation Graph XML 的全局 action ≈ GoRouter redirect
iOSSwiftUI 的 NavigationPath + .navigationDestination(for:destination:) 是声明式路由的原生方案;UIKit 的 UINavigationController + Coordinator pattern 是更成熟的命令式替代。


3.4 Dio —— HTTP 客户端

是什么Flutter 最流行的 HTTP 客户端,支持拦截器链、文件上传、取消请求、FormData 等。

解决什么问题:原生 http 包功能简单,Dio 提供完整的中间件(拦截器)机制,可以统一处理 Token 注入、错误映射、重试等横切关注点。

Options.extra 信号机制(本项目独特设计):

拦截器之间通过 RequestOptions.extra Map 传递信号,避免拦截器之间直接耦合:

// 登录接口调用时设置,告知 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); // 不再重试

📱 原生对比
AndroidOkHttp 的 Interceptor 接口与 Dio 拦截器机制几乎完全相同(连"应用层拦截器 vs 网络层拦截器"的分类思路都一致)。Retrofit 基于 OkHttp 提供注解式 API 定义。OkHttpClient.Builder().addInterceptor() 对应 dio.interceptors.add()
iOSURLSession 原生没有拦截器链;Alamofire 的 RequestInterceptoradapt + retry)和 EventMonitor 提供等价能力;Moya(基于 Alamofire)提供更高层的抽象。


3.5 Drift —— 类型安全 ORM

是什么Flutter/Dart 的类型安全 SQLite ORM,通过代码生成将 Dart 类映射为数据库表,查询语句在编译期检查。

解决什么问题:直接写 SQL 字符串没有类型检查,容易出现拼写错误和运行时崩溃。Drift 让你用 Dart 类型化的 API 写查询。

// 表定义(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);
}

后台 IsolateDrift 的 NativeDatabase.createInBackground 在独立 Isolate 中运行数据库操作,不阻塞 UI 线程:

// 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
iOSCore 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 的加密存储符合安全规范。

// 零容忍规则: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(); // 退出登录时调用
}

📱 原生对比
AndroidEncryptedSharedPreferencesJetpack Security)直接对标,底层用 Android Keystore + AES-256-GCM;也可直接操作 Android Keystore 存证书/密钥。
iOSKeychain Services 是原生安全存储,SecItemAdd / SecItemCopyMatching 是底层 API;大多数项目用 KeychainAccess(三方)简化调用。


3.7 Talker —— 结构化日志

是什么:Flutter 的结构化日志库,支持按级别过滤(verbose/debug/info/warning/error)、颜色输出、集成 Dio 和 Riverpod 的自动日志。

解决什么问题print() 没有级别、没有颜色、Release 包无法关闭,会泄露敏感信息。

// 使用(零容忍:禁止 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 设计理念完全一致。
iOSOSLog(官方,iOS 14+)是结构化日志的现代方案;CocoaLumberjack(三方)提供更丰富的级别过滤和文件输出,功能上更接近 Talker。


3.8 Sentry —— 云端错误追踪

是什么:错误监控 SaaS,将 App 崩溃和异常上报到云端,提供错误聚合、影响用户数统计、堆栈还原(ProGuard / dSYM)。

本项目重点:PII 脱敏

// sentry_setup.dart — beforeSend 钩子过滤敏感信息
SentryFlutter.init(
  (options) {
    options.beforeSend = (event, hint) => _sanitize(event);
    // 手机号脱敏、Token 完全移除、身份证号脱敏
  },
);
// DSN 留空 = 不上报(dev 环境默认不上报)

📱 原生对比
Android / iOSFirebase Crashlytics 是最主流的替代(Google 出品,免费,与 Firebase 生态集成深);Sentry 同样提供 Android SDK 和 iOS SDK,功能对等。两者都支持 beforeSend 钩子脱敏。


3.9 Slang —— 编译期 i18n

是什么:基于代码生成的国际化方案,翻译文件(JSON)在编译期转为类型安全的 Dart 类,拼错翻译键名会在编译期报错。

// 使用(context.t 是类型安全的)
Text(context.t.auth.login)         // "登录"
Text(context.t.auth.phoneHint)     // "请输入手机号"camelCase 自动转换)
// 修改翻译后:make gen-i18n

📱 原生对比
AndroidString Resourcesres/values/strings.xml)是官方方案,通过 R.string.xxx 访问(编译期检查)。无需三方库,但键名是字符串常量,IDE 支持弱于 Slang 的类型链式访问。
iOSLocalizable.strings + NSLocalizedString("key", comment:) 是传统方案;SwiftGen(三方)与 Slang 最接近,从资源文件生成类型安全的 Swift 访问器。


3.10 flutter_screenutil —— 屏幕适配

是什么:基于设计稿尺寸自动缩放 UI 元素,确保在不同屏幕尺寸的设备上界面比例一致。

本项目配置:设计基准 375×812(与 iOS 6s 逻辑分辨率相同),minTextAdapt: true 防止文字在小屏上过小。

// 使用
Container(
  width: 200.w,    // 200 / 375 * 屏幕宽度
  height: 50.h,    // 50 / 812 * 屏幕高度
  padding: EdgeInsets.all(16.r),  // 按最小边缩放
  child: Text('登录', style: TextStyle(fontSize: 16.sp)),
)

📱 原生对比
Androiddpdensity-independent pixels+ spscale-independent pixels)是系统内置适配方案;ConstraintLayout 的百分比约束和 guideline 实现比例布局。无需三方库。
iOSAuto 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 间序列化失败,加密静默不生效。

📱 原生对比
AndroidSQLCipher for Android(同一家公司的 SDK)可直接替换 android.database.sqliteJetpack Security 的 EncryptedFile + Room 组合提供另一种加密路径(实验性,不如 SQLCipher 成熟)。
iOSSQLCipher for iOS(同一 SDK 体系);Core Data 不原生支持加密,需要通过 SQLCipher 替换底层存储或使用 SQLite 层加密。


3.12 pointycastle + asn1lib —— RSA 加密传输

是什么:纯 Dart 密码学库,pointycastle 提供 RSA/AES 等算法实现,asn1lib 解析 ASN.1/DER 格式的密钥。

本项目用途:登录密码在客户端用服务端公钥加密后传输,防止明文密码在网络中传输(即使 HTTPS 中间人攻击场景下也有额外保护层)。

// rsa_helper.dart
// 失败时抛 RsaEncryptionException(禁止 fallback 明文,零容忍)
static String encrypt(String plainText, String spkiBase64) {
  // Base64 解码 → DER → ASN.1 解析 → RSA 公钥 → PKCS#1 v1.5 加密 → Base64 输出
}

📱 原生对比
Androidjavax.cryptoJCE+ java.securityJCA)是 JVM 原生加密 APIAndroid Keystore System 提供硬件级密钥保护。Bouncy Castle 是最流行的三方密码学库(pointycastle 是其 Dart 移植版)。
iOSSecurity frameworkSecKeySecKeyCreateWithData)处理 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

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 之前——appDatabaseProvideroverrideWithValue 注入,需要实例准备好

4.2 七层拦截器网络栈

(参见:3.4 Dio 拦截器链)

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

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

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 层UserEntityAuthRepository):纯 Dart,无框架依赖,可在服务端/命令行复用
  • Data 层UserModelAuthRepositoryImpl):负责 JSON 解析和持久化,UserModel.toEntity() 是跨层桥梁
  • Presentation 层AuthNotifierLoginPage):只依赖 AuthRepository 接口,测试时可 Mock

4.5 SQLCipher 加密数据库启动流程

(参见:3.5 Drift、3.11 SQLCipher、3.6 SecureStorage

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 与异步)

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

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

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 套色板切换(运行时热切换,无需重启):

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 层

// 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 层

// 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 层

// 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:注册路由

// 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:代码生成

make gen  # 生成 order_model.g.dart / order_notifier.g.dart 等

5.3 常见坑与最佳实践

原因 正确做法
initStateref.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 了解整体架构决策, 阅读 docs/setup-project.md 完成新项目配置。