- 新建 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 实体 <T> → «T»
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
49 KiB
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 let和if 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;
📱 原生对比
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 Class(Dart 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 class(Dart 的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时会被覆盖。
📱 原生对比
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 渲染界面靠三层树协同工作:
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 中变化的部分。
📱 原生对比
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 |
// 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 树中的位置标识,通过它可以:
// 向上查找最近的 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:
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 生命周期
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 |
第三章:生态库逐一解析
学习建议:本章库的讲解顺序经过设计——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 极其繁琐且容易出错。
// 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 序列化;enumwith 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 实例) |
📱 原生对比
Android:ViewModel(AAC)提供类似"状态容器",StateFlow<T>对应AsyncValue<T>(Riverpod 的异步状态包装),viewModels()/ Hilt@Inject对应ref.watch/read。Riverpod 的ProviderScope≈ Hilt 的@HiltAndroidApp(应用级依赖注入容器)。
iOS:@StateObject+ObservableObject/@Published对应 Riverpod Notifier;@EnvironmentObject对应keepAlive: true的全局 Provider;CombinePublisher对应StreamProvider。TCA(The 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;
},
)
📱 原生对比
Android:Jetpack Navigation Component + Safe Args(类型安全参数传递)是直接对标;NavController.navigate()≈context.go();Navigation Graph XML 的全局 action ≈ GoRouterredirect。
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 传递信号,避免拦截器之间直接耦合:
// 登录接口调用时设置,告知 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 写查询。
// 表定义(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 线程:
// app_database.dart
NativeDatabase.createInBackground(
file,
isolateSetup: _sqlCipherIsolateSetup, // 必须是顶层函数!
setup: (db) {
db.execute("PRAGMA key = '$escaped';"); // 解锁加密
},
)
📱 原生对比
Android:Room 是直接对标——同样注解式、同样代码生成 DAO、同样禁止主线程 IO(Room 默认在 IO Dispatcher 执行)。@Entity≈ DriftTable,@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 的加密存储符合安全规范。
// 零容忍规则: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 包无法关闭,会泄露敏感信息。
// 使用(零容忍:禁止 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 的各级别,DebugTreevsReleaseTree实现 Release 环境静默——与 Talker 设计理念完全一致。
iOS:OSLog(官方,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 / iOS:Firebase 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
📱 原生对比
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 防止文字在小屏上过小。
// 使用
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 中间人攻击场景下也有额外保护层)。
// 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:Securityframework(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)
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)
为什么顺序不能颠倒:
CrashReporter.preInit()必须最早——它准备崩溃文件写入路径,后续任何阶段崩溃都能被捕获SentrySetup.init()包裹整个appRunner——确保 Sentry 捕获到启动期异常AppDatabase.open()必须在ProviderScope之前——appDatabaseProvider用overrideWithValue注入,需要实例准备好
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] TokenRefreshInterceptor(onError)<br/>🔄 401 自动刷新<br/>业务码分类 → 不消耗非 token 类错误<br/>Completer 互斥防并发重复消耗"]
I4["[4] RetryInterceptor(onError)<br/>🔁 指数退避重试<br/>408/429/5xx → 最多3次<br/>200ms→400ms→800ms"]
I5["[5] ErrorInterceptor(onError)<br/>🗺️ 异常映射<br/>DioException → sealed Failure<br/>写入 err.error 供下游读取"]
I6["[6] AuthLogoutInterceptor(onError)<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 层(
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)
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/>后台 Isolate:isolateSetup=_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 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)
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["App(ConsumerWidget)<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 1:Domain 层
// 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 2:Data 层
// 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 3:Presentation 层
// 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 常见坑与最佳实践
| 坑 | 原因 | 正确做法 |
|---|---|---|
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 了解整体架构决策, 阅读 docs/setup-project.md 完成新项目配置。