Initial Flutter project structure、
This commit is contained in:
@@ -0,0 +1,582 @@
|
||||
# `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` 构造函数或参数里有运行时内容。
|
||||
Reference in New Issue
Block a user