Flutter for OpenHarmony:Dart 语法入门(下)
本文介绍了Dart异步编程的核心概念和实用技巧。主要内容包括:1) 异步的必要性,通过"服务员-后厨"比喻解释Flutter单线程模型中异步操作的重要性;2) Future、then()和catchError()的基本用法;3) async/await语法简化异步代码;4) Dart扩展特性如空安全运算符(?.、??)、枚举和扩展方法的使用。
目录
这篇就是Dart 语法的收尾,主要讲一下异步编程。
一、异步编程
1. 先搞懂:为什么要用异步?
Flutter 采用单线程模型,主线程(UI 线程)负责处理用户交互、渲染界面和执行逻辑。若在主线程执行耗时操作(如网络请求、文件读写),会导致界面卡顿或无响应(ANR)。
必须通过异步操作将耗时任务转移到后台线程,主线程继续响应用户交互,任务完成后通过回调或事件通知更新界面。
你可以把 Flutter 想象成一个「单线程的小饭馆」:
- 「主线程」就是唯一的「服务员」,既要给客人点菜(处理用户点击),又要上菜(渲染界面),还要洗碗(处理逻辑);
- 如果客人点了一道「需要炖 2 小时的汤」(耗时操作:比如网络请求、读取大文件),服务员全程盯着炖汤,就没法给其他客人服务了 → 界面就会「卡住」(ANR),用户点按钮没反应、界面不刷新;
- 「异步」就是:服务员把炖汤的活交给「后厨」(异步线程),自己继续给其他客人服务,等汤炖好了,后厨喊一声,服务员再过来端汤 → 界面不卡,用户体验好。
核心结论:只要代码需要「等」(比如等网络数据、等文件读取),就必须用异步,否则界面会卡死。
2. 同步 vs 异步
先看「同步代码」
void main() {
print("1. 服务员开始工作");
// 模拟「炖2小时汤」:这里用sleep(同步等待,会卡住)
// 注意:Flutter里绝对不能用sleep,这里只是举例!
sleep(Duration(seconds: 2));
print("2. 汤炖好了");
print("3. 服务员继续服务其他客人");
}
运行结果:先打印 1 → 卡 2 秒 → 打印 2 → 打印 3。
卡 2 秒期间,服务员啥也干不了,界面会卡死。
再看「异步代码」
void main() {
print("1. 服务员开始工作");
// 模拟把炖汤交给后厨:用Future.delayed(异步等待)
Future.delayed(Duration(seconds: 2), () {
print("2. 汤炖好了(后厨喊服务员)");
});
print("3. 服务员继续服务其他客人");
}
运行结果:先打印 1 → 立刻打印 3 → 等 2 秒打印 2。
优点:服务员没闲着,界面不卡。
Future.delayed(时间, 回调函数):延迟指定时间后,执行回调函数里的代码;- 回调函数:要具体做的操作。
3. 拿到异步的结果
异步代码是在后台运行的,它的运行结果可能不会立刻出来,那我们要怎么才能拿到?
关键字: then()
举例说明
// 定义一个「让后厨炖汤」的函数
// Future<String>:表示后厨最终会返回「汤的名字」(字符串)
Future<String> cookSoup() {
// 后厨开始炖汤,需要2秒
return Future.delayed(Duration(seconds: 2), () {
// 炖汤的结果:返回汤的名字
return "番茄牛腩汤";
});
}
void main() {
print("1. 客人点单,服务员把炖汤交给后厨");
// 调用异步函数:拿到「纸条(Future)」,不是汤本身
Future<String> soupFuture = cookSoup();
// then():纸条上写的「汤好后要做的事」
soupFuture.then((String soupName) {
// soupName就是后厨返回的结果(汤的名字)
print("2. 汤炖好了:$soupName,端给客人");
});
print("3. 服务员继续服务其他客人");
}
运行结果
1. 客人点单,服务员把炖汤交给后厨
3. 服务员继续服务其他客人
2. 汤炖好了:番茄牛腩汤,端给客人
万一操作失败了,就要使用关键字:catchError () 来捕获异常。
举例说明
Future<String> cookSoup() {
return Future.delayed(Duration(seconds: 2), () {
// 模拟炖汤失败:抛出错误
throw Exception("汤炖糊了!");
// return "番茄牛腩汤"; // 正常情况
});
}
void main() {
print("1. 客人点单");
cookSoup()
.then((soupName) {
print("2. 汤好了:$soupName");
})
.catchError((error) {
// 处理失败情况
print("2. 出错了:$error");
});
print("3. 服务员继续干活");
}
运行结果
1. 客人点单
3. 服务员继续干活
2. 出错了:Exception: 汤炖糊了!
4. async和await
then() 写多了会像「套娃」,因为then() 可以链式调用,但语法观感很差,所以async/await是「把异步代码写成同步样子」,看起来会很舒服。
核心:
- 给函数加
async:告诉 Dart「这个函数里有异步操作」,比如void main() async;- 异步操作前加
await:告诉 Dart「等这个异步操作做完,再往下走」,只能在加了 async 的函数里用。
// 异步函数:炖汤
Future<String> cookSoup() async {
// await:等2秒,再继续(后厨炖汤)
await Future.delayed(Duration(seconds: 2));
// 正常返回结果
return "番茄牛腩汤";
// 失败就抛错:throw Exception("汤糊了");
}
// main函数加async:里面要用await
void main() async {
print("1. 客人点单");
// try/catch:处理成功/失败(替代then/catchError)
try {
// await:等炖汤完成,直接拿到结果(不用纸条了)
String soupName = await cookSoup();
print("2. 汤好了:$soupName");
} catch (error) {
// 失败时执行这里
print("2. 出错了:$error");
}
print("3. 服务员继续干活");
}
运行结果:
1. 客人点单
(等2秒)
2. 汤好了:番茄牛腩汤
3. 服务员继续干活
大家可能会觉得,这不同样等了2秒吗,哪来的异步?注意看await的执行流程
await 的执行机制:暂停当前函数,但不阻塞线程
- 当前函数(如 main)会被“挂起” :代码执行到 await 处时,函数暂停,控制权返回给事件循环。
- 线程不阻塞 :事件循环可以处理其他任务
因为刚才那段代码中就一个任务,所以在这里它等了2秒。
接下来这段代码应该能让大家更清晰的感觉到await的用法
// 模拟耗时较长的异步任务(炖汤,2秒)
Future<String> cookSoup() async {
print("开始炖汤(需要2秒)");
await Future.delayed(Duration(seconds: 2));
return "番茄牛腩汤";
}
// 模拟耗时较短的异步任务(打扫卫生,1秒)
Future<void> cleanTable() async {
print("开始打扫卫生(需要1秒)");
await Future.delayed(Duration(seconds: 1));
print("卫生打扫完成");
}
void main() async {
print("1. 服务员开始工作");
// 启动打扫卫生任务(异步执行,不等待)
cleanTable();
// await 等待炖汤完成(挂起 main 函数,但线程不阻塞)
print("2. 开始等汤...");
String soup = await cookSoup();
print("3. 汤好了:$soup");
print("4. 服务员继续其他工作");
}
运行结果
1. 服务员开始工作
开始打扫卫生(需要1秒)
2. 开始等汤...
开始炖汤(需要2秒)
卫生打扫完成
3. 汤好了:番茄牛腩汤
4. 服务员继续其他工作
在打扫卫生的时候开始等汤,在炖汤的时候,卫生打扫完成了,所以使用await当前函数会被“挂起”,但还可以处理其他任务,并不是同步执行。
注:没加async,不能用await
二、Dart 扩展特性
这部分不用懂原理,记「固定写法」就行,学会了能少踩坑、少写重复代码。
1. 空安全 —— 避免「程序崩了不知道为啥」
最常见的崩溃:「空指针异常」(比如用一个值为 null 的变量)。
Dart 的空安全就是「提前防错」。
| 运算符 | 写法 | 作用 | 例子 |
| ?. | 变量?. 属性 | 变量不为空才取属性,为空就返回 null | str?.length |
| ?? | 变量 ?? 默认值 | 变量为空就用默认值,不为空就用自己的值 | str ?? "默认文字" |
void main() {
String? str = null; // ?表示这个变量可以为空
// ?.:str是空,所以返回null
int? len = str?.length;
print(len); // 打印:null
// ??:str是空,用默认值
String newStr = str ?? "我是默认文字";
print(newStr); // 打印:我是默认文字
}
2. 空安全在Flutter的用法:
后端返回的数据可能少字段,用这两个运算符就不会崩:
// 模拟后端返回的数据(name字段为空)
Map<String, dynamic> user = {"name": null};
// 安全解析:为空就用「匿名用户」
String userName = user["name"] ?? "匿名用户";
print(userName); // 打印:匿名用户
3. 枚举
表示状态用枚举,用字符串写状态(比如 "加载中" "成功" "失败"),容易拼错(比如写成 "加载zhong" "成攻"),代码运行时才报错;用枚举的话,编辑器会提示可选值,写错了直接标红,根本运行不了。枚举是「固定的选项」,只能选里面的,不会错。
// 第一步:定义枚举(给“选择题”定选项)
// 规则:枚举名首字母大写,选项小写(比如PageState、loading)
enum PageState { loading, success, error }
void main() {
// 第二步:使用枚举(选一个选项)
// 只能写PageState.loading/success/error,写别的会报错
PageState nowState = PageState.loading;
// 第三步:判断枚举(用if/switch都行,新手先学if)
if (nowState == PageState.loading) {
print("页面正在转圈圈加载");
} else if (nowState == PageState.success) {
print("数据加载好了,显示内容");
} else if (nowState == PageState.error) {
print("加载失败,点一下重试吧");
}
}
4. 扩展方法
举个例子,比如 Dart 的 String 类(字符串)本身只有toUpperCase()(全大写)、length(长度)等功能,没有 “首字母大写” 的功能;用扩展方法就能给 String 加这个功能,以后所有字符串都能直接用。
// 第一步:定义扩展(给String加“首字母大写”技能)
// 规则:extension 自定义名字 on 要扩展的类(比如MyString on String)
extension MyStringHelper on String {
// 第二步:写自定义功能(方法名自己取,比如bigFirst)
String bigFirst() {
// this代表当前字符串(比如调用"flutter".bigFirst(),this就是"flutter")
// 逻辑:取第一个字符转大写 + 剩下的字符
return this.substring(0,1).toUpperCase() + this.substring(1);
}
}
void main() {
// 第三步:使用扩展功能(直接用字符串.方法名)
String text1 = "flutter";
print(text1.bigFirst()); // 打印:Flutter
String text2 = "hello";
print(text2.bigFirst()); // 打印:Hello
}
结尾
这些基础知识点看着零散,但都是 Flutter 开发里「能直接落地」的核心技巧 —— 不用急着一次性全记住,先把异步 + setState、空安全这两个最常用的点敲熟,再慢慢把枚举、扩展方法加进去。
编程的核心从来不是背语法,而是「用起来」:哪怕今天只学会了给异步请求加 try/catch,或者用?. 避免了一次崩溃,都是实实在在的进步。
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)