封装与复用
在 Flutter 开发中,封装就是把重复出现的逻辑或 UI 片段提取出来,变成可复用的独立单元。封装的好处很直接:少写重复代码、改一处全局生效、降低出错率。
封装的四个层次
| 层次 | 封装对象 | 典型手段 | 复用范围 |
|---|---|---|---|
| 1 | 通用方法/计算 | 工具类(static 方法) | 全项目 |
| 2 | 共享行为 | Mixin | 跨类复用 |
| 3 | 已有类型的增强 | Extension | 增强已有类 |
| 4 | 可复用 UI 片段 | 自定义 Widget | 多页面复用 |
一、工具类封装(通用方法)
当你发现同一个计算逻辑出现在多个地方时,就该提取成工具方法。
// lib/utils/date_formatter.dart
class DateFormatter {
/// 将 DateTime 格式化为 yyyy-MM-dd
static String formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
}
/// 友好时间:刚刚 / x分钟前 / x小时前
static String timeAgo(DateTime date) {
final diff = DateTime.now().difference(date);
if (diff.inMinutes < 1) return '刚刚';
if (diff.inHours < 1) return '${diff.inMinutes}分钟前';
if (diff.inDays < 1) return '${diff.inHours}小时前';
return formatDate(date);
}
}// lib/utils/validators.dart
class Validators {
static String? required(String? value, [String label = '此项']) {
if (value == null || value.trim().isEmpty) return '$label不能为空';
return null;
}
static String? email(String? value) {
if (value == null || value.isEmpty) return '请输入邮箱';
final regex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!regex.hasMatch(value)) return '邮箱格式不正确';
return null;
}
}工具类要点
- 方法用
static修饰,不需要创建实例就能调用 - 类名要语义清晰,如
DateFormatter、Validators - 放在
lib/utils/目录下,按职责拆分文件
二、Mixin 封装(共享行为)
当多个类需要相同的字段或方法,但又不在同一条继承链上时,用 Mixin。
// lib/mixins/loading_mixin.dart
mixin LoadingMixin on State {
bool _isLoading = false;
bool get isLoading => _isLoading;
void showLoading() => setState(() => _isLoading = true);
void hideLoading() => setState(() => _isLoading = false);
/// 便捷方法:执行异步操作时自动管理 loading 状态
Future<T> withLoading<T>(Future<T> Function() action) async {
showLoading();
try {
return await action();
} finally {
hideLoading();
}
}
}在多个页面中复用:
class _HomePageState extends State<HomePage> with LoadingMixin {
Future<void> loadData() async {
await withLoading(() async {
final data = await api.fetchData();
// 处理数据...
});
}
@override
Widget build(BuildContext context) {
if (isLoading) return const CircularProgressIndicator();
return ListView(...);
}
}Mixin 要点
- 用
mixin关键字声明,而不是class on State限定只能用于 State 子类,这样才能调用setState- 一个类可以
with多个 Mixin,比继承更灵活
三、Extension 封装(增强已有类型)
当你想给别人写的类(如 String、BuildContext、DateTime)添加方法,又不能修改源码时,用 Extension。
// lib/extensions/string_extension.dart
extension StringX on String {
bool get isPhone => RegExp(r'^1[3-9]\d{9}$').hasMatch(this);
bool get isEmail => RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(this);
String get capitalize => isEmpty ? '' : '${this[0].toUpperCase()}${substring(1)}';
String ellipsis(int maxLen) => length > maxLen ? '${substring(0, maxLen)}...' : this;
}// lib/extensions/context_extension.dart
extension ContextX on BuildContext {
ThemeData get theme => Theme.of(this);
Size get screenSize => MediaQuery.sizeOf(this);
double get bottomPadding => MediaQuery.viewPaddingOf(this).bottom;
void showSnackBar(String message) {
ScaffoldMessenger.of(this).showSnackBar(SnackBar(content: Text(message)));
}
}Extension 要点
- 不修改原类代码,无侵入地增强已有类型
- 放在
lib/extensions/目录下 - 注意:Extension 方法不能访问私有成员
四、Widget 封装(可复用 UI 组件)
当一段 UI 在多个页面重复出现时,提取为自定义 Widget。
// lib/widgets/state_view.dart
enum ViewState { loading, empty, error, content }
class StateView extends StatelessWidget {
const StateView({
super.key,
required this.state,
required this.builder,
this.emptyText = '暂无数据',
this.errorText = '加载失败',
this.onRetry,
});
final ViewState state;
final WidgetBuilder builder;
final String emptyText;
final String errorText;
final VoidCallback? onRetry;
@override
Widget build(BuildContext context) {
return switch (state) {
ViewState.loading => const Center(child: CircularProgressIndicator()),
ViewState.empty => Center(child: Text(emptyText)),
ViewState.error => Center(child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(errorText),
if (onRetry != null) ElevatedButton(onPressed: onRetry, child: Text('重试')),
],
)),
ViewState.content => builder(context),
};
}
}// lib/widgets/primary_button.dart
class PrimaryButton extends StatelessWidget {
const PrimaryButton({
super.key,
required this.onPressed,
required this.text,
this.isLoading = false,
});
final VoidCallback? onPressed;
final String text;
final bool isLoading;
@override
Widget build(BuildContext context) {
return FilledButton(
onPressed: isLoading ? null : onPressed,
child: isLoading
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
: Text(text),
);
}
}Widget 封装要点
- 至少 2-3 处复用时才提取,不要过度封装
- 通过构造函数参数控制变化的部分(如文案、颜色、回调)
- 不变的部分在 Widget 内部固定(如间距、字号、图标)
- 放在
lib/widgets/目录下
五、服务类封装(业务逻辑)
当某个功能涉及多步操作或维护内部状态时,用服务类封装。
// lib/services/storage_service.dart
class StorageService {
static StorageService? _instance;
late SharedPreferences _prefs;
static Future<StorageService> get instance async {
_instance ??= StorageService._();
await _instance!._init();
return _instance!;
}
StorageService._();
Future<void> _init() async {
_prefs = await SharedPreferences.getInstance();
}
String? getToken() => _prefs.getString('token');
Future<void> setToken(String token) => _prefs.setString('token', token);
bool get isLoggedIn => getToken() != null;
}六、包的导出与引用
封装好了代码,还得让其他文件能方便地使用。这就涉及 Dart 的 import(导入)和 export(导出)机制。
import — 导入别人的代码
Dart 有两种导入方式:
// 1. package: 导入(最常用)—— 从当前项目的 lib/ 或第三方包导入
import 'package:my_app/utils/validators.dart'; // 自己项目的文件
import 'package:dio/dio.dart'; // 第三方包
// 2. 相对路径导入 —— 从当前文件的相对位置导入
import '../utils/validators.dart'; // 上级目录的文件
import 'src/internal_helper.dart'; // 子目录的文件使用规则:
| 场景 | 推荐方式 | 原因 |
|---|---|---|
导入 lib/ 下的文件 | package: | 不受文件移动影响,路径稳定 |
导入 lib/ 外的文件(如 test/) | 相对路径 | package: 只能访问 lib/ |
| 导入第三方包 | package: | 唯一方式 |
给导入起别名(解决命名冲突):
import 'package:http/http.dart' as http; // 别名 http
// 使用时
http.get(Uri.parse('...'));选择性导入(只导入部分内容):
// 只导入 show 后面列出的内容
import 'package:flutter/material.dart' show Colors, Icons;
// 隐藏 hide 后面列出的内容(避免冲突)
import 'package:flutter/material.dart' hide Colors;export — 导出自己的公共 API
export 的作用是把多个文件的公开内容统一汇总到一个入口文件,让使用者只需导入一个文件就够了。
问题:没有 export 时
// 使用者需要记住每个文件的路径,逐一导入
import 'package:my_app/utils/date_formatter.dart';
import 'package:my_app/utils/validators.dart';
import 'package:my_app/utils/number_formatter.dart';解决:用 export 做 barrel 文件
创建一个汇总文件(通常叫 xxx.dart 或 xxx/index.dart),把所有公共 API 集中导出:
// lib/utils/utils.dart(barrel 文件)
export 'date_formatter.dart';
export 'validators.dart';
export 'number_formatter.dart';使用者只需一行导入:
import 'package:my_app/utils/utils.dart'; // 一个入口搞定
// 现在可以直接用 DateFormatter、Validators、NumberFormatter实际项目中的 barrel 文件组织
lib/
├── utils/
│ ├── utils.dart ← barrel 文件(汇总导出)
│ ├── date_formatter.dart ← 具体实现
│ ├── validators.dart ← 具体实现
│ └── number_formatter.dart ← 具体实现
├── widgets/
│ ├── widgets.dart ← barrel 文件
│ ├── state_view.dart
│ ├── primary_button.dart
│ └── loading_indicator.dart
└── extensions/
├── extensions.dart ← barrel 文件
├── string_extension.dart
└── context_extension.dart使用时:
import 'package:my_app/utils/utils.dart';
import 'package:my_app/widgets/widgets.dart';
import 'package:my_app/extensions/extensions.dart';barrel 文件命名约定
- 文件夹名 +
.dart:utils/utils.dart、widgets/widgets.dart - 或
index.dart:utils/index.dart(导入时写package:my_app/utils/)
选择性导出
// 只导出部分内容
export 'date_formatter.dart' show DateFormatter;
export 'validators.dart' show Validators, email;
// 隐藏内部实现
export 'storage_service.dart' hide StorageServiceImpl;创建自己的包
当你的封装代码需要在多个项目中复用时,可以把它提取为独立的 Dart 包。
创建包项目
# 创建一个 Dart 包(纯 Dart 代码,无平台依赖)
dart create -t package my_package
# 创建一个 Flutter 插件(包含平台原生代码)
flutter create --template=plugin --platforms=android,ios,ohos my_plugin包项目结构
my_package/
├── lib/ # ⭐ 公共代码(别人 import 的就是这里)
│ ├── my_package.dart # 主入口(barrel 文件)
│ ├── src/ # 内部实现
│ │ ├── calculator.dart
│ │ └── formatter.dart
│ └── ...
├── test/ # 测试
├── pubspec.yaml # 包配置
└── README.md关键:lib/ 目录是公共 API,lib/src/ 是内部实现。
// lib/my_package.dart(主入口,导出公共 API)
export 'src/calculator.dart';
export 'src/formatter.dart';pubspec.yaml 配置
name: my_package # 包名(import 时用这个名字)
description: 我的工具包
version: 1.0.0
environment:
sdk: ^3.6.0
dependencies:
# 这个包依赖的其他包
http: ^1.2.0
dev_dependencies:
# 只在开发和测试时用的包
test: ^1.25.0在项目中引用本地包
开发阶段,包还没发布到 pub.dev,可以通过路径引用本地包:
方式一:路径引用(推荐开发时使用)
my_workspace/
├── my_app/ # 你的应用项目
│ ├── pubspec.yaml
│ └── lib/
└── my_package/ # 你的包项目
├── pubspec.yaml
└── lib/在 my_app/pubspec.yaml 中:
dependencies:
my_package:
path: ../my_package # 相对路径指向本地包使用时与普通包完全一致:
import 'package:my_package/my_package.dart';方式二:Git 引用
dependencies:
my_package:
git:
url: https://github.com/username/my_package.git
ref: v1.0.0 # 分支名、tag 或 commit hash方式三:pub.dev 引用
dependencies:
my_package: ^1.0.0 # 从 pub.dev 下载发布包到 pub.dev
当包开发完成并测试通过后,可以发布到 pub.dev 供全球开发者使用。
发布前检查
# 1. 检查包是否有问题
dart pub publish --dry-run
# 2. 确认以下内容
# - pubspec.yaml 中填写了 description、homepage/repository
# - 有 LICENSE 文件
# - 有 README.md
# - 版本号符合语义化版本(SemVer):主版本.次版本.修订号发布
# 正式发布
dart pub publish注意
- pub.dev 上的包名一旦发布就永久占用,不能删除
- 每次发布版本号必须递增
- 发布后无法撤回,只能发布新版本修复
版本号规则(语义化版本)
主版本.次版本.修订号
1.0.0 首次正式发布
1.0.1 修复 bug(向后兼容)
1.1.0 新增功能(向后兼容)
2.0.0 破坏性变更(不向后兼容)在 pubspec.yaml 中声明版本:
version: 1.0.0导入导出速查
| 需求 | 做法 |
|---|---|
| 导入自己项目的文件 | import 'package:my_app/xxx.dart' |
| 导入第三方包 | import 'package:dio/dio.dart' |
| 汇总导出多个文件 | 创建 barrel 文件用 export |
| 隐藏内部实现 | 代码放 lib/src/,只 export 公共部分 |
| 引用本地未发布的包 | pubspec.yaml 中用 path: 引用 |
| 发布包到 pub.dev | dart pub publish |
封装决策速查
这段代码要复用吗?
├── 否 → 不需要封装,保持原样
└── 是 → 复用的是什么?
├── 纯计算/校验逻辑 → 工具类(static 方法)
├── 给已有类加方法 → Extension
├── 跨类的共享行为 → Mixin
├── 可复用 UI 片段 → 自定义 Widget
├── 有状态的业务逻辑 → 服务类
└── 跨项目复用 → 独立包(export + pubspec.yaml)避免过度封装
- 只有一个地方使用的代码不需要封装
- 不要为了封装而封装,2-3 处复用是提取的最低门槛
- 封装后的命名要让人一看就知道做什么
下一步
- 项目架构 — 目录组织 / 代码规范
