【Harmonyos】Flutter开源鸿蒙跨平台训练营 Day16动效与数据请求联动:状态同步核心实现
【Flutter/鸿蒙跨端开发】动效与数据请求联动+低性能适配实战(Day2):状态同步核心实现
🔍 摘要
本文为Flutter/鸿蒙跨端动效开发系列教程第二天核心内容,针对动效与数据请求状态脱节的核心痛点,遵循单一数据源和状态驱动UI的统一原则,分别通过Flutter的Bloc和鸿蒙(ArkTS)的AppStorage实现双端状态统一管理,完成动效状态与数据请求状态的双向绑定。教程包含完整的核心概念解析、可直接复制的代码实现、分步实操步骤和效果验证,彻底解决「请求结束动效仍运行」「请求失败无错误动效」「跨组件状态丢失」等真实开发问题,为后续动效时序调度和低性能适配夯实状态层基础。
📝 前言
在Day1(Day15)我们已完成双端开发环境搭建和基础Loading动效组件封装 ✅,但仅通过手动方式控制动效状态,未解决跨端开发的核心痛点——动效与数据请求状态脱节。
Day2将聚焦状态驱动动效的核心逻辑,通过Flutter的Bloc实现业务逻辑与UI的完全分离,通过鸿蒙的AppStorage实现全局状态双向绑定,让动效状态完全由数据请求状态驱动,禁止任何手动控制动效的操作,从根源上解决状态不一致问题。全程附带可直接运行的代码、详细的步骤注释,新手可一步一步跟着落地。
一、状态同步的核心痛点回顾 💥
在Day1的模拟请求中,我们通过setState(Flutter)/直接修改状态(鸿蒙)手动控制动效,在真实商业开发中会遇到以下3个高频致命问题:
- 状态分散:数据请求状态与动效状态分别维护,易出现「数据已返回但动效未终止」「请求失败但动效仍加载」的脱节问题,用户体验混乱。
- 无异常处理:请求失败/用户主动取消时,动效无法自动切换为错误状态,也无对应的反馈提示,无法引导用户重试。
- 跨页面状态丢失:复杂页面跳转、多组件联动场景下,动效状态无法跨组件/跨页面同步,出现局部动效异常。
核心解决方案:采用专业的状态管理工具实现统一状态管理,让动效状态成为数据请求状态的「附属值」,完全由数据请求状态决定,禁止任何手动控制动效显示/隐藏的操作。
二、双端状态管理选型与统一原则 📊
结合Flutter/鸿蒙的技术生态、开发效率和场景适配性,以下为最优状态管理工具选型,双端虽技术实现不同,但核心思路完全一致:
| 技术栈 | 状态管理工具 | 核心优势 | 适用场景 | 官方文档链接 |
|---|---|---|---|---|
| Flutter | Bloc | 完全分离UI与业务逻辑,支持复杂状态流转,内置事件-状态单向数据流,易维护可扩展 | 中大型跨端应用、多页面/多组件状态同步、复杂动效联动 | Flutter Bloc官方文档 |
| Flutter | Provider | 轻量易用,基于InheritedWidget,学习成本极低,代码量少 | 小型应用、单页面状态管理、简单动效场景 | Flutter Provider官方文档 |
| 鸿蒙(ArkTS) | AppStorage | 鸿蒙原生全局状态容器,支持跨页面/跨组件双向绑定,完美适配Stage模型,鸿蒙官方推荐 | 鸿蒙原生应用全局状态同步、跨页面动效联动、多组件状态共享 | 鸿蒙AppStorage官方文档 |
| 鸿蒙(ArkTS) | 自定义状态类 | 轻量灵活,无额外依赖,代码简洁 | 单页面/单个组件内的局部状态管理、简单动效场景 | 鸿蒙状态管理官方文档 |
🔥 双端统一核心原则(必守,后续所有实操的基础)
- 单一数据源:所有动效状态、数据请求状态都维护在统一的状态管理工具中,避免状态分散在各个组件/页面,这是解决状态脱节的根本。
- 状态驱动UI/动效:动效的显示/隐藏、切换,完全由状态值的变化自动触发,无需手动调用任何动效控制方法。
- 单向数据流(推荐):数据请求改变状态,状态变化驱动动效/UI更新,禁止UI/动效直接修改请求状态,保证状态流转的可追溯性。
三、Flutter端:Bloc实现动效与数据请求状态同步 🚀
Bloc是Flutter生态中最适合复杂状态流转和业务逻辑分离的状态管理工具,通过「Event→Bloc→State→UI/动效」的单向数据流,实现状态与动效的强绑定,从根源上避免状态不一致。
3.1 Bloc核心概念(新手必懂,一分钟快速掌握)
Bloc的核心是事件驱动状态变化,所有概念围绕「状态流转」设计,与动效联动的核心关联点已标注:
- Event(事件):触发状态变化的行为,如「用户点击发起数据请求」「用户点击取消请求」,是动效触发的「源头」。
- State(状态):数据请求的当前状态,如「初始状态」「加载中」「请求成功」「请求失败」,是动效状态的唯一判断依据。
- Bloc(核心处理层):接收Event,处理网络请求等业务逻辑,输出新的State,是状态流转的「中间处理器」。
- UI层:监听State的变化,自动更新动效显示/隐藏和页面内容,是状态变化的「最终展示层」。
3.2 步骤1:定义Event与State(动效的状态依据)
在Flutter项目的lib目录下新建bloc文件夹(规范目录结构),在该文件夹下创建data_bloc.dart文件,定义数据请求的状态枚举、State和Event,这是动效状态的唯一判断标准。
import 'package:bloc/bloc.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
// 🔥 数据请求状态枚举(动效状态的唯一依据,所有动效判断都基于此)
enum DataStatus {
initial, // 初始状态:动效隐藏
loading, // 加载中:动效显示
success, // 请求成功:动效隐藏
failure, // 请求失败:动效隐藏并触发错误反馈
}
// 🔥 数据请求State:封装所有请求相关状态,供UI/动效监听
class DataState {
final DataStatus status; // 核心状态:决定动效显示/隐藏
final dynamic data; // 请求成功的返回数据
final String? errorMsg; // 请求失败的错误信息
DataState({
required this.status,
this.data,
this.errorMsg,
});
// 初始状态构造函数:页面初始化时的默认状态
factory DataState.initial() => DataState(status: DataStatus.initial);
// 复制方法:Bloc推荐方式,生成新的State(不可直接修改原State)
DataState copyWith({
DataStatus? status,
dynamic data,
String? errorMsg,
}) {
return DataState(
status: status ?? this.status,
data: data ?? this.data,
errorMsg: errorMsg ?? this.errorMsg,
);
}
}
// 🔥 数据请求Event:抽象基类,所有具体事件都继承此类
abstract class DataEvent {}
// 触发数据请求的Event:用户点击「发起请求」按钮时触发
class FetchDataEvent extends DataEvent {
final BuildContext context; // 用于显示SnackBar提示
FetchDataEvent(this.context);
}
// 取消数据请求的Event:用户点击「取消请求」按钮时触发
class CancelRequestEvent extends DataEvent {}
3.3 步骤2:实现Bloc核心业务逻辑(状态流转处理)
在data_bloc.dart文件中继续编写Bloc的核心实现,处理网络请求、异常捕获、状态更新逻辑,所有状态变化都在此处完成,UI/动效仅做监听。
// 🔥 DataBloc核心:接收Event,处理业务逻辑,输出新State
class DataBloc extends Bloc<DataEvent, DataState> {
late CancelToken _cancelToken; // Dio取消请求令牌:用于用户主动取消请求
// 构造函数:初始化初始状态,并绑定Event与处理方法
DataBloc() : super(DataState.initial()) {
on<FetchDataEvent>(_onFetchData); // 绑定「发起请求」事件与处理方法
on<CancelRequestEvent>(_onCancelRequest); // 绑定「取消请求」事件与处理方法
}
// 🔥 处理「发起数据请求」事件:核心业务逻辑
Future<void> _onFetchData(FetchDataEvent event, Emitter<DataState> emit) async {
// 1. 触发加载状态:自动显示Loading动效
emit(state.copyWith(status: DataStatus.loading));
// 2. 初始化取消令牌:为用户取消请求做准备
_cancelToken = CancelToken();
try {
// 3. 发起真实网络请求(模拟接口,可替换为实际业务接口)
final response = await Dio().get(
'https://api.example.com/data', // 模拟GET请求接口
cancelToken: _cancelToken, // 绑定取消令牌
timeout: const Duration(seconds: 5), // 设置超时时间:5秒
);
// 4. 请求成功:更新状态为success,自动隐藏Loading动效
emit(state.copyWith(
status: DataStatus.success,
data: response.data, // 保存返回数据
errorMsg: null, // 清空错误信息
));
// 5. 成功提示:仅当页面未销毁时显示
if (event.context.mounted) {
ScaffoldMessenger.of(event.context).showSnackBar(
const SnackBar(content: Text('数据请求成功!'), backgroundColor: Colors.green),
);
}
} catch (e) {
// 6. 请求失败(网络错误/超时/取消):更新状态为failure,自动隐藏动效
emit(state.copyWith(
status: DataStatus.failure,
errorMsg: e.toString(), // 保存错误信息
));
// 7. 失败提示:仅当页面未销毁时显示
if (event.context.mounted) {
ScaffoldMessenger.of(event.context).showSnackBar(
SnackBar(content: Text('请求失败:${e.toString()}'), backgroundColor: Colors.red),
);
}
}
}
// 🔥 处理「取消数据请求」事件
Future<void> _onCancelRequest(CancelRequestEvent event, Emitter<DataState> emit) async {
// 1. 取消网络请求:触发Dio的取消回调
_cancelToken.cancel('用户主动取消请求');
// 2. 重置状态为初始值:自动隐藏Loading动效,恢复初始页面
emit(DataState.initial());
}
}
3.4 步骤3:UI层绑定Bloc状态(动效自动响应)
修改lib/main.dart文件,将UI页面、Day1封装的CommonLoading组件与Bloc状态进行绑定,无需任何手动控制动效的代码,动效将根据Bloc的状态变化自动显示/隐藏。
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'bloc/data_bloc.dart';
import 'widgets/common_loading.dart'; // 引入Day1封装的Loading组件
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
// 🔥 注入Bloc:让整个应用可访问DataBloc
return BlocProvider(
create: (context) => DataBloc(),
child: MaterialApp(
title: 'Flutter状态同步Demo(Day2)',
theme: ThemeData(primarySwatch: Colors.blue),
home: const DataSyncPage(),
),
);
}
}
// 🔥 状态同步演示页面:Bloc状态与UI/动效绑定
class DataSyncPage extends StatelessWidget {
const DataSyncPage({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter Bloc状态同步(Day2)'),
centerTitle: true,
),
// 🔥 BlocBuilder:监听DataBloc的状态变化,自动重建UI/动效
body: BlocBuilder<DataBloc, DataState>(
builder: (context, state) {
// 🔥 绑定Loading组件与Bloc状态:完全由state.status决定,无手动控制
return CommonLoading(
isLoading: state.status == DataStatus.loading,
color: Colors.blue,
size: 60.0,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 🔥 根据状态显示不同页面内容
if (state.status == DataStatus.initial)
const Text(
'点击下方按钮发起数据请求',
style: TextStyle(fontSize: 18),
),
if (state.status == DataStatus.success)
Text(
'请求结果:\n${state.data.toString()}',
style: const TextStyle(fontSize: 16),
textAlign: TextAlign.center,
),
if (state.status == DataStatus.failure)
Text(
'错误信息:\n${state.errorMsg}',
style: const TextStyle(fontSize: 16, color: Colors.red),
textAlign: TextAlign.center,
),
const SizedBox(height: 40),
// 🔥 发起请求按钮:触发FetchDataEvent
ElevatedButton(
onPressed: () => context.read<DataBloc>().add(FetchDataEvent(context)),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 12),
textStyle: const TextStyle(fontSize: 16),
),
child: const Text('发起数据请求'),
),
const SizedBox(height: 15),
// 🔥 取消请求按钮:仅当加载中时可点击
ElevatedButton(
onPressed: state.status == DataStatus.loading
? () => context.read<DataBloc>().add(CancelRequestEvent())
: null,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey,
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 12),
textStyle: const TextStyle(fontSize: 16),
),
child: const Text('取消请求'),
),
],
),
),
);
},
),
);
}
}
3.5 运行验证(动效与状态自动联动)
运行Flutter项目,点击对应按钮,验证动效与数据请求状态的联动效果,全程无任何手动控制动效的代码,所有变化均由Bloc状态驱动:
- 点击「发起数据请求」:自动显示Loading动效,请求过程中动效持续运行;
- 请求成功:自动隐藏动效,显示返回数据并弹出绿色成功提示;
- 模拟网络错误(断网/修改接口地址):自动隐藏动效,显示错误信息并弹出红色失败提示;
- 加载中点击「取消请求」:立即隐藏动效,状态重置为初始值,页面恢复初始状态。
四、鸿蒙端:AppStorage实现动效与数据请求状态同步 📱
鸿蒙(ArkTS)的AppStorage是官方推荐的全局状态管理工具,基于双向绑定实现跨页面/跨组件的状态同步,完美适配Stage模型,仅需简单的装饰器即可实现状态与动效的绑定,代码简洁、开发效率高。
4.1 AppStorage核心概念(新手必懂)
鸿蒙AppStorage的核心是全局状态容器+绑定装饰器,无需复杂的分层设计,轻量高效,与动效联动的核心概念:
- AppStorage:鸿蒙应用的全局状态容器,存放所有需要跨组件/跨页面同步的状态,是单一数据源;
- @StorageLink:双向绑定装饰器,组件可读写AppStorage中的状态值,状态变化时组件自动重建;
- @StorageProp:单向绑定装饰器,组件仅可读AppStorage中的状态值,适用于纯展示组件;
- 状态驱动动效:将Loading组件的
isLoading与AppStorage中的请求状态绑定,实现动效自动响应。
4.2 步骤1:定义全局状态模型(动效的状态依据)
在鸿蒙项目的src/main_pages目录下新建model文件夹(规范目录结构),在该文件夹下创建data_model.ets文件,定义全局状态模型,封装请求状态、业务逻辑和状态更新方法。
import axios from '@ohos/axios';
import promptAction from '@ohos.promptAction';
// 🔥 数据请求状态枚举(动效状态的唯一依据)
export enum DataStatus {
INITIAL = 'initial', // 初始状态:动效隐藏
LOADING = 'loading', // 加载中:动效显示
SUCCESS = 'success', // 请求成功:动效隐藏
FAILURE = 'failure' // 请求失败:动效隐藏并触发错误反馈
}
// 🔥 全局状态模型:基于AppStorage实现,所有状态全局共享
export class DataModel {
// 🔥 双向绑定全局状态:动效的核心判断依据
@StorageLink('dataStatus') dataStatus: DataStatus = DataStatus.INITIAL;
@StorageLink('data') data: any = ''; // 请求成功返回数据
@StorageLink('errorMsg') errorMsg: string = ''; // 请求失败错误信息
private cancelToken: any; // axios取消请求令牌
// 🔥 发起数据请求:更新全局状态,驱动动效变化
async fetchData() {
// 1. 更新为加载状态:自动显示Loading动效
this.dataStatus = DataStatus.LOADING;
// 初始化取消令牌
this.cancelToken = axios.CancelToken.source();
try {
// 2. 发起真实网络请求(模拟接口,可替换为实际业务接口)
const response = await axios.get('https://api.example.com/data', {
cancelToken: this.cancelToken.token,
timeout: 5000 // 超时时间:5秒
});
// 3. 请求成功:更新状态,自动隐藏动效
this.data = JSON.stringify(response.data);
this.dataStatus = DataStatus.SUCCESS;
this.errorMsg = '';
// 成功提示
promptAction.showToast({
message: '数据请求成功!',
backgroundColor: '#00C853',
duration: promptAction.ShowToastDuration.SHORT
});
} catch (e) {
// 4. 请求失败:更新状态,自动隐藏动效
this.errorMsg = e.toString();
this.dataStatus = DataStatus.FAILURE;
this.data = '';
// 失败提示
promptAction.showToast({
message: `请求失败:${e.toString().substring(0, 20)}...`,
backgroundColor: '#FF4444',
duration: promptAction.ShowToastDuration.LONG
});
}
}
// 🔥 取消数据请求:重置全局状态,自动隐藏动效
cancelRequest() {
if (this.dataStatus === DataStatus.LOADING) {
// 取消网络请求
this.cancelToken.cancel('用户主动取消请求');
// 重置为初始状态
this.dataStatus = DataStatus.INITIAL;
this.data = '';
this.errorMsg = '';
// 取消提示
promptAction.showToast({
message: '已取消请求',
backgroundColor: '#FF9800'
});
}
}
}
4.3 步骤2:UI层绑定全局状态(动效自动响应)
修改src/main_pages/index.ets文件,引入Day1封装的CommonLoading组件和上述全局状态模型,将Loading组件的isLoading与AppStorage中的dataStatus绑定,无需手动控制动效,动效将根据全局状态自动显示/隐藏。
import { DataModel, DataStatus } from './model/data_model';
import CommonLoading from './widgets/CommonLoading'; // 引入Day1封装的Loading组件
import { Column, Text, Button, FlexAlign, ItemAlign, Color, FontWeight } from '@ohos/ui';
@Entry
@Component
struct DataSyncPage {
// 实例化全局状态模型
private dataModel = new DataModel();
build() {
Column() {
// 🔥 绑定Loading组件与全局状态:完全由dataStatus决定
CommonLoading({
isLoading: $dataModel.dataStatus === DataStatus.LOADING,
color: Color.Blue,
size: 60
})
// 🔥 根据全局状态显示不同页面内容
Column({ space: 10 }) {
if (this.dataModel.dataStatus === DataStatus.INITIAL) {
Text('点击下方按钮发起数据请求')
.fontSize(18)
.fontWeight(FontWeight.Normal);
}
if (this.dataModel.dataStatus === DataStatus.SUCCESS) {
Text('请求结果:')
.fontSize(16)
.fontWeight(FontWeight.Bold);
Text(this.dataModel.data)
.fontSize(14)
.maxLines(3)
.textOverflow(TextOverflow.Ellipsis);
}
if (this.dataModel.dataStatus === DataStatus.FAILURE) {
Text('错误信息:')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(Color.Red);
Text(this.dataModel.errorMsg)
.fontSize(14)
.fontColor(Color.Red)
.maxLines(2)
.textOverflow(TextOverflow.Ellipsis);
}
}
.width('80%')
.margin({ bottom: 40 })
.textAlign(TextAlign.Center);
// 🔥 发起请求按钮
Button('发起数据请求')
.onClick(() => this.dataModel.fetchData())
.width(200)
.height(50)
.fontSize(16)
.backgroundColor(Color.Blue)
.fontColor(Color.White);
// 🔥 取消请求按钮:仅加载中时可点击
Button('取消请求')
.onClick(() => this.dataModel.cancelRequest())
.width(200)
.height(50)
.margin({ top: 15 })
.fontSize(16)
.backgroundColor(Color.Grey)
.fontColor(Color.White)
.enabled(this.dataModel.dataStatus === DataStatus.LOADING);
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(ItemAlign.Center);
}
}
4.4 运行验证(动效与全局状态自动联动)
运行鸿蒙项目(模拟器/真实设备/Hi3516开发板),验证动效与数据请求状态的联动效果,全程无任何手动控制动效的代码,所有变化均由AppStorage全局状态驱动:
- 点击「发起数据请求」:自动显示Loading动效,请求过程中动效持续运行;
- 请求成功:自动隐藏动效,显示返回数据并弹出绿色成功Toast;
- 模拟网络错误(断网/修改接口地址):自动隐藏动效,显示错误信息并弹出红色失败Toast;
- 加载中点击「取消请求」:立即隐藏动效,状态重置为初始值,弹出橙色取消提示。
五、Day2关键总结(必记,后续实操高频用到) 📌
- 核心思想:解决动效与数据请求状态脱节的根本方法是统一状态管理,让动效状态完全由数据请求状态驱动,禁止任何手动控制动效的操作。
- Flutter端核心:Bloc通过「Event→Bloc→State→UI/动效」的单向数据流,实现业务逻辑与UI的完全分离,状态流转可追溯,适合复杂跨端应用。
- 鸿蒙端核心:AppStorage通过全局状态容器+双向绑定,实现跨组件/跨页面的状态同步,代码简洁、开发效率高,完美适配鸿蒙Stage模型。
- 双端统一原则:均遵循「单一数据源」和「状态驱动UI/动效」,仅技术实现不同,核心逻辑和最终效果完全一致。
- 动效判断依据:双端均将数据请求状态枚举作为动效显示/隐藏的唯一依据,让动效成为请求状态的「附属值」,从根源上避免状态不一致。
- 后续铺垫:Day2完成的状态统一管理,是Day3「动效资源预加载+智能时序调度」和Day4「低性能设备动效适配」的基础,所有动效优化都将基于此状态层实现。
明日预告(Day17) 🚀
Day3将聚焦跨端动效的用户体验优化,解决Day1/Day2未覆盖的「动效延迟」「短请求动效一闪而过」「长请求无进度反馈」等问题,实现:
- 动效资源预加载:解决动效触发时的「空白期」,提升动效启动速度;
- 智能时序调度:短请求(<300ms)屏蔽动效,长请求(>3s)显示进度反馈;
- 双端实操:分别在Flutter和鸿蒙端实现上述优化,基于Day2的状态管理层进行扩展。
所有优化均贴合低性能设备(Hi3516开发板、百元安卓机)的运行场景,兼顾流畅度和用户体验!
更多推荐




所有评论(0)