Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Flutter Demo 脚手架使用教程

一个基于 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 doctor

快速开始

1. 获取依赖

flutter pub get

2. 运行项目

# 默认设备运行
flutter run

# 指定设备运行
flutter devices                    # 查看可用设备
flutter run -d <device_id>         # 指定设备

3. 构建发布版本

# Android
flutter build apk --release
flutter build appbundle --release

# iOS
flutter build ios --release

# Web
flutter build web --release

项目结构

flutter_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。

状态管理(Riverpod)

本项目已集成 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)

本项目使用 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 API 速查

// --- 基础类型 ---
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 是否已初始化

SpKeys 常量管理

所有 key 通过 SpKeys 类统一管理,避免拼写错误:

SpKeys.userToken    // → 'user_token'
SpKeys.userInfo     // → 'user_info'
SpKeys.isLoggedIn   // → 'is_logged_in'
SpKeys.themeMode    // → 'theme_mode'
SpKeys.language     // → 'language'

通用组件

CommonTitleBar

位于 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() 自定义返回回调

RefreshConfig

位于 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/。

开发指南

新增 Riverpod 页面

需要共享状态的页面,采用「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,包含以下三步:

1. 创建页面文件

在 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

2. 注册路由

打开 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()),
  ],
);

3. 在其它页面中跳转

context.push('/third');

页面跳转与参数传递

方式一:Query 参数(脚手架内置示例)

跳转:

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!

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages