127 lines
7.3 KiB
Dart
127 lines
7.3 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:go_router/go_router.dart';
|
||
import 'package:new_learn/core/app.dart';
|
||
import 'package:new_learn/core/network/dio_client.dart';
|
||
import 'package:new_learn/core/providers/privacy_provider.dart';
|
||
import 'package:new_learn/core/storage/database/app_database.dart';
|
||
import 'package:new_learn/core/storage/privacy_storage.dart';
|
||
|
||
/// App 启动入口 —— 真正的业务代码都在 app.dart / pages/ 里
|
||
///
|
||
/// 〇、环境 / 品牌参数(--dart-define 编译期注入,详见 core/config/app_config.dart):
|
||
/// 日常开发(默认正式环境 + 爱学蝶变,零参数):
|
||
/// flutter run
|
||
/// 连测试环境调试:
|
||
/// flutter run --dart-define=ENV=test
|
||
/// 打楚才正式包:
|
||
/// flutter build apk --release --dart-define=ENV=prod --dart-define=BRAND=chucai
|
||
/// 打豫志测试包:
|
||
/// flutter build apk --release --dart-define=ENV=test --dart-define=BRAND=yuzhi
|
||
/// BRAND 可选:aixue / chucai / yule / yuzhi / qinxiao / wansi / shucai
|
||
/// (品牌表见 core/config/app_brand.dart;不传默认 aixue=爱学蝶变)
|
||
///
|
||
/// 一、启动顺序(每步都是插件初始化,必须 runApp 前 await):
|
||
/// 1. WidgetsFlutterBinding.ensureInitialized():插件通道必须在 runApp 前就绪;
|
||
/// 2. await DioClient.restoreToken():冷启动把磁盘上的 Token 回填内存,
|
||
/// 这样即便没进登录页,后续请求也自动带 Authorization;
|
||
/// 3. await PrivacyStorage.readAccepted():恢复启动隐私协议同意状态;
|
||
/// 未同意时,App 首屏会先被 StartupGate + 路由守卫拦住;
|
||
/// 4. await AppDatabase.instance.init():初始化本地业务数据库(Isar,
|
||
/// 题库/错题/播放记录集合;web 调试模式自动跳过,见 app_database.dart);
|
||
/// 5. runApp:ProviderScope 包根(Riverpod 全局状态容器),进入 UI。
|
||
///
|
||
/// 二、完整路由运行流程(从 main() 到页面渲染):
|
||
///
|
||
/// main()
|
||
/// │
|
||
/// ├─ 1. WidgetsFlutterBinding.ensureInitialized()
|
||
/// ├─ 2. GoRouter.optionURLReflectsImperativeAPIs = true
|
||
/// ├─ 3. await DioClient.restoreToken() ← 恢复登录态
|
||
/// ├─ 4. privacyAcceptedNotifier.value = ... ← 恢复隐私同意状态
|
||
/// ├─ 5. await AppDatabase.instance.init() ← 初始化 Isar
|
||
/// │
|
||
/// └─ 6. runApp(ProviderScope(child: TabApp()))
|
||
/// │
|
||
/// └─ MaterialApp.router(routerConfig: appRouter)
|
||
/// │
|
||
/// ├─ builder: PrivacyStartupGate ← 视觉第一拦
|
||
/// │ ├─ 未同意隐私 → 全屏盖 PrivacyConsentPage
|
||
/// │ └─ 已同意隐私 → 放行 child(路由树)
|
||
/// │
|
||
/// └─ appRouter (GoRouter)
|
||
/// │
|
||
/// ├─ redirect: _guardLogin ← 每次导航都过一遍
|
||
/// │ │
|
||
/// │ │ 判断逻辑(按顺序):
|
||
/// │ │ ① 未同意隐私 且不在隐私页 → 重定向 /privacy-consent
|
||
/// │ │ ② 已同意隐私 且在隐私页 → 已登录去 /,未登录去 /login
|
||
/// │ │ ③ 未登录 且不在登录页 → 重定向 /login
|
||
/// │ │ ④ 已登录 且在登录页 → 重定向 /(首页)
|
||
/// │ │ ⑤ 以上都不满足 → return null(放行)
|
||
/// │ │
|
||
/// │ └─ 触发时机:
|
||
/// │ • 每次 context.push/go 导航时
|
||
/// │ • authTick 变化时(登录/登出 → 自动 refresh)
|
||
/// │ • privacyAcceptedNotifier 变化时
|
||
/// │
|
||
/// └─ routes 路由表:
|
||
/// │
|
||
/// ├─ ShellRoute (MainTabPage) ← 底部 4 tab 壳
|
||
/// │ ├─ / → TaskPage (任务)
|
||
/// │ ├─ /recite → RecitePage (背诵)
|
||
/// │ ├─ /class → ClassPage (班级)
|
||
/// │ └─ /my → MyPage (我的)
|
||
/// │ │
|
||
/// │ └─ MainTabPage.initState
|
||
/// │ └─ addPostFrameCallback
|
||
/// │ └─ _checkVersion() ← 版本检查(仅冷启动一次)
|
||
/// │
|
||
/// ├─ /privacy-consent → PrivacyConsentPage
|
||
/// ├─ /user-agreement → LegalDocumentPage
|
||
/// ├─ /privacy-policy → LegalDocumentPage
|
||
/// ├─ /login → LoginPage
|
||
/// └─ /myPoint → MyPointPage
|
||
///
|
||
/// 三、用户正常流程:
|
||
///
|
||
/// 首次安装:
|
||
/// 启动 → PrivacyStartupGate 拦住 → 隐私协议页 → 同意
|
||
/// → redirect 重定向 /login → 登录成功 → authTick 变化
|
||
/// → redirect 重定向 / → ShellRoute → MainTabPage → 版本检查
|
||
///
|
||
/// 老用户(已登录):
|
||
/// 启动 → 已同意隐私 + 有 Token → redirect 直接放行
|
||
/// → / → ShellRoute → MainTabPage → 版本检查
|
||
///
|
||
/// 四、关键文件对照:
|
||
/// main.dart = 启动脚本(本文件)
|
||
/// core/app.dart = 根组件(ScreenUtil + MaterialApp + PrivacyStartupGate)
|
||
/// core/router/app_router.dart = 路由表 + 登录守卫(对标 vue-router/index.ts)
|
||
/// core/router/main_tab_page.dart = 底部 tab 壳 + 版本检查触发点
|
||
/// core/router/router_keys.dart = 全局 NavigatorKey
|
||
///
|
||
/// 想单独跑某个学习 Demo 时,临时改这里,例如:
|
||
/// runApp(const ProviderScope(child: MaterialApp(home: DioDemoPage())));
|
||
/// 跑完记得改回 runApp(const ProviderScope(child: TabApp()));
|
||
Future<void> main() async {
|
||
// 需要在启动 App 之前读取本地存储、调用原生插件,必须写在 runApp 之前!
|
||
// 作用: 初始化 Flutter 和原生的绑定桥梁
|
||
WidgetsFlutterBinding.ensureInitialized();
|
||
// go_router 17.x 大坑:命令式导航(context.push/go)默认不写回浏览器
|
||
// 地址栏(optionURLReflectsImperativeAPIs 默认 false)。必须在使用
|
||
// appRouter(runApp)之前全局开启,否则 web 调试 URL 不随页面变化。
|
||
GoRouter.optionURLReflectsImperativeAPIs = true;
|
||
// 冷启动恢复登录态(Token 从安全存储回填内存)
|
||
await DioClient.instance.restoreToken();
|
||
// 冷启动恢复"是否已同意启动隐私协议"状态。
|
||
// 这个值会同时驱动:
|
||
// 1. app.dart 里的 StartupGate(视觉上先拦住);
|
||
// 2. app_router.dart 里的 redirect(路由层再兜底一次)。
|
||
privacyAcceptedNotifier.value = await PrivacyStorage.instance.readAccepted();
|
||
// 初始化本地业务数据库(Isar 题库/错题/播放记录;web 调试自动跳过)
|
||
await AppDatabase.instance.init();
|
||
// ProviderScope:Riverpod 所有 provider 的容器,必须包在根
|
||
runApp(const ProviderScope(child: TabApp()));
|
||
}
|