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 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())); }