flutter架 构项目基础

This commit is contained in:
崔宝新
2026-09-04 15:22:20 +08:00
parent 70967229d0
commit 0c5aee59c0
265 changed files with 35484 additions and 0 deletions
+126
View File
@@ -0,0 +1,126 @@
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()));
}