一个基于 Flutter 的快速开发脚手架,已预置屏幕适配、路由管理、页面生命周期封装、本地存储工具、状态管理(Riverpod)、数据模型(freezed)和通用组件等常用的基础能力,让你专注于业务逻辑的实现。
在开始之前,请确保已安装以下开发环境:
| 工具 | 版本要求 | 说明 |
|---|---|---|
| Flutter SDK | >= 3.5.0 | Dart SDK ^3.5.0 |
| Dart SDK | ^3.5.0 | 一般随 Flutter SDK 一起安装 |
| Android Studio / VS Code | 最新版 | Flutter 官方 IDE 任选其一 |
| Xcode (可选) | 最新版 | 如需在 iOS / macOS 上运行 |
检查环境是否就绪:
flutter doctorflutter pub get# 默认设备运行
flutter run
# 指定设备运行
flutter devices # 查看可用设备
flutter run -d <device_id> # 指定设备# Android
flutter build apk --release
flutter build appbundle --release
# iOS
flutter build ios --release
# Web
flutter build web --releaseflutter_demo/
├── android/ # Android 平台原生工程
├── ios/ # iOS 平台原生工程
├── macos/ # macOS 平台原生工程
├── windows/ # Windows 平台原生工程
├── linux/ # Linux 平台原生工程
├── web/ # Web 平台原生工程
├── assets/
│ └── images/ # 图片资源目录
├── lib/
│ ├── main.dart # 应用入口,全局初始化(SpUtil + Riverpod + ScreenUtilInit)
│ ├── route.dart # 全局路由配置(基于 go_router)
│ ├── api/
│ │ └── models/ # 数据模型(freezed)
│ │ └── login_response.dart # 登录响应模型
│ ├── base/
│ │ ├── base_page_state.dart # 页面 State 基类,封装生命周期
│ │ └── consumer_base_page_state.dart # Riverpod 版本页面 State 基类
│ ├── components/
│ │ └── common_title_bar.dart # 通用标题栏组件
│ ├── constants/
│ │ └── refresh_config.dart # EasyRefresh 全局样式
│ ├── pages/ # 业务页面
│ │ ├── home/ # 首页
│ │ ├── second/ # 第二页
│ │ ├── splash/ # 启动页
│ │ ├── login/ # 登录页(Riverpod)
│ │ └── list_demo/ # 列表演示(Riverpod + EasyRefresh)
│ └── utils/
│ └── sp_util.dart # SharedPreferences 封装工具类
├── test/ # 测试目录
├── pubspec.yaml # 依赖与配置
└── README.md
本项目使用 flutter_screenutil 进行屏幕适配,已在 main.dart 中初始化:
// lib/main.dart
ScreenUtilInit(
designSize: const Size(360, 690), // UI 设计稿基准尺寸(宽 x 高)
minTextAdapt: true, // 文字最小自适应
splitScreenMode: true, // 支持分屏模式
builder: (context, child) => MaterialApp.router(
routerConfig: global_router,
),
);设计稿基准为 360 × 690,在使用 .w(宽度)、.h(高度)、.r(圆角)、.sp(字号)等扩展单位时,会按比例自动换算:
import 'package:flutter_screenutil/flutter_screenutil.dart';
SizedBox(
width: 330.w, // 按设计稿宽度比例缩放
height: 50.h, // 按设计稿高度比例缩放
child: Text(
"Hello",
style: TextStyle(fontSize: 14.sp), // 文字自适应
),
)💡 设计稿尺寸变更时,只需修改
main.dart中的designSize。
本项目使用 go_router 管理路由,全局路由配置集中在 lib/route.dart:
// lib/route.dart
final global_router = GoRouter(
observers: [routeObserver],
routes: [
GoRoute(path: '/', builder: (context, state) => HomePage()),
GoRoute(path: '/second', builder: (context, state) => SecondPage(state: state)),
],
);- 通过声明式
routes集中管理路由。 - 无需使用
Navigator.push,只需调用context.push('/path')。 - 可通过
state.uri.queryParameters接收参数。 routeObserver用作页面监听,配合BasePageState实现页面生命周期感知。
lib/base/base_page_state.dart 仿照 Android Activity 的生命周期模型,封装了 4 个便于业务使用的钩子方法:
| 钩子方法 | 触发时机 |
|---|---|
onCreate() |
State 创建时(仅一次),适合做一次性初始化 |
onStart() |
页面首次可见,或从其它页面返回后重新可见 |
onStop() |
页面被其它页面盖住时(didPushNext) |
onDestroy() |
State 销毁时,适合释放资源、解注册监听等 |
📌 关于首次进入:首次进入页面时
onCreate()与onStart()都会被各调用一次(基类在initState中依次触发);之后只有当页面从其它页面返回时(didPopNext),onStart()才会再次触发,onCreate()不会再触发。
继承 BasePageState 即可使用:
import 'package:flutter_demo/base/base_page_state.dart';
class _HomePageState extends BasePageState<HomePage> {
@override
void onCreate() {
super.onCreate();
// 只在页面创建时执行一次,例如:网络请求、订阅事件
}
@override
void onStart() {
super.onStart();
// 页面可见时执行,例如:刷新数据、恢复轮播
}
@override
void onStop() {
super.onStop();
// 页面不可见时执行,例如:暂停动画、暂停计时器
}
@override
Widget build(BuildContext context) {
return Scaffold(/* ... */);
}
}
⚠️ BasePageState已与RouteObserver关联,请直接在 build 方法中编写页面 UI,不要在onCreate中调用 setState 或修改 UI 状态。
📌 如果页面需要使用 Riverpod 状态管理,请继承
ConsumerBasePageState(lib/base/consumer_base_page_state.dart),生命周期钩子完全一致,同时获得WidgetRef ref用于ref.watch/ref.read。
本项目已集成 flutter_riverpod 作为状态管理方案。入口 main.dart 已包裹 ProviderScope。
每个需要共享状态的页面,将逻辑抽离到独立的 Provider 文件中:
lib/pages/<page>/
├── xxx_provider.dart # 状态定义 + 业务逻辑(Notifier)
└── xxx_page.dart # 纯 UI,通过 ref.watch / ref.read消费
示例:登录页的 Provider(lib/pages/login/login_provider.dart):
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'login_provider.freezed.dart';
@freezed
class LoginState with _$LoginState {
const factory LoginState({
@Default(false) bool isLoading,
}) = _LoginState;
}
class LoginNotifier extends StateNotifier<LoginState> {
LoginNotifier() : super(const LoginState());
Future<void> login(String phone, String password) async {
state = state.copyWith(isLoading: true); // 改数据 = 自动通知 UI
await Future.delayed(const Duration(seconds: 1));
state = state.copyWith(isLoading: false); // 改数据 = 自动通知 UI
}
}
final loginProvider = StateNotifierProvider<LoginNotifier, LoginState>(
(ref) => LoginNotifier(),
);页面消费(lib/pages/login/login_page.dart):
class _LoginPageState extends ConsumerBasePageState<LoginPage> {
@override
Widget build(BuildContext context) {
final isLoading = ref.watch(loginProvider).isLoading; // 订阅状态
return ElevatedButton(
onPressed: isLoading
? null
: () => ref.read(loginProvider.notifier).login(...), // 触发操作
child: isLoading ? 转圈 : Text("登录"),
);
}
}核心原则:
ref.watch(provider)— 在build中订阅,状态变时自动重建ref.read(provider.notifier).xxx()— 在事件回调中触发操作ref.listen(provider, callback)— 监听状态变化执行副作用(如跳转、弹 SnackBar)
本项目使用 freezed 定义不可变数据模型,自动生成 copyWith、==、hashCode、toString()、fromJson / toJson。
示例(lib/api/models/login_response.dart):
import 'package:freezed_annotation/freezed_annotation.dart';
part 'login_response.freezed.dart';
part 'login_response.g.dart';
@freezed
class LoginResponse with _$LoginResponse {
const factory LoginResponse({
required String token,
required UserInfo userInfo,
}) = _LoginResponse;
factory LoginResponse.fromJson(Map<String, dynamic> json) =>
_$LoginResponseFromJson(json);
}
@freezed
class UserInfo with _$UserInfo {
const factory UserInfo({
required int id,
required String nickname,
required String avatar,
}) = _UserInfo;
factory UserInfo.fromJson(Map<String, dynamic> json) =>
_$UserInfoFromJson(json);
}自动获得的能力:
final user = UserInfo(id: 1, nickname: '张三', avatar: 'https://...');
// JSON 互转
final json = user.toJson();
final parsed = UserInfo.fromJson(json);
// 不可变修改
final updated = user.copyWith(nickname: '李四');
// 值比较
user == user.copyWith(); // true
// 调试友好
print(user); // UserInfo(id: 1, nickname: 张三, avatar: https://...)💡 修改模型后需运行
dart run build_runner build --delete-conflicting-outputs重新生成代码。
本项目使用 shared_preferences,并封装了 SpUtil 工具类(lib/utils/sp_util.dart),以单例模式提供简洁类型安全的 API。
已在 lib/main.dart 中完成:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await SpUtil.init(); // 全局初始化,仅需一次
runApp(const MyApp());
}// --- 基础类型 ---
SpUtil.getString('key') // → String?
SpUtil.setString('key', 'value')
SpUtil.getInt('key') // → int?
SpUtil.setInt('key', 123)
SpUtil.getDouble('key') // → double?
SpUtil.setDouble('key', 3.14)
SpUtil.getBool('key') // → bool?
SpUtil.setBool('key', true)
SpUtil.getStringList('key') // → List<String>?
SpUtil.setStringList('key', ['a', 'b'])
// --- JSON 对象存取(自动序列化/反序列化) ---
SpUtil.getObject('key') // → dynamic(自动 jsonDecode)
SpUtil.setObject('key', {'name': 'Tom'}) // 自动 jsonEncode
// --- 通用简写(自动类型推断) ---
SpUtil.get<Map>('key') // 泛型读取
SpUtil.put('key', value) // 自动选择存储方式
// --- 其他操作 ---
SpUtil.remove('key') // 删除指定键
SpUtil.clear() // 清空所有数据
SpUtil.containsKey('key') // → bool 检查是否存在
SpUtil.isInitialized() // → bool 是否已初始化所有 key 通过 SpKeys 类统一管理,避免拼写错误:
SpKeys.userToken // → 'user_token'
SpKeys.userInfo // → 'user_info'
SpKeys.isLoggedIn // → 'is_logged_in'
SpKeys.themeMode // → 'theme_mode'
SpKeys.language // → 'language'位于 lib/components/common_title_bar.dart,统一管理页面顶部导航栏样式,替代原生 AppBar:
Scaffold(
body: SafeArea(
child: Column(
children: [
CommonTitleBar(title: "页面标题"), // 自动判断是否显示返回按钮
Expanded(child: ...),
],
),
),
)| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
title |
String |
必填 | 标题文字 |
showBack |
bool |
true |
是否显示返回按钮(自动根据 canPop() 判断) |
backgroundColor |
Color? |
主题色 | 背景色 |
actions |
List<Widget>? |
null |
右侧操作按钮组 |
onBackTap |
VoidCallback? |
context.pop() |
自定义返回回调 |
位于 lib/constants/refresh_config.dart,easy_refresh 全局样式配置,避免每个页面重复写 header / footer:
EasyRefresh(
header: RefreshConfig.header, // 下拉刷新文案(中文)
footer: RefreshConfig.footer, // 上拉加载文案(中文)
onRefresh: _onRefresh,
onLoad: _onLoad,
child: ListView.builder(...),
)💡 新增通用组件放入
lib/components/,全局常量放入lib/constants/。
需要共享状态的页面,采用「Provider 单独文件 + Page 纯 UI」模式。以登录页为例:
1. 创建 Provider 文件:
// lib/pages/login/login_provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'login_provider.freezed.dart';
@freezed
class LoginState with _$LoginState {
const factory LoginState({@Default(false) bool isLoading}) = _LoginState;
}
class LoginNotifier extends StateNotifier<LoginState> {
LoginNotifier() : super(const LoginState());
Future<void> login(String phone, String password) async {
state = state.copyWith(isLoading: true);
// ... 业务逻辑
state = state.copyWith(isLoading: false);
}
}
final loginProvider = StateNotifierProvider<LoginNotifier, LoginState>(
(ref) => LoginNotifier(),
);2. 创建页面文件,继承 ConsumerBasePageState:
// lib/pages/login/login_page.dart
class _LoginPageState extends ConsumerBasePageState<LoginPage> {
@override
Widget build(BuildContext context) {
final isLoading = ref.watch(loginProvider).isLoading; // watch = 订阅
return ElevatedButton(
onPressed: isLoading ? null
: () => ref.read(loginProvider.notifier).login(...), // read = 触发
child: isLoading ? 转圈 : Text("登录"),
);
}
}💡 再次强调:
build中用ref.watch订阅,事件回调中用ref.read触发。不需要setState,不需要if (!mounted) return。
下面演示如何新增一个 ThirdPage,包含以下三步:
在 lib/pages/ 下新建 third_page.dart:
// lib/pages/third_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_demo/base/base_page_state.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class ThirdPage extends StatefulWidget {
const ThirdPage({super.key});
@override
State<ThirdPage> createState() => _ThirdPageState();
}
class _ThirdPageState extends BasePageState<ThirdPage> {
@override
void onCreate() {
super.onCreate();
print('ThirdPage onCreate');
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
const CommonTitleBar(title: 'Third Page'),
Expanded(
child: Center(
child: SizedBox(
width: 200.w,
height: 80.h,
child: const Text('这是第三个页面'),
),
),
),
],
),
),
);
}
}对于复杂页面,可以像首页 HomePage 一样在 lib/pages/ 下创建子目录分层管理:
lib/pages/
├── home/
│ └── home_page.dart # 首页
├── second_page.dart # 第二页
└── third/
├── third_page.dart # 第三页
└── widgets/ # 该页面专属 Widget
打开 lib/route.dart 新增一条路由:
import 'package:flutter_demo/pages/third_page.dart';
final global_router = GoRouter(
observers: [routeObserver],
routes: [
GoRoute(path: '/', builder: (context, state) => HomePage()),
GoRoute(path: '/second', builder: (context, state) => SecondPage(state: state)),
GoRoute(path: '/third', builder: (context, state) => const ThirdPage()),
],
);context.push('/third');跳转:
context.push('/second?id=2&name=flutter');接收(在 SecondPage 的 State 中,注意 ! 非空断言):
final id = widget.state.uri.queryParameters['id']!;
final name = widget.state.uri.queryParameters['name']!;💡 实际项目中可参考
lib/pages/second_page.dart:print("参数id=" + widget.state.uri.queryParameters['id']!);
脚手架默认未演示。最稳妥的做法是让目标 StatefulWidget 接收构造参数,并在路由 builder 中从 state.extra 取值:
// 1) StatefulWidget 增加构造参数
class ThirdPage extends StatefulWidget {
final int id;
final String name;
const ThirdPage({super.key, required this.id, required this.name});
@override
State<ThirdPage> createState() => _ThirdPageState();
}
// 2) 路由声明时从 state.extra 中取出
GoRoute(
path: '/third',
builder: (context, state) {
final args = (state.extra as Map<String, dynamic>?) ?? <String, dynamic>{};
return ThirdPage(
id: args['id'] as int? ?? 0,
name: args['name'] as String? ?? '',
);
},
),
// 3) 跳转
context.push('/third', extra: {'id': 1, 'name': 'ice'});占位路由语法来自
go_router,本脚手架默认仅演示 query 传参,按需自行扩展。
GoRoute(
path: '/user/:id',
builder: (context, state) => UserPage(id: state.pathParameters['id']!),
)跳转:
context.push('/user/1001');# 安装依赖
flutter pub get
# 升级依赖
flutter pub upgrade
# 查看过期依赖
flutter pub outdated
# 清理构建产物
flutter clean
# 静态分析(lint)
flutter analyze
# 运行测试
flutter test
# 格式化代码
flutter format lib/
# 启动应用到连接的设备
flutter run
# 热重启(终端中按 R)
# 热重载(终端中按 r)
# 代码生成监听(freezed / json_serializable,改模型自动生成)
dart run build_runner watch -d
# 代码生成(手动一次)
dart run build_runner build --delete-conflicting-outputs已内置的能力:屏幕适配、路由管理、页面生命周期、本地存储、状态管理(Riverpod)、数据模型(freezed)、通用组件。
以下能力按需通过 flutter pub add <pkg> 添加:
- 网络请求:引入
dio,在lib/下新建api/目录统一封装。 - 全局通用 Widget:放入
lib/components/沉淀。 - 图片加载优化:引入
cached_network_image缓存网络图片。
🎉 Happy coding with Flutter!