583 lines
9.2 KiB
Markdown
583 lines
9.2 KiB
Markdown
# `main.dart`、`app.dart` 和 `const App()` 说明
|
||
|
||
## `main.dart` 和 `app.dart` 应该放什么
|
||
|
||
以常见 Flutter App 项目结构来看,`main.dart` 和 `app.dart` 的职责应该分得很清楚:
|
||
|
||
- `main.dart`:负责“启动前准备”和“启动 App”
|
||
- `app.dart`:负责“描述整个 App 长什么样、用什么主题、从哪个页面开始、怎么路由”
|
||
|
||
## `main.dart` 应该放什么
|
||
|
||
`main.dart` 是程序入口,应该尽量薄。
|
||
|
||
最基础版本:
|
||
|
||
```dart
|
||
import 'package:flutter/material.dart';
|
||
|
||
import 'app/app.dart';
|
||
|
||
void main() {
|
||
runApp(const App());
|
||
}
|
||
```
|
||
|
||
如果项目刚开始,这样就够了。
|
||
|
||
## 稍微正式一点的 `main.dart`
|
||
|
||
如果后面需要初始化一些东西,可以这样:
|
||
|
||
```dart
|
||
import 'package:flutter/material.dart';
|
||
|
||
import 'app/app.dart';
|
||
|
||
Future<void> main() async {
|
||
WidgetsFlutterBinding.ensureInitialized();
|
||
|
||
runApp(const App());
|
||
}
|
||
```
|
||
|
||
`WidgetsFlutterBinding.ensureInitialized()` 的作用是:在 `runApp` 之前确保 Flutter 框架初始化完成。
|
||
|
||
什么时候需要它:
|
||
|
||
- 初始化 `SharedPreferences`
|
||
- 初始化本地数据库
|
||
- 初始化 Firebase
|
||
- 加载 `.env`
|
||
- 初始化本地存储
|
||
- 设置屏幕方向
|
||
- 初始化依赖注入
|
||
|
||
例如以后可能会变成:
|
||
|
||
```dart
|
||
Future<void> main() async {
|
||
WidgetsFlutterBinding.ensureInitialized();
|
||
|
||
await AppStorage.init();
|
||
await Firebase.initializeApp();
|
||
|
||
runApp(const App());
|
||
}
|
||
```
|
||
|
||
但现在不需要提前写这些。
|
||
|
||
## `main.dart` 不应该放什么
|
||
|
||
不建议在 `main.dart` 里放:
|
||
|
||
- 页面 UI
|
||
- `Scaffold`
|
||
- `AppBar`
|
||
- 首页布局
|
||
- 登录逻辑
|
||
- 网络请求
|
||
- 路由表
|
||
- 大量主题配置
|
||
- 业务状态管理
|
||
|
||
也就是说,不要让 `main.dart` 变成这样:
|
||
|
||
```dart
|
||
void main() {
|
||
runApp(MaterialApp(
|
||
home: Scaffold(
|
||
appBar: AppBar(...),
|
||
body: ...,
|
||
),
|
||
));
|
||
}
|
||
```
|
||
|
||
这只适合临时 Demo,不适合项目长期维护。
|
||
|
||
## `app.dart` 应该放什么
|
||
|
||
`app.dart` 放根组件,也就是整个应用壳子。
|
||
|
||
最基础版本:
|
||
|
||
```dart
|
||
import 'package:flutter/material.dart';
|
||
|
||
import '../features/home/home_page.dart';
|
||
import 'theme.dart';
|
||
|
||
class App extends StatelessWidget {
|
||
const App({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return MaterialApp(
|
||
title: 'Patbond',
|
||
debugShowCheckedModeBanner: false,
|
||
theme: AppTheme.light,
|
||
home: const HomePage(),
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
这里负责:
|
||
|
||
- App 标题
|
||
- 是否显示右上角 debug 标识
|
||
- 全局主题
|
||
- 首页入口
|
||
- 路由配置
|
||
- 本地化配置
|
||
- 全局导航配置
|
||
|
||
## 如果暂时没有路由系统
|
||
|
||
项目刚开始,页面少,可以这样:
|
||
|
||
```dart
|
||
return MaterialApp(
|
||
title: 'Patbond',
|
||
debugShowCheckedModeBanner: false,
|
||
theme: AppTheme.light,
|
||
home: const HomePage(),
|
||
);
|
||
```
|
||
|
||
这种最简单。
|
||
|
||
适合:
|
||
|
||
- 只有首页
|
||
- 页面数量很少
|
||
- 暂时不需要登录拦截
|
||
- 暂时不需要深链接
|
||
- 暂时不需要命名路由
|
||
|
||
## 如果使用 Flutter 自带命名路由
|
||
|
||
可以这样:
|
||
|
||
```dart
|
||
return MaterialApp(
|
||
title: 'Patbond',
|
||
debugShowCheckedModeBanner: false,
|
||
theme: AppTheme.light,
|
||
initialRoute: '/',
|
||
routes: {
|
||
'/': (_) => const HomePage(),
|
||
'/login': (_) => const LoginPage(),
|
||
'/profile': (_) => const ProfilePage(),
|
||
},
|
||
);
|
||
```
|
||
|
||
适合小到中等项目。
|
||
|
||
优点:
|
||
|
||
- 简单
|
||
- 不需要额外依赖
|
||
- 比 `Navigator.push` 字符串散落各处更统一
|
||
|
||
缺点:
|
||
|
||
- 登录拦截不够优雅
|
||
- 嵌套路由不方便
|
||
- Web URL 管理不如 `go_router`
|
||
|
||
## 如果以后使用 `go_router`
|
||
|
||
`app.dart` 会变成:
|
||
|
||
```dart
|
||
import 'package:flutter/material.dart';
|
||
|
||
import 'router.dart';
|
||
import 'theme.dart';
|
||
|
||
class App extends StatelessWidget {
|
||
const App({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return MaterialApp.router(
|
||
title: 'Patbond',
|
||
debugShowCheckedModeBanner: false,
|
||
theme: AppTheme.light,
|
||
routerConfig: appRouter,
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
然后 `router.dart` 单独管理路由。
|
||
|
||
这种适合:
|
||
|
||
- 页面多
|
||
- Web 地址栏需要正常变化
|
||
- 有登录态跳转
|
||
- 有底部 Tab
|
||
- 有深链接
|
||
- 有权限拦截
|
||
|
||
## `app.dart` 不应该放什么
|
||
|
||
不建议在 `app.dart` 里放:
|
||
|
||
- 某个页面的具体 UI
|
||
- 首页复杂布局
|
||
- 登录表单逻辑
|
||
- 网络请求实现
|
||
- 数据模型
|
||
- 业务判断细节
|
||
- 大量工具函数
|
||
|
||
`app.dart` 是应用壳,不是业务页面。
|
||
|
||
## 推荐当前项目的版本
|
||
|
||
当前项目还很新,建议先用最简单的版本。
|
||
|
||
目录:
|
||
|
||
```text
|
||
lib/
|
||
main.dart
|
||
app/
|
||
app.dart
|
||
theme.dart
|
||
features/
|
||
home/
|
||
home_page.dart
|
||
```
|
||
|
||
`main.dart`:
|
||
|
||
```dart
|
||
import 'package:flutter/material.dart';
|
||
|
||
import 'app/app.dart';
|
||
|
||
void main() {
|
||
runApp(const App());
|
||
}
|
||
```
|
||
|
||
`app/app.dart`:
|
||
|
||
```dart
|
||
import 'package:flutter/material.dart';
|
||
|
||
import '../features/home/home_page.dart';
|
||
import 'theme.dart';
|
||
|
||
class App extends StatelessWidget {
|
||
const App({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return MaterialApp(
|
||
title: 'Patbond',
|
||
debugShowCheckedModeBanner: false,
|
||
theme: AppTheme.light,
|
||
home: const HomePage(),
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
`app/theme.dart`:
|
||
|
||
```dart
|
||
import 'package:flutter/material.dart';
|
||
|
||
class AppTheme {
|
||
const AppTheme._();
|
||
|
||
static ThemeData get light {
|
||
return ThemeData(
|
||
colorScheme: ColorScheme.fromSeed(
|
||
seedColor: Colors.deepPurple,
|
||
),
|
||
useMaterial3: true,
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
`features/home/home_page.dart`:
|
||
|
||
```dart
|
||
import 'package:flutter/material.dart';
|
||
|
||
class HomePage extends StatelessWidget {
|
||
const HomePage({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return const Scaffold(
|
||
body: Center(
|
||
child: Text('Patbond'),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
## 职责总结
|
||
|
||
`main.dart`:
|
||
|
||
```text
|
||
负责启动
|
||
```
|
||
|
||
应该包含:
|
||
|
||
```text
|
||
runApp
|
||
启动前初始化
|
||
全局异常捕获入口
|
||
```
|
||
|
||
不应该包含:
|
||
|
||
```text
|
||
页面 UI
|
||
业务逻辑
|
||
路由细节
|
||
主题细节
|
||
网络请求
|
||
```
|
||
|
||
`app.dart`:
|
||
|
||
```text
|
||
负责 App 壳子
|
||
```
|
||
|
||
应该包含:
|
||
|
||
```text
|
||
MaterialApp
|
||
主题配置入口
|
||
路由配置入口
|
||
首页入口
|
||
本地化配置入口
|
||
```
|
||
|
||
不应该包含:
|
||
|
||
```text
|
||
具体页面布局
|
||
业务逻辑
|
||
接口请求
|
||
复杂状态处理
|
||
```
|
||
|
||
`home_page.dart`:
|
||
|
||
```text
|
||
负责首页 UI
|
||
```
|
||
|
||
应该包含:
|
||
|
||
```text
|
||
Scaffold
|
||
AppBar
|
||
body
|
||
首页交互
|
||
首页状态
|
||
```
|
||
|
||
## 关于 `const App()`
|
||
|
||
如果写:
|
||
|
||
```dart
|
||
import 'package:flutter/material.dart';
|
||
import 'app/app.dart';
|
||
|
||
void main() {
|
||
runApp(App());
|
||
}
|
||
```
|
||
|
||
IDE 提示要去掉或加上 `const`,通常是因为 `App` 构造函数是否支持 `const`。
|
||
|
||
在 Flutter 里,如果一个 Widget 可以在编译期确定,推荐写成:
|
||
|
||
```dart
|
||
runApp(const App());
|
||
```
|
||
|
||
而不是:
|
||
|
||
```dart
|
||
runApp(App());
|
||
```
|
||
|
||
## 正确写法
|
||
|
||
`main.dart`:
|
||
|
||
```dart
|
||
import 'package:flutter/material.dart';
|
||
|
||
import 'app/app.dart';
|
||
|
||
void main() {
|
||
runApp(const App());
|
||
}
|
||
```
|
||
|
||
`app/app.dart` 里面要这样写:
|
||
|
||
```dart
|
||
import 'package:flutter/material.dart';
|
||
|
||
class App extends StatelessWidget {
|
||
const App({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return MaterialApp(
|
||
title: 'Patbond',
|
||
debugShowCheckedModeBanner: false,
|
||
home: const Scaffold(
|
||
body: Center(
|
||
child: Text('Patbond'),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
关键是这一行:
|
||
|
||
```dart
|
||
const App({super.key});
|
||
```
|
||
|
||
如果 `App` 类里没有 `const` 构造函数,比如是这样:
|
||
|
||
```dart
|
||
class App extends StatelessWidget {
|
||
App({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return MaterialApp(...);
|
||
}
|
||
}
|
||
```
|
||
|
||
那 `runApp(const App());` 就不能用,IDE 可能会提示去掉 `const`。
|
||
|
||
## 为什么 Flutter 推荐 `const`
|
||
|
||
`const` 的好处是:
|
||
|
||
- 编译期创建对象
|
||
- 减少运行时重复创建 Widget
|
||
- Flutter rebuild 时更容易优化
|
||
- 代码语义更清楚:这个 Widget 本身没有可变状态
|
||
|
||
对于 `StatelessWidget`,只要构造参数也都是可 `const` 的,一般都应该写 `const` 构造函数。
|
||
|
||
## 看到“让我去掉 const”的常见原因
|
||
|
||
### 原因 1:`App` 构造函数不是 `const`
|
||
|
||
错误:
|
||
|
||
```dart
|
||
class App extends StatelessWidget {
|
||
App({super.key});
|
||
}
|
||
```
|
||
|
||
正确:
|
||
|
||
```dart
|
||
class App extends StatelessWidget {
|
||
const App({super.key});
|
||
}
|
||
```
|
||
|
||
### 原因 2:`App` 里面继承的不是 Widget,或者写法不完整
|
||
|
||
正确:
|
||
|
||
```dart
|
||
class App extends StatelessWidget {
|
||
const App({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return const MaterialApp();
|
||
}
|
||
}
|
||
```
|
||
|
||
### 原因 3:`App` 构造函数里有非 const 默认值或初始化逻辑
|
||
|
||
例如:
|
||
|
||
```dart
|
||
class App extends StatelessWidget {
|
||
App({super.key}) {
|
||
print('init');
|
||
}
|
||
}
|
||
```
|
||
|
||
这种就不能是 `const` 构造函数。
|
||
|
||
### 原因 4:`App` 构造函数接收了运行时对象
|
||
|
||
例如:
|
||
|
||
```dart
|
||
class App extends StatelessWidget {
|
||
final DateTime now;
|
||
|
||
const App({super.key, required this.now});
|
||
}
|
||
```
|
||
|
||
调用时不能这样:
|
||
|
||
```dart
|
||
runApp(const App(now: DateTime.now()));
|
||
```
|
||
|
||
因为 `DateTime.now()` 不是编译期常量。
|
||
|
||
## 结论
|
||
|
||
优先写成:
|
||
|
||
```dart
|
||
void main() {
|
||
runApp(const App());
|
||
}
|
||
```
|
||
|
||
并确保 `App` 里面是:
|
||
|
||
```dart
|
||
class App extends StatelessWidget {
|
||
const App({super.key});
|
||
}
|
||
```
|
||
|
||
如果这样之后 IDE 还提示去掉 `const`,说明 `App` 构造函数或参数里有运行时内容。
|