Flutter 空安全规范指南
本文档旨在帮助团队成员理解和正确使用 Dart 空安全特性,避免常见的空指针错误。
第一部分:为什么要处理空安全
1.1 什么是空安全
Dart 2.12 引入了****健全的空安全(Sound Null Safety)** **,这是一种类型系统特性,用于区分可以为 null 的值和不能为 null 的值。
// 非空类型 - 不能赋值为 null
String name = "张三";
// 可空类型 - 可以赋值为 null
String? nickname = null;
1.2 为什么需要空安全
❌ 不处理空安全的风险
- 运行时崩溃
```dart
// 危险代码
String? name = null;
print(name.length); // 💥 运行时崩溃:Null check operator used on a null value
```
- 难以调试
- 空指针错误可能在任何地方发生
- 错误信息不明确,难以定位问题根源
- 生产环境中的崩溃影响用户体验
- 代码质量下降
- 大量防御性代码
- 逻辑混乱,可读性差
✅ 处理空安全的好处
-
编译时检查 - 在代码运行前发现潜在的空指针问题
-
代码更健壮 - 明确表达哪些值可以为空
-
更好的 IDE 支持 - 自动补全和错误提示更准确
-
性能优化 - 编译器可以生成更高效的代码
第二部分:核心概念
2.1 可空类型 vs 非空类型
// 非空类型 - 必须有值
String name = "张三";
int age = 18;
List<String> items = [];
// 可空类型 - 可以为 null
String? nickname; // 默认为 null
int? score; // 默认为 null
List<String>? data; // 默认为 null
2.2 空值检查操作符
?. 安全调用操作符
String? name = null;
// ❌ 危险
print(name.length); // 崩溃
// ✅ 安全
print(name?.length); // 输出 null
?? 空值合并操作符
String? name = null;
// 如果 name 为 null,使用默认值
String displayName = name ?? "未知用户";
??= 空值赋值操作符
String? name;
name ??= "默认名称"; // 只有当 name 为 null 时才赋值
! 强制解包操作符(谨慎使用)
String? name = "张三";
// 告诉编译器:我确定这个值不为 null
print(name!.length);
// ⚠️ 警告:如果 name 为 null,会崩溃!
2.3 late 关键字
class MyWidget extends StatefulWidget {
// late 表示:这个变量会在使用前被初始化
late TextEditingController controller;
@override
void initState() {
super.initState();
controller = TextEditingController(); // 在使用前初始化
}
}
使用场景:
-
变量在声明时无法初始化,但在使用前一定会被初始化
-
延迟初始化昂贵的对象
⚠️ 注意:如果 late 变量在使用前未初始化,会抛出 LateInitializationError
2.4 dynamic 类型的风险
dynamic 是 Dart 中的一个特殊类型,它会绕过编译时类型检查,导致空安全失效。
为什么 dynamic 危险
// dynamic 类型绕过编译检查
dynamic value = null;
// 编译器不会报错,但运行时会崩溃!
print(value.length); // 💥 NoSuchMethodError: The getter 'length' was called on null
dynamic vs Object? vs var
| 类型 | 编译检查 | 可为 null | 说明 |
|------|----------|---------|------|
| dynamic | ❌ 无 | ✅ 是 | 危险,绕过所有检查 |
| Object? | ✅ 有 | ✅ 是 | 安全,需要类型转换 |
| Object | ✅ 有 | ❌ 否 | 安全,非空 |
| var | ✅ 有 | 取决于初始化 | 类型推断 |
常见的 dynamic 来源
- JSON 解析
```dart
// json.decode 返回 dynamic
dynamic data = json.decode(response.body);
print(data['name'].length); // 💥 如果 name 不存在或为 null
```
- Map 的值类型
```dart
Map<String, dynamic> json = {'name': null};
String name = json['name']; // 💥 编译通过,运行崩溃
```
- 不明确的参数类型
```dart
void process(dynamic data) { // ❌ 不推荐
print(data.name); // 编译器不知道 data 有没有 name
}
```
✅ 正确做法
// 1. JSON 解析时明确类型
Map<String, dynamic> json = jsonDecode(response.body);
String name = json['name'] as String? ?? ''; // ✅ 类型转换 + 空值处理
// 2. 使用模型类
class User {
final String name;
User.fromJson(Map<String, dynamic> json)
: name = json['name'] as String? ?? '';
}
// 3. 使用泛型代替 dynamic
void process<T>(T data) { // ✅ 泛型更安全
// ...
}
// 4. 使用 Object? 代替 dynamic
void handleAny(Object? data) { // ✅ 需要类型检查
if (data is String) {
print(data.length); // 编译器知道这里是 String
}
}
项目中的实际案例
// ❌ 危险:CacheUtils 返回 dynamic
dynamic value = CacheUtils.getValeDynamic(SharedPreferencesUntils.Device_ID);
if (value == null || value == "") { // ✅ 这里有检查,但依赖运行时
// ...
}
// ✅ 更安全的写法
String? deviceId = CacheUtils.getValeDynamic(SharedPreferencesUntils.Device_ID) as String?;
if (deviceId == null || deviceId.isEmpty) {
// ...
}
第三部分:项目中如何预防空安全问题
3.1 开启严格的分析器规则
在 analysis_options.yaml 中配置严格的 lint 规则,让编译器帮你发现问题:
# analysis_options.yaml
analyzer:
errors:
# 将这些警告升级为错误
avoid_dynamic_calls: error
avoid_returning_null_for_void: error
null_check_on_nullable_type_parameter: error
language:
strict-casts: true # 禁止隐式类型转换
strict-raw-types: true # 禁止原始泛型
linter:
rules:
# 空安全相关
- avoid_null_checks_in_equality_operators
- avoid_returning_null_for_void
- null_check_on_nullable_type_parameter
- unnecessary_nullable_for_final_variable_declarations
# dynamic 相关
- avoid_dynamic_calls
# 类型相关
- always_declare_return_types
- always_specify_types # 可选,较严格
- avoid_types_on_closure_parameters
# 其他最佳实践
- prefer_final_locals
- prefer_const_declarations
3.2 使用代码生成工具
避免手写 JSON 解析代码,使用代码生成工具自动处理空安全:
推荐工具:
- json_serializable - 官方推荐
```yaml
# pubspec.yaml
dependencies:
json_annotation: ^4.8.1
dev_dependencies:
build_runner: ^2.4.6
json_serializable: ^6.7.1
```
- freezed - 更强大的不可变模型
```yaml
dependencies:
freezed_annotation: ^2.4.1
dev_dependencies:
freezed: ^2.4.5
```
示例:
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';
@JsonSerializable()
class User {
final String name; // 必填字段
final String? avatar; // 可选字段
@JsonKey(defaultValue: 0)
final int age; // 带默认值
User({required this.name, this.avatar, required this.age});
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
3.3 建立空安全工具类
创建统一的工具方法来处理空值:
// lib/utils/null_safety_utils.dart
/// 空安全工具类
class NullSafetyUtils {
/// 安全获取字符串,如果为 null 返回默认值
static String safeString(dynamic value, [String defaultValue = '']) {
if (value == null) return defaultValue;
return value.toString();
}
/// 安全获取整数
static int safeInt(dynamic value, [int defaultValue = 0]) {
if (value == null) return defaultValue;
if (value is int) return value;
if (value is String) return int.tryParse(value) ?? defaultValue;
if (value is double) return value.toInt();
return defaultValue;
}
/// 安全获取布尔值
static bool safeBool(dynamic value, [bool defaultValue = false]) {
if (value == null) return defaultValue;
if (value is bool) return value;
if (value is String) return value.toLowerCase() == 'true';
if (value is int) return value != 0;
return defaultValue;
}
/// 安全获取列表
static List<T> safeList<T>(dynamic value) {
if (value == null) return <T>[];
if (value is List) return value.cast<T>();
return <T>[];
}
}
使用示例:
// 从 JSON 中安全获取值
Map<String, dynamic> json = {'name': null, 'age': '25'};
String name = NullSafetyUtils.safeString(json['name'], '未知'); // "未知"
int age = NullSafetyUtils.safeInt(json['age']); // 25
3.4 使用 Extension 扩展方法
// lib/extensions/null_safety_extensions.dart
extension NullableStringExtension on String? {
/// 如果为 null 或空字符串,返回 true
bool get isNullOrEmpty => this == null || this!.isEmpty;
/// 如果为 null 或空字符串,返回默认值
String orDefault([String defaultValue = '']) => isNullOrEmpty ? defaultValue : this!;
}
extension NullableListExtension<T> on List<T>? {
/// 如果为 null 或空列表,返回 true
bool get isNullOrEmpty => this == null || this!.isEmpty;
/// 安全获取元素
T? safeGet(int index) {
if (this == null || index < 0 || index >= this!.length) return null;
return this![index];
}
}
使用示例:
String? name = null;
print(name.isNullOrEmpty); // true
print(name.orDefault('未知')); // "未知"
List<String>? items = ['a', 'b'];
print(items.safeGet(0)); // "a"
print(items.safeGet(10)); // null
3.5 建立代码审查流程
- PR 提交前
- 运行 flutter analyze 检查所有警告
- 确保没有 dynamic 相关警告
- Code Review 时
- 检查本文档第六部分的审查清单
- 特别关注异步回调中的 setState
- 定期扫描
- 使用 dart fix --apply 自动修复部分问题
- 定期检查项目中的 ! 使用情况
第四部分:变量声明规范
4.1 变量声明规范
// ✅ 推荐:明确类型
String name = "";
List<String> items = [];
// ✅ 推荐:确实可能为空时使用可空类型
String? errorMessage;
// ❌ 避免:滥用可空类型
String? name; // 如果 name 永远不会为 null,不要用 ?
3.2 函数参数规范
// ✅ 推荐:必需参数使用非空类型
void greet(String name) {
print("Hello, $name");
}
// ✅ 推荐:可选参数使用可空类型或默认值
void greet2({String? name}) {
print("Hello, ${name ?? 'Guest'}");
}
void greet3({String name = "Guest"}) {
print("Hello, $name");
}
// ✅ 推荐:required 关键字用于必需的命名参数
void createUser({required String name, required int age}) {
// ...
}
3.3 返回值规范
// ✅ 推荐:明确返回值是否可能为空
String getName() {
return "张三"; // 永远返回非空值
}
String? findUser(int id) {
// 可能返回 null(用户不存在)
return users[id];
}
// ✅ 推荐:使用注释说明返回 null 的情况
/// 根据 ID 查找用户
///
/// 返回 null 如果用户不存在
String? findUserById(int id) {
// ...
}
3.4 类成员规范
class User {
// ✅ 必需字段使用非空类型
final String name;
final int age;
// ✅ 可选字段使用可空类型
final String? avatar;
final String? bio;
// ✅ 使用 required 确保必需参数被传入
User({
required this.name,
required this.age,
this.avatar,
this.bio,
});
}
第五部分:常见错误与修复
4.1 强制解包 ! 的滥用
❌ 错误示例(来自项目实际代码)
// ai_picture_books.dart 第183行(修复前)
Image.network(picBookList[curPBIndex].pictureUrl!, // 💥 如果 pictureUrl 为 null 会崩溃
errorBuilder: (context, error, stackTrace) {
return Container(...); // errorBuilder 永远不会执行
}
)
问题:pictureUrl! 的强制解包在 Image.network() 被调用之前执行,如果为 null 会直接崩溃,errorBuilder 根本不会执行。
✅ 正确做法
// ai_picture_books.dart 第185-186行(修复后)
Image.network(
picBookList[curPBIndex].pictureUrl ?? '', // 使用空字符串代替 null
errorBuilder: (context, error, stackTrace) {
return Container(...); // 空字符串会触发 errorBuilder
}
)
4.2 未检查 mounted 的异步回调
❌ 错误示例
// 异步回调中直接调用 setState
audioPlayer.onPlayerStateChanged.listen((PlayerState state) {
setState(() { // 💥 如果页面已销毁,会报错
isPlaying = state == PlayerState.playing;
});
});
✅ 正确做法
audioPlayer.onPlayerStateChanged.listen((PlayerState state) {
if (!mounted) return; // ✅ 先检查页面是否还存在
setState(() {
isPlaying = state == PlayerState.playing;
});
});
4.3 数组越界访问
❌ 错误示例
// 直接访问数组元素,未检查边界
playAsyncAudio(picBookList[curPBIndex].audioUrl);
✅ 正确做法
// 先检查数组是否为空和索引是否有效
if (picBookList.isNotEmpty && curPBIndex < picBookList.length) {
playAsyncAudio(picBookList[curPBIndex].audioUrl);
}
4.4 网络请求回调中的空安全
❌ 错误示例
HttpUtil().get(url).then((value) {
setState(() { // 💥 页面可能已销毁
data = value.data;
});
});
✅ 正确做法
HttpUtil().get(url).then((value) {
if (!mounted) return; // ✅ 检查 mounted
if (value.data == null) return; // ✅ 检查数据
setState(() {
data = value.data!;
});
});
4.5 JSON 解析中的空安全
❌ 错误示例
class User {
final String name;
User.fromJson(Map<String, dynamic> json)
: name = json['name']; // 💥 如果 json['name'] 为 null 会崩溃
}
✅ 正确做法
class User {
final String name;
User.fromJson(Map<String, dynamic> json)
: name = json['name'] ?? ''; // ✅ 提供默认值
// 或者使用可空类型
// final String? name;
// User.fromJson(Map<String, dynamic> json) : name = json['name'];
}
第六部分:代码审查清单
在代码审查时,请检查以下要点:
✅ 变量声明
-
是否正确区分可空和非空类型?
-
是否避免了不必要的可空类型?
-
late变量是否在使用前一定会被初始化?
✅ 强制解包 !
-
是否有充分理由使用
!? -
使用
!前是否已确认值不为 null? -
是否可以用
??或?.代替?
✅ 异步回调
-
setState前是否检查了mounted? -
网络请求回调是否处理了页面销毁的情况?
-
Timer 回调是否检查了
mounted?
✅ 集合操作
-
访问数组元素前是否检查了边界?
-
是否检查了集合是否为空?
✅ 函数参数
-
必需参数是否使用了
required? -
可选参数是否有合理的默认值?
✅ dynamic 类型
-
是否有不必要的
dynamic类型? -
JSON 解析后是否进行了类型转换?
-
Map<String, dynamic>的值是否在使用前进行了类型检查?
附录:快速参考
| 操作符 | 用途 | 示例 |
|--------|------|------|
| ? | 声明可空类型 | String? name; |
| ?. | 安全调用 | name?.length |
| ?? | 空值合并 | name ?? "默认" |
| ??= | 空值赋值 | name ??= "默认" |
| ! | 强制解包(谨慎) | name!.length |
| late | 延迟初始化 | late String name; |
| required | 必需参数 | {required String name} |
| dynamic | 动态类型(谨慎) | 避免使用,用 Object? 代替 |
| as | 类型转换 | json['name'] as String? |
| is | 类型检查 | if (data is String) |