Kazumi 应用启动流程详解
Kazumi 应用启动流程详解
本文档详细介绍 Kazumi 应用从 main.dart 的 main() 函数开始,如何一步步加载不同的页面和路由,最终显示主页面的完整流程。
📋 目录
- 一、启动流程概览
- [二、main() 函数初始化阶段](#二 main-函数初始化阶段)
- [三、AppWidget 构建阶段](#三 appwidget-构建阶段)
- 四、路由匹配与页面渲染
- 五、主界面显示阶段
- 六、完整调用链总结
一、启动流程概览
1.1 整体流程图
┌─────────────────────────────────────────┐
│ 1. main() 函数入口 │
│ - Flutter 绑定初始化 │
│ - MediaKit 初始化 │
│ - 移动端 UI 设置 │
│ - Android WebView 检查 │
└──────────────┬──────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 2. 存储初始化(Hive) │
│ - 初始化数据库 │
│ - 注册适配器 │
│ - 打开数据表 │
│ └─> 失败:显示错误页面 │
└──────────────┬──────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 3. 桌面端窗口配置(Windows/macOS/Linux)│
│ - 读取用户设置 │
│ - 配置窗口参数 │
│ - 显示窗口 │
└──────────────┬──────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 4. 网络模块初始化 │
│ - Request 单例初始化 │
│ - Cookie 设置 │
│ - 代理配置 │
└──────────────┬──────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 5. runApp(AppWidget) │
│ - ChangeNotifierProvider │
│ - ThemeProvider │
│ - ModularApp │
│ - AppModule │
└──────────────┬──────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 6. AppWidget.build() │
│ - MaterialApp.router │
│ - routerConfig: Modular.routerConfig │
│ - 主题配置 │
│ - 本地化配置 │
└──────────────┬──────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 7. 路由匹配 "/" │
│ - AppModule.routes("/") │
│ - IndexModule │
│ - InitPage │
└──────────────┬──────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 8. InitPage 渲染与初始化 │
│ - LoadingWidget (空白页) │
│ - 后台执行初始化任务 │
│ - 跳转到默认启动页面 │
└──────────────┬──────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 9. 导航到 /tab/popular/ │
│ - IndexModule.routes("/tab") │
│ - menu.routes["/popular"] │
│ - PopularModule │
└──────────────┬──────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 10. 主界面显示 │
│ - IndexPage │
│ - ScaffoldMenu │
│ - PopularPage (推荐页面) │
│ - BangumiCardV (番剧卡片) │
└─────────────────────────────────────────┘二、main() 函数初始化阶段
2.1 代码位置
文件: lib/main.dart
行数: 第 14-198 行
2.2 详细流程
步骤 1:Flutter 框架初始化(第 21-30 行)
// 确保 Flutter 框架的绑定已经初始化
WidgetsFlutterBinding.ensureInitialized();
// 初始化 media-kit 媒体播放库
MediaKit.ensureInitialized();
// 针对 Android 和 iOS 移动设备进行系统 UI 设置
if (Platform.isAndroid || Platform.isIOS) {
// 设置系统 UI 为边缘到边缘模式(edge-to-edge)
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
// 配置系统 UI 覆盖层样式
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
statusBarColor: Colors.transparent,
));
}作用:
- ✅ 创建 WidgetsBinding 实例,使应用能够与底层平台通信
- ✅ 初始化视频播放功能
- ✅ 设置移动端沉浸式全屏效果
步骤 2:Android WebView 功能检查(第 46-52 行)
// 仅在 Android 平台上执行 WebView 功能支持检查
if (Platform.isAndroid) {
await Utils.checkWebViewFeatureSupport();
}作用:
- ✅ 验证 Android System WebView 是否支持所需特性
- ✅ 确保后续网页解析和视频播放功能正常
步骤 3:存储初始化(第 54-84 行)
try {
// 获取应用程序支持目录并初始化 Hive
final hivePath = '${(await getApplicationSupportDirectory()).path}/hive';
await Hive.initFlutter(hivePath);
// 调用全局存储初始化方法
await GStorage.init();
} catch (e) {
// 捕获存储初始化异常
debugPrint('Storage initialization failed: $e');
// Windows 平台特殊处理:确保窗口管理器已初始化
if (Platform.isWindows) {
await windowManager.ensureInitialized();
windowManager.waitUntilReadyToShow(null, () async {
await windowManager.show();
await windowManager.focus();
});
}
// 显示存储错误页面
runApp(MaterialApp(
title: '初始化失败',
builder: (context, child) {
return const StorageErrorPage();
}
));
return;
}GStorage.init() 做了什么:
- 注册所有 Hive 数据模型适配器
- 打开各个数据表(Box):
bangumi_history_box:观看历史collect_box:追番列表setting_box:应用设置search_history_box:搜索历史
如果初始化失败:
- ❌ 显示
StorageErrorPage错误页面 - ❌ 阻止应用继续启动
- ✅ Windows 平台手动显示窗口(避免窗口不出现的问题)
步骤 4:桌面端窗口配置(第 117-152 行)
// 从设置存储中读取是否显示窗口按钮的配置
bool showWindowButton = await GStorage.setting
.get(SettingBoxKey.showWindowButton, defaultValue: false);
// 判断当前是否为桌面平台
if (Utils.isDesktop()) {
// 确保窗口管理器已经初始化完成
await windowManager.ensureInitialized();
// 检测屏幕分辨率
bool isLowResolution = await Utils.isLowResolution();
// 创建窗口配置对象
WindowOptions windowOptions = WindowOptions(
size: isLowResolution
? const Size(840, 600)
: const Size(1280, 860),
center: true,
skipTaskbar: false,
titleBarStyle: (Platform.isMacOS || !showWindowButton)
? TitleBarStyle.hidden
: TitleBarStyle.normal,
windowButtonVisibility: showWindowButton,
title: 'Kazumi',
);
// 等待窗口准备就绪后显示窗口
windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
});
}作用:
- ✅ 仅对 Windows、macOS、Linux 平台生效
- ✅ 根据用户设置和分辨率配置窗口大小
- ✅ 在 flutter_windows.cpp L36 阻止了自动显示以避免闪烁
- ✅ 手动调用
show()和focus()显示窗口
步骤 5:网络模块初始化(第 166-173 行)
// 创建 Request 单例对象,初始化网络请求模块
Request();
// 设置 Cookie,为后续的网络请求准备认证信息
await Request.setCookie();
// 应用代理设置
ProxyManager.applyProxy();作用:
- ✅ 初始化 Dio HTTP 客户端
- ✅ 配置 Cookie Jar 用于会话管理
- ✅ 如果用户配置了代理,启用代理设置
三、AppWidget 构建阶段
3.1 Widget 层次结构
runApp(
ChangeNotifierProvider( ← 状态管理容器
create: (_) => ThemeProvider(), ← 提供主题管理
child: ModularApp( ← 模块化路由容器
module: AppModule(), ← 主模块配置
child: const AppWidget(), ← 应用界面
),
),
)3.2 各层职责详解
第一层:ChangeNotifierProvider
作用: 状态管理提供者
ChangeNotifierProvider(
create: (_) => ThemeProvider(), // 创建 ThemeProvider 单例
child: ...,
)ThemeProvider 管理什么:
- 亮色/暗色主题切换
- OLED 增强模式
- 动态配色(Material You)
- 字体选择(系统字体/内置字体)
第二层:ModularApp
作用: flutter_modular 提供的根容器 Widget
ModularApp(
module: AppModule(), // 加载主模块配置
child: const AppWidget(),
)为什么要用 ModularApp:
- ✅ 启用路由系统(
context.pushNamed()) - ✅ 启用依赖注入(
Modular.get()) - ✅ 实现模块化架构
- ✅ 支持懒加载
第三层:AppModule
作用: 应用的主模块配置文件
文件位置: lib/app_module.dart
class AppModule extends Module {
@override
void binds(i) {
// 注册可注入的服务
}
@override
void routes(r) {
r.module("/", module: IndexModule()); // 根路径指向 IndexModule
}
}职责:
- 路由配置:定义 URL → 页面的映射关系
- 依赖绑定:注册全局服务(Controller、Repository)
- 组织子模块:导入其他功能模块
第四层:AppWidget
作用: 应用的实际界面容器
文件位置: lib/app_widget.dart
核心代码(第 190-310 行):
@override
Widget build(BuildContext context) {
final ThemeProvider themeProvider = Provider.of<ThemeProvider>(context);
// 桌面端托盘图标配置
if (Utils.isDesktop()) {
_handleTray();
}
// 读取用户设置
dynamic defaultThemeColor = setting.get(SettingBoxKey.themeColor, defaultValue: 'default');
bool oledEnhance = setting.get(SettingBoxKey.oledEnhance, defaultValue: false);
final defaultThemeMode = setting.get(SettingBoxKey.themeMode, defaultValue: 'system');
// 配置主题
themeProvider.setThemeMode(...);
themeProvider.setFontFamily(...);
themeProvider.setTheme(light, dark, ...);
// 构建 MaterialApp
var app = DynamicColorBuilder(
builder: (theme, darkTheme) {
return MaterialApp.router(
title: "Kazumi",
localizationsDelegates: GlobalMaterialLocalizations.delegates,
supportedLocales: const [
Locale.fromSubtags(
languageCode: 'zh', scriptCode: 'Hans', countryCode: "CN")
],
locale: const Locale.fromSubtags(
languageCode: 'zh', scriptCode: 'Hans', countryCode: "CN"),
theme: themeProvider.light,
darkTheme: themeProvider.dark,
themeMode: themeProvider.themeMode,
routerConfig: Modular.routerConfig, // ← 关键!连接路由系统
);
},
);
// 设置路由观察者
Modular.setObservers([KazumiDialog.observer]);
// Android 高帧率设置
if (Platform.isAndroid) {
// 设置首选显示模式
}
return app;
}AppWidget 的职责:
- ✅ 构建
MaterialApp.router(Flutter 的基础框架) - ✅ 配置全局主题(颜色、字体、暗色模式)
- ✅ 处理桌面端特性(托盘图标、窗口事件)
- ✅ 监听应用生命周期(前后台切换)
- ✅ 设置本地化(简体中文)
- ✅ 配置路由系统(
routerConfig: Modular.routerConfig)
关键点:
routerConfig: Modular.routerConfig这行代码连接了 AppModule 的路由配置MaterialApp.router会自动监听路由变化并渲染对应页面
四、路由匹配与页面渲染
4.1 路由树结构
/ (根路径)
├── IndexModule (app_module.dart L12)
│ │
│ ├── routes("/") → InitPage (index_module.dart L54-55)
│ │ └── routes("/error") → 错误页面 (L57-63)
│ │
│ └── routes("/tab") → IndexPage (L66-74)
│ └── children: menu.routes (L71)
│ ├── /popular → PopularModule (router.dart L38-41)
│ ├── /timeline → TimelineModule (L42-45)
│ ├── /collect → CollectModule (L46-49)
│ └── /my → MyModule (L50-53)
│
├── routes("/video") → VideoModule (L75)
├── routes("/info") → InfoModule (L77)
├── routes("/settings") → SettingsModule (L78)
└── routes("/search") → SearchModule (L79)4.2 第一次路由匹配:"/"
匹配过程
// 1. MaterialApp.router 启动时访问根路径 "/"
// 2. 查找 AppModule.routes("/")
// 文件:lib/app_module.dart 第 12 行
r.module("/", module: IndexModule());
// 3. 加载 IndexModule
// 文件:lib/pages/index_module.dart
class IndexModule extends Module {
@override
void routes(r) {
r.child("/",
child: (_) => const InitPage(), // ← 匹配到这里!
children: [...],
transition: TransitionType.noTransition);
}
}结果: 渲染 InitPage
4.3 InitPage 的作用
文件位置: lib/pages/init_page.dart
InitPage 的结构
class InitPage extends StatefulWidget {
const InitPage({super.key});
@override
State<InitPage> createState() => _InitPageState();
}
class _InitPageState extends State<InitPage> {
@override
void initState() {
super.initState();
_initializeApp(); // ← 执行初始化任务
}
Future<void> _initializeApp() async {
_migrateStorage(); // 迁移旧版本数据
_loadShaders(); // 加载着色器
_loadDanmakuShield(); // 加载弹幕屏蔽列表
_webDavInit(); // WebDav 同步
await downloadController.init(); // 下载管理器初始化
await _checkRunningOnX11(); // Linux X11 环境检测
await _pluginInit(); // 插件初始化
_startDefaultPage(); // 跳转到默认启动页面
await Future.delayed(const Duration(milliseconds: 500));
_update(); // 检查更新
}
@override
Widget build(BuildContext context) {
return const LoadingWidget(); // ← 显示空白加载页
}
}
class LoadingWidget extends StatelessWidget {
const LoadingWidget({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(body: Container()); // 简单的空白容器
}
}InitPage 的特点:
- ✅ 渲染内容:
LoadingWidget= 空白的Container() - ✅ 后台工作:在
initState()中执行各种初始化任务 - ✅ 完成后跳转:调用
_startDefaultPage()导航到主界面
_startDefaultPage() 方法详解
代码位置: init_page.dart 第 111-121 行
void _startDefaultPage() {
final defaultStartupPage = setting.get(
SettingBoxKey.defaultStartupPage,
defaultValue: '/tab/popular/', // 默认值:推荐页面
);
// Workaround for dynamic_color
themeProvider.setDynamic(
setting.get(SettingBoxKey.useDynamicColor, defaultValue: false));
Modular.to.navigate(defaultStartupPage); // ← 关键!导航到主界面
}用户可以在设置中修改默认启动页面:
/tab/popular/→ 推荐/tab/timeline/→ 时间表/tab/collect/→ 追番/tab/my/→ 我的
五、主界面显示阶段
5.1 第二次路由匹配:"/tab/popular/"
匹配过程
// 1. Modular.to.navigate('/tab/popular/')
// 2. 查找 IndexModule.routes("/tab")
// 文件:index_module.dart 第 66-74 行
r.child(
"/tab",
child: (_) {
return const IndexPage(); // ← 第一步:匹配到 /tab
},
children: menu.routes, // ← 第二步:在 menu.routes 中继续匹配
transition: TransitionType.fadeIn,
duration: Duration(milliseconds: 70),
);
// 3. 在 menu.routes 中查找 "/popular/"
// 文件:router.dart 第 37-41 行
final MenuRoute menu = MenuRoute([
MenuRouteItem(
path: "/popular",
module: PopularModule(), // ← 匹配到这里!
),
// ... 其他菜单项
]);结果: 加载 PopularModule 并渲染 PopularPage
5.2 IndexPage 的结构
文件位置: lib/pages/index_page.dart
class IndexPage extends StatefulWidget {
const IndexPage({super.key});
@override
State<IndexPage> createState() => _IndexPageState();
}
class _IndexPageState extends State<IndexPage> {
@override
Widget build(BuildContext context) {
return const ScaffoldMenu(); // ← 就这一行!
}
}IndexPage 的作用:
- ✅ 返回
ScaffoldMenu组件 - ✅ 不包含任何实际 UI 元素
- ✅ 只是一个"包装器"
5.3 ScaffoldMenu 的详细结构
文件位置: lib/pages/menu/menu.dart
ScaffoldMenu 的核心功能
class ScaffoldMenu extends StatefulWidget {
const ScaffoldMenu({super.key});
@override
State<ScaffoldMenu> createState() => _ScaffoldMenu();
}
class _ScaffoldMenu extends State<ScaffoldMenu> {
final PageController _page = PageController();
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => NavigationBarState(),
child: Consumer<NavigationBarState>(builder: (context, state, _) {
return OrientationBuilder(builder: (context, orientation) {
state._isBottom = orientation == Orientation.portrait;
return orientation != Orientation.portrait
? sideMenuWidget(context, state) // 横屏:侧边栏
: bottomMenuWidget(context, state); // 竖屏:底部导航
});
}));
}
}ScaffoldMenu 的职责:
- 提供 NavigationBarState:管理当前选中的标签索引
- 检测设备方向:
- 竖屏(Portrait)→ 底部导航栏
- 横屏(Landscape)→ 侧边导航栏
- 渲染 PageView:包含所有子页面的可滚动容器
底部导航栏配置(竖屏模式)
代码位置: menu.dart 第 77-120 行
Widget bottomMenuWidget(BuildContext context, NavigationBarState state) {
return Scaffold(
body: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: PageView.builder(
physics: const NeverScrollableScrollPhysics(),
controller: _page,
itemCount: menu.size,
itemBuilder: (_, __) => const RouterOutlet(), // ← 路由出口
),
),
bottomNavigationBar: state.isHide
? const SizedBox(height: 0)
: NavigationBar(
destinations: const <Widget>[
NavigationDestination(
selectedIcon: Icon(Icons.home),
icon: Icon(Icons.home_outlined),
label: '推荐', // ← 第 0 个标签
),
NavigationDestination(
selectedIcon: Icon(Icons.timeline),
icon: Icon(Icons.timeline_outlined),
label: '时间表', // ← 第 1 个标签
),
NavigationDestination(
selectedIcon: Icon(Icons.favorite),
icon: Icon(Icons.favorite_outlined),
label: '追番', // ← 第 2 个标签
),
NavigationDestination(
selectedIcon: Icon(Icons.settings),
icon: Icon(Icons.settings),
label: '我的', // ← 第 3 个标签
),
],
selectedIndex: state.selectedIndex,
onDestinationSelected: (int index) {
state.updateSelectedIndex(index);
Modular.to.navigate("/tab${menu.getPath(index)}/");
},
),
);
}关键点:
- ✅
RouterOutlet():路由出口,显示当前路由对应的页面 - ✅
PageView.builder:包含所有页面的容器 - ✅
NeverScrollableScrollPhysics():禁止手势滑动切换页面 - ✅ 底部导航栏有 4 个标签:推荐、时间表、追番、我的
NavigationBarState 状态管理
代码位置: menu.dart 第 15-58 行
class NavigationBarState extends ChangeNotifier {
late int _selectedIndex = getDefaultSelectedIndex();
bool _isHide = false;
bool _isBottom = false;
int get selectedIndex => _selectedIndex;
bool get isHide => _isHide;
bool get isBottom => _isBottom;
int getDefaultSelectedIndex() {
final defaultPage = GStorage.setting
.get(SettingBoxKey.defaultStartupPage, defaultValue: "/tab/popular/");
switch (defaultPage) {
case "/tab/popular/":
return 0; // 推荐
case "/tab/timeline/":
return 1; // 时间表
case "/tab/collect/":
return 2; // 追番
case "/tab/my/":
return 3; // 我的
default:
return 0; // 默认推荐
}
}
void updateSelectedIndex(int pageIndex) {
_selectedIndex = pageIndex;
notifyListeners();
}
void hideNavigate() {
_isHide = true;
notifyListeners();
}
void showNavigate() {
_isHide = false;
notifyListeners();
}
}作用:
- ✅ 管理当前选中的标签索引
- ✅ 根据默认启动页面计算初始选中项
- ✅ 控制导航栏的显示/隐藏
- ✅ 检测设备方向(横屏/竖屏)
5.4 PopularModule 加载 PopularPage
PopularModule 的结构
文件位置: lib/pages/popular/popular_module.dart
import 'package:flutter_modular/flutter_modular.dart';
import 'package:kazumi/pages/popular/popular_page.dart';
import 'package:kazumi/pages/popular/popular_controller.dart';
class PopularModule extends Module {
@override
void binds(i) {
i.addSingleton(PopularController.new); // 注册控制器
}
@override
void routes(r) {
r.child("/", child: (_) => const PopularPage()); // 根路径显示 PopularPage
}
}职责:
- 依赖注入:注册
PopularController单例 - 路由配置:定义
/路径对应PopularPage
PopularPage 的详细结构
文件位置: lib/pages/popular/popular_page.dart
PopularPage 的核心组成
class PopularPage extends StatefulWidget {
const PopularPage({super.key});
@override
State<PopularPage> createState() => _PopularPageState();
}
class _PopularPageState extends State<PopularPage>
with AutomaticKeepAliveClientMixin {
final PopularController popularController = Modular.get<PopularController>();
final ScrollController scrollController = ScrollController();
@override
bool get wantKeepAlive => true; // 保持页面状态
@override
void initState() {
super.initState();
scrollController.addListener(scrollListener);
if (popularController.trendList.isEmpty) {
popularController.queryBangumiByTrend(); // 加载热门番剧
}
}
@override
Widget build(BuildContext context) {
super.build(context);
return PopScope(
canPop: false,
onPopInvokedWithResult: (bool didPop, Object? result) {
if (didPop) return;
onBackPressed(context); // 返回按钮处理
},
child: Scaffold(
body: CustomScrollView(
controller: scrollController,
slivers: [
buildSliverAppBar(), // 可伸缩的顶部栏
SliverToBoxAdapter(
child: Observer(
builder: (_) => AnimatedOpacity(
opacity: popularController.isLoadingMore ? 1.0 : 0.0,
child: popularController.isLoadingMore
? const LinearProgressIndicator(minHeight: 4)
: const SizedBox(height: 4),
),
),
),
SliverPadding(
padding: const EdgeInsets.fromLTRB(
StyleString.cardSpace, 0, StyleString.cardSpace, 0),
sliver: Observer(builder: (_) {
if (popularController.isTimeOut) {
return SliverToBoxAdapter(
child: SizedBox(
height: 400,
child: GeneralErrorWidget(
errMsg: '什么都没有找到 (´;ω;`)',
actions: [...],
),
),
);
}
return contentGrid(
(popularController.currentTag == '')
? popularController.trendList
: popularController.bangumiList,
);
}),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () => scrollController.animateTo(0,
duration: const Duration(milliseconds: 350),
curve: Curves.easeOut),
child: const Icon(Icons.arrow_upward),
),
),
);
}
}PopularPage 的组成部分:
| 组件 | 作用 |
|---|---|
| PopScope | 处理物理返回键 |
| Scaffold | Material Design 页面框架 |
| CustomScrollView | 可滚动的视图 |
| buildSliverAppBar() | 可伸缩的顶部栏(显示"热门番组") |
| LinearProgressIndicator | 加载更多时的进度条 |
| contentGrid() | 番剧卡片网格布局 |
| FloatingActionButton | 回到顶部按钮 |
buildSliverAppBar() 详解
代码位置: popular_page.dart 第 200-264 行
Widget buildSliverAppBar() {
final theme = Theme.of(context);
return SliverAppBar(
pinned: true, // 固定在顶部
stretch: true, // 拉伸效果
expandedHeight: 120, // 展开高度
elevation: 0,
backgroundColor: Theme.of(context).colorScheme.surface,
actions: buildActions(), // 右侧按钮(搜索、历史等)
flexibleSpace: SafeArea(
child: dtb.DragToMoveArea(
child: LayoutBuilder(
builder: (context, constraints) {
final double maxExtent = 120 - MediaQuery.of(context).padding.top;
final t = (1 -
((constraints.maxHeight - kToolbarHeight) /
(maxExtent - kToolbarHeight))
.clamp(0.0, 1.0));
final fontWeight = t < 0.5 ? FontWeight.w700 : FontWeight.w500;
final fontSize = lerpDouble(28, 20, t)!;
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.only(left: 16, top: 8, bottom: 8, right: 60),
child: SizedBox(
height: 44,
child: Observer(
builder: (_) {
final bool isTrend = popularController.currentTag == '';
return InkWell(
key: selectorKey,
borderRadius: BorderRadius.circular(8),
onTap: showTagMenu,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
isTrend ? '热门番组' : popularController.currentTag,
style: theme.textTheme.headlineMedium!.copyWith(
fontWeight: fontWeight,
fontSize: fontSize,
),
),
const SizedBox(width: 4),
Icon(Icons.keyboard_arrow_down,
size: fontSize, color: theme.iconTheme.color),
],
),
);
},
),
),
),
);
},
),
),
),
);
}特点:
- ✅ 标题会根据滚动动态变化(字重、字号)
- ✅ 点击标题可以切换标签(热门番组、恋爱、奇幻等)
- ✅ 支持拖拽移动区域(桌面端可拖动窗口)
- ✅ 右侧有搜索、历史记录等按钮
contentGrid() 详解
代码位置: popular_page.dart 第 166-198 行
Widget contentGrid(bangumiList) {
int crossCount = 3;
if (MediaQuery.sizeOf(context).width > LayoutBreakpoint.compact['width']!) {
crossCount = 5;
}
if (MediaQuery.sizeOf(context).width > LayoutBreakpoint.medium['width']!) {
crossCount = 6;
}
return SliverPadding(
padding: const EdgeInsets.all(8),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
mainAxisSpacing: StyleString.cardSpace - 2,
crossAxisSpacing: StyleString.cardSpace,
crossAxisCount: crossCount, // 列数自适应
mainAxisExtent:
MediaQuery.of(context).size.width / crossCount / 0.65 +
MediaQuery.textScalerOf(context).scale(32.0),
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return bangumiList!.isNotEmpty
? BangumiCardV(bangumiItem: bangumiList[index]) // ← 番剧卡片
: null;
},
childCount: bangumiList!.isNotEmpty ? bangumiList!.length : 10,
),
),
);
}响应式布局:
- ✅ 窄屏(手机竖屏):3 列
- ✅ 中等屏幕(平板/小屏电脑):5 列
- ✅ 宽屏(大屏电脑):6 列
BangumiCardV 组件详解
文件位置: lib/bean/card/bangumi_card.dart
// 视频卡片 - 垂直布局
class BangumiCardV extends StatelessWidget {
const BangumiCardV({
super.key,
required this.bangumiItem,
this.canTap = true,
this.enableHero = true,
});
final BangumiItem bangumiItem;
final bool canTap;
final bool enableHero;
@override
Widget build(BuildContext context) {
return Card(
elevation: 0,
clipBehavior: Clip.antiAlias,
margin: EdgeInsets.zero,
child: GestureDetector(
child: InkWell(
onTap: () {
if (!canTap) {
KazumiDialog.showToast(message: '编辑模式');
return;
}
Modular.to.pushNamed('/info/', arguments: bangumiItem); // ← 跳转到详情页
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
AspectRatio(
aspectRatio: 0.65, // 宽高比
child: LayoutBuilder(builder: (context, boxConstraints) {
final double maxWidth = boxConstraints.maxWidth;
final double maxHeight = boxConstraints.maxHeight;
return enableHero
? Hero(
transitionOnUserGestures: true,
tag: bangumiItem.id, // Hero 动画标识
child: NetworkImgLayer(
src: bangumiItem.images['large'] ?? '',
width: maxWidth,
height: maxHeight,
),
)
: NetworkImgLayer(
src: bangumiItem.images['large'] ?? '',
width: maxWidth,
height: maxHeight,
);
}),
),
BangumiContent(bangumiItem: bangumiItem) // ← 标题内容
],
),
),
),
);
}
}
class BangumiContent extends StatelessWidget {
const BangumiContent({super.key, required this.bangumiItem});
final BangumiItem bangumiItem;
@override
Widget build(BuildContext context) {
final ts = MediaQuery.textScalerOf(context);
final int maxTextLines = Utils.isDesktop() ? 3
: (Utils.isTablet() && MediaQuery.of(context).orientation == Orientation.landscape) ? 3 : 2;
return Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(5, 3, 5, 1),
child: Text(
bangumiItem.nameCn, // ← 显示中文名称
textAlign: TextAlign.start,
style: const TextStyle(
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),
textScaler: ts.clamp(maxScaleFactor: 1.1),
maxLines: maxTextLines,
overflow: TextOverflow.ellipsis,
),
),
);
}
}BangumiCardV 的组成:
封面图部分
- 使用
NetworkImgLayer加载网络图片 - 支持 Hero 动画(点击时有过渡效果)
- 宽高比固定为 0.65(典型的海报比例)
- 使用
标题部分(BangumiContent)
- 显示番剧的中文名称(
bangumiItem.nameCn) - 根据设备类型调整最大行数
- 超出部分显示省略号
- 显示番剧的中文名称(
点击交互
- 点击卡片跳转到详情页(
/info/) - 传递整个
BangumiItem对象作为参数 - 编辑模式下显示提示"编辑模式"
- 点击卡片跳转到详情页(
六、完整调用链总结
6.1 完整的启动流程
┌─────────────────────────────────────────────────────────────┐
│ 阶段 1:Flutter 引擎启动 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 阶段 2:main() 函数执行 │
│ 文件:lib/main.dart │
│ ├─ L21: WidgetsFlutterBinding.ensureInitialized() │
│ ├─ L24: MediaKit.ensureInitialized() │
│ ├─ L27-38: 移动端沉浸式 UI 设置 │
│ ├─ L46-52: Android WebView 检查 │
│ ├─ L54-84: Hive 存储初始化 │
│ │ └─ 失败:显示 StorageErrorPage │
│ ├─ L117-152: 桌面端窗口配置 │
│ ├─ L166: Request() 网络模块初始化 │
│ ├─ L171: Request.setCookie() │
│ └─ L173: ProxyManager.applyProxy() │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 阶段 3:runApp(AppWidget) │
│ ├─ ChangeNotifierProvider │
│ │ └─ create: (_) => ThemeProvider() │
│ ├─ ModularApp │
│ │ └─ module: AppModule() │
│ └─ child: const AppWidget() │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 阶段 4:AppWidget.build() │
│ 文件:lib/app_widget.dart │
│ ├─ L191: 读取 ThemeProvider │
│ ├─ L193: 配置桌面端托盘图标 │
│ ├─ L196-239: 读取用户设置并配置主题 │
│ ├─ L240-287: DynamicColorBuilder │
│ │ └─ MaterialApp.router( │
│ │ routerConfig: Modular.routerConfig, │
│ │ ) │
│ └─ L288: Modular.setObservers([KazumiDialog.observer]) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 阶段 5:第一次路由匹配 "/" │
│ 文件:lib/app_module.dart │
│ └─ L12: r.module("/", module: IndexModule()) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 阶段 6:IndexModule 加载 │
│ 文件:lib/pages/index_module.dart │
│ └─ L54-55: r.child("/", child: (_) => const InitPage()) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 阶段 7:InitPage 渲染与初始化 │
│ 文件:lib/pages/init_page.dart │
│ ├─ L327-329: return const LoadingWidget() │
│ │ └─ Scaffold(body: Container()) ← 用户看到的第一个画面 │
│ ├─ L44-63: _initializeApp() 后台执行初始化任务 │
│ │ ├─ _migrateStorage() │
│ │ ├─ _loadShaders() │
│ │ ├─ _loadDanmakuShield() │
│ │ ├─ _webDavInit() │
│ │ ├─ downloadController.init() │
│ │ ├─ _checkRunningOnX11() │
│ │ ├─ _pluginInit() │
│ │ └─ _startDefaultPage() │
│ └─ L111-121: _startDefaultPage() │
│ └─ Modular.to.navigate('/tab/popular/') │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 阶段 8:第二次路由匹配 "/tab/popular/" │
│ 文件:lib/pages/index_module.dart │
│ ├─ L66-74: r.child("/tab", child: (_) => const IndexPage())│
│ └─ L71: children: menu.routes │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 阶段 9:menu.routes 匹配 │
│ 文件:lib/pages/router.dart │
│ ├─ L38-41: path: "/popular", module: PopularModule() │
│ ├─ L42-45: path: "/timeline", module: TimelineModule() │
│ ├─ L46-49: path: "/collect", module: CollectModule() │
│ └─ L50-53: path: "/my", module: MyModule() │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 阶段 10:IndexPage 渲染 │
│ 文件:lib/pages/index_page.dart │
│ └─ L17: return const ScaffoldMenu() │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 阶段 11:ScaffoldMenu 渲染 │
│ 文件:lib/pages/menu/menu.dart │
│ ├─ L65-74: ChangeNotifierProvider + NavigationBarState │
│ ├─ L77-120: bottomMenuWidget() 竖屏模式 │
│ │ ├─ NavigationBar (底部导航栏) │
│ │ │ ├─ 标签 0: "推荐" │
│ │ │ ├─ 标签 1: "时间表" │
│ │ │ ├─ 标签 2: "追番" │
│ │ │ └─ 标签 3: "我的" │
│ │ └─ PageView.builder │
│ │ └─ RouterOutlet() ← 当前路由对应的页面 │
│ └─ L122-197: sideMenuWidget() 横屏模式 │
│ └─ NavigationRail (侧边导航栏) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 阶段 12:PopularModule 加载 │
│ 文件:lib/pages/popular/popular_module.dart │
│ ├─ binds: i.addSingleton(PopularController.new) │
│ └─ routes: r.child("/", child: (_) => const PopularPage()) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 阶段 13:PopularPage 渲染 │
│ 文件:lib/pages/popular/popular_page.dart │
│ ├─ L96-164: build() 方法 │
│ │ ├─ PopScope (返回键处理) │
│ │ ├─ Scaffold (页面框架) │
│ │ ├─ CustomScrollView (滚动视图) │
│ │ │ ├─ buildSliverAppBar() (顶部栏) │
│ │ │ │ └─ 标题:"热门番组" │
│ │ │ ├─ LinearProgressIndicator (加载进度) │
│ │ │ └─ contentGrid() (内容网格) │
│ │ │ └─ SliverGrid │
│ │ │ └─ BangumiCardV × N (番剧卡片) │
│ │ └─ FloatingActionButton (回到顶部) │
│ └─ L44-46: initState() │
│ └─ popularController.queryBangumiByTrend() │
│ ← 加载热门番剧数据 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 阶段 14:用户看到主界面 │
│ ├─ 底部导航栏:推荐、时间表、追番、我的 │
│ ├─ 顶部标题:"热门番组" │
│ ├─ 内容区域:番剧卡片网格(3/5/6 列自适应) │
│ └─ 每个卡片包含:封面图 + 中文标题 │
└─────────────────────────────────────────────────────────────┘6.2 关键文件清单
| 文件名 | 路径 | 作用 | 关键行数 |
|---|---|---|---|
| main.dart | lib/ | 应用入口,负责所有初始化 | 14-198 |
| app_widget.dart | lib/ | 应用界面容器,配置 MaterialApp | 190-310 |
| app_module.dart | lib/ | 主模块配置,定义根路由 | 10-13 |
| index_module.dart | lib/pages/ | 首页模块,包含所有主要路由 | 52-80 |
| router.dart | lib/pages/ | 菜单路由定义 | 37-54 |
| init_page.dart | lib/pages/ | 初始化页面,执行后台初始化 | 20-340 |
| index_page.dart | lib/pages/ | 主界面包装器 | 15-19 |
| menu.dart | lib/pages/menu/ | 底部/侧边导航栏 | 8-198 |
| popular_module.dart | lib/pages/popular/ | 推荐页面模块 | 全部 |
| popular_page.dart | lib/pages/popular/ | 推荐页面 UI | 96-164 |
| bangumi_card.dart | lib/bean/card/ | 番剧卡片组件 | 9-104 |
6.3 关键设计模式
1. 依赖注入(Dependency Injection)
使用场景:
ModularApp+AppModule提供全局依赖注入容器PopularController通过Modular.get<PopularController>()获取
好处:
- ✅ 解耦组件之间的依赖关系
- ✅ 便于单元测试
- ✅ 支持单例模式
2. 模块化路由(Modular Routing)
使用场景:
AppModule→IndexModule→PopularModule- 每个功能模块独立管理自己的路由
好处:
- ✅ 代码组织清晰
- ✅ 易于维护和扩展
- ✅ 支持懒加载
3. 状态管理(State Management)
使用的方案:
- Provider:
ThemeProvider(主题管理) - MobX:
PopularController(响应式数据) - ChangeNotifier:
NavigationBarState(导航状态)
好处:
- ✅ 状态与 UI 分离
- ✅ 自动通知 UI 更新
- ✅ 易于调试
4. 观察者模式(Observer Pattern)
使用场景:
WidgetsBindingObserver:监听应用生命周期TrayListener:监听托盘事件WindowListener:监听窗口事件KazumiDialog.observer:对话框管理
好处:
- ✅ 解耦事件源和监听者
- ✅ 支持多个观察者
- ✅ 便于添加新功能
6.4 性能优化点
1. 懒加载(Lazy Loading)
// index_module.dart 第 68-70 行
child: (_) {
return const IndexPage();
},- ✅ 使用工厂函数延迟创建 Widget
- ✅ 只在需要时才实例化
2. 页面状态保持
// popular_page.dart 第 27-28 行
class _PopularPageState extends State<PopularPage>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
}- ✅ 切换标签时保持页面状态
- ✅ 避免重复加载数据
3. 响应式布局
// popular_page.dart 第 167-173 行
int crossCount = 3;
if (MediaQuery.sizeOf(context).width > LayoutBreakpoint.compact['width']!) {
crossCount = 5;
}
if (MediaQuery.sizeOf(context).width > LayoutBreakpoint.medium['width']!) {
crossCount = 6;
}- ✅ 根据屏幕宽度自动调整列数
- ✅ 充分利用屏幕空间
4. 防抖处理
// popular_page.dart 第 85-93 行
if (_lastPressedAt == null ||
DateTime.now().difference(_lastPressedAt!) >
const Duration(seconds: 2)) {
_lastPressedAt = DateTime.now();
KazumiDialog.showToast(message: "再按一次退出应用", context: context);
return;
}
SystemNavigator.pop();- ✅ 防止误触退出
- ✅ 提升用户体验
6.5 平台适配总结
移动端特有功能(Android/iOS)
// main.dart L27-38
if (Platform.isAndroid || Platform.isIOS) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setSystemUIOverlayStyle(...);
}
// popular_page.dart L291-306
if (Platform.isAndroid) {
FlutterDisplayMode.setPreferredMode(preferred);
}适配内容:
- ✅ 沉浸式全屏
- ✅ 高刷新率屏幕支持
- ✅ WebView 功能检查
桌面端特有功能(Windows/macOS/Linux)
// main.dart L117-152
if (Utils.isDesktop()) {
await windowManager.ensureInitialized();
WindowOptions windowOptions = WindowOptions(...);
windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
});
}
// app_widget.dart L192-193
if (Utils.isDesktop()) {
_handleTray();
}适配内容:
- ✅ 窗口管理(大小、标题栏样式)
- ✅ 托盘图标
- ✅ 窗口关闭事件处理
- ✅ 前后台状态监听
七、常见问题排查
Q1: 为什么启动时先看到空白页?
答案: 因为 InitPage 的 build() 方法返回 LoadingWidget,而 LoadingWidget 只是一个空的 Container()。
解决方案: 这是正常行为,目的是在后台执行初始化任务。如果觉得体验不好,可以在 InitPage 中添加加载动画或 Logo。
Q2: 为什么点击卡片后跳转到详情页?
答案: BangumiCardV 的 onTap 回调调用了 Modular.to.pushNamed('/info/', arguments: bangumiItem)。
调试方法: 检查 bangumi_card.dart 第 36 行,确认路由路径正确。
Q3: 如何修改默认启动页面?
答案: 在设置中修改 SettingBoxKey.defaultStartupPage 的值。
代码位置: init_page.dart 第 112-115 行
final defaultStartupPage = setting.get(
SettingBoxKey.defaultStartupPage,
defaultValue: '/tab/popular/',
);Q4: 底部导航栏为什么不显示?
可能原因:
NavigationBarState.isHide = true- 设备检测到横屏模式(显示侧边栏)
调试方法: 检查 menu.dart 第 88-89 行和第 69-70 行。
Q5: 如何添加新的底部导航标签?
步骤:
创建新页面模块
// lib/pages/newpage/newpage_module.dart class NewPageModule extends Module { @override void routes(r) { r.child("/", child: (_) => const NewPage()); } }添加到 router.dart
// lib/pages/router.dart 第 37-54 行 final MenuRoute menu = MenuRoute([ // ... 现有项 MenuRouteItem( path: "/newpage", module: NewPageModule(), ), ]);添加导航标签
// lib/pages/menu/menu.dart 第 91-111 行 NavigationDestination( selectedIcon: Icon(Icons.new_icon), icon: Icon(Icons.new_icon_outlined), label: '新页面', ),
八、总结
Kazumi 应用的启动流程是一个精心设计的、分阶段的复杂过程,涉及:
- Flutter 框架初始化
- 原生平台适配
- 存储系统初始化
- 网络模块配置
- Widget 层次构建
- 路由系统匹配
- 页面懒加载
- 状态管理
- UI 渲染
整个过程体现了以下设计原则:
- ✅ 模块化:不同功能独立成模块
- ✅ 解耦:通过依赖注入和路由系统降低耦合
- ✅ 性能优化:懒加载、状态保持、响应式布局
- ✅ 用户体验:平滑的过渡动画、合理的加载提示
- ✅ 平台适配:针对不同平台提供定制化功能
理解这个流程对于:
- 🔧 调试启动问题
- 🚀 优化启动速度
- 🎨 自定义 UI/UX
- 📦 添加新功能
都具有重要意义!
文档版本: v1.0
最后更新: 2026 年
维护者: AI Assistant