【Harmonyos】Flutter开源鸿蒙跨平台训练营 Day17动效与数据请求联动:资源预加载+智能时序调度
本文是Flutter/鸿蒙跨端开发系列教程的第三部分,重点解决动效延迟和时序混乱两大体验问题。通过动效资源预加载(提前加载图片、初始化组件)消除动效空白期,采用智能时序调度策略(最短300ms展示、最长3s切换简化动效)优化用户等待感知。教程提供双端完整代码实现,承接前两天的Loading组件和状态管理逻辑,特别适配低性能设备场景,为后续性能优化奠定基础。包含详细实操步骤、验证方法和避坑指南,适合
【Flutter/鸿蒙跨端开发】动效与数据请求联动+低性能适配实战(Day3):资源预加载+智能时序调度
🔍 摘要
本文为Flutter/鸿蒙跨端动效开发系列教程Day3核心内容,针对Day1/Day2未解决的动效延迟(触发后空白期)和时序混乱(短请求一闪而过、长请求无反馈)两大高频体验痛点,实现双端统一的优化方案:
- 动效资源预加载:通过提前加载图片、初始化动效组件,解决动效触发后100-300ms空白期、首次渲染卡顿问题;
- 智能时序调度:制定「最短300ms展示、最长3s切换简化动效、超时触发错误反馈」的统一规则,集成到Day2的状态管理中(Flutter Bloc/鸿蒙DataModel),优化用户等待感知。
全程承接Day1的CommonLoading组件、Day2的状态同步逻辑,提供双端完整可运行代码、实操步骤、验证方法及避坑指南,所有方案均贴合低性能设备开发场景,为Day4低性能适配铺垫核心基础。
📝 前言
Day2我们已通过Bloc(Flutter)和AppStorage(鸿蒙)✅,解决了动效与数据状态脱节的核心痛点,实现了「请求触发动效、状态控制动效」的基础联动。但在真实商业开发中,仍有两个直接影响用户体验的高频问题未解决:
- 🌀 动效延迟:数据请求触发后,骨架屏、加载指示器动效加载缓慢,出现100-300ms空白期,首次触发动效时渲染卡顿;
- ⏰ 动效时序混乱:短请求(<300ms)动效一闪而过、长请求(>3s)动效单调循环无反馈、超时请求无异常提示,用户等待感知差。
Day3将聚焦这两个痛点,手把手实现「动效资源预加载」和「智能时序调度」💡,让双端动效既流畅无延迟,又能贴合用户等待感知。所有实操均承接前两日的代码逻辑,不重新开发基础组件,体现「增量开发」的真实项目思路,新手可直接复制代码落地,附带详细注释和避坑点。
一、Day3核心痛点与解决方案(必懂)📊
先明确Day3要解决的2个核心问题及对应方案,建立整体思路,后续实操不迷路:
| 核心痛点 | 具体表现 | 解决方案 | 核心目标 | 权威参考 |
|---|---|---|---|---|
| 动效延迟 🌀 | 1. 动效触发后出现100-300ms空白期; 2. 首次触发动效时组件初始化耗时,渲染卡顿; 3. 图片类动效(骨架屏)加载缓慢 |
动效资源预加载 📦 | 1. 提前加载动效图片、配置资源; 2. 预初始化复杂动效组件; 3. 动效触发时直接复用,无空白、无延迟 |
Flutter官方资源预加载指南、鸿蒙资源管理官方文档 |
| 时序混乱 ⏰ | 1. 短请求(<300ms):动效一闪而过,用户误以为未加载; 2. 长请求(>3s):动效单调循环,用户产生焦虑; 3. 超时请求:无异常提示,用户不知是否等待 |
智能时序调度 ⚙️ | 1. 最短展示300ms,避免一闪而过; 2. 最长展示3s,超时切换简化动效+进度提示; 3. 超时/失败触发错误动效+重试按钮 |
Google Material Design动效时序规范、鸿蒙动效设计指南 |
🔗 关键关联(承接Day1、Day2)
- 预加载的资源:均为Day1封装的CommonLoading组件、骨架屏等动效相关资源,不新增无关联内容;
- 时序调度逻辑:集成到Day2的状态管理中(Flutter Bloc/鸿蒙DataModel),不破坏原有状态同步逻辑,仅增强时序控制;
- 双端统一原则:核心规则、逻辑完全一致,仅API/语言不同,重点记忆「预加载时机」和「时序规则」,无需单独记忆双端差异。
二、动效资源预加载(双端实战,解决延迟问题)📦
动效延迟的根源:动效所需资源(图片、复杂组件、动画配置)在请求触发后才开始加载,IO操作+组件初始化耗时导致空白期。
核心方案:提前加载资源,在应用启动/页面显示前完成初始化,请求触发时直接复用。
📜 核心预加载资源清单(双端统一)
- 图片资源:骨架屏图片、加载指示器背景图、错误动效图片;
- 动效组件:提前初始化复杂加载动效(Flutter SpinKit/鸿蒙LoadingProgress),避免首次渲染卡顿;
- 配置资源:动效时序配置、颜色配置,提前解析减少运行时计算。
(一)Flutter端:动效资源预加载(Dart实操)🚀
Flutter端预加载核心:使用precacheImage预加载图片,通过Future预初始化组件,结合应用生命周期确保加载时机合理,不影响启动速度。
3.1 步骤1:创建预加载工具类(复用性强)
在lib目录下新建utils文件夹,创建preload_utils.dart文件,通用方法可直接复制:
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
/// Flutter动效资源预加载工具类(Day3核心)
/// 提前加载动效相关资源,解决动效延迟、首次渲染卡顿问题
/// 预加载时机:应用启动时、页面显示前(按需选择)
class PreloadUtils {
/// 1. 预加载动效图片资源(骨架屏、加载背景等)
/// context:BuildContext,用于关联当前页面资源
/// imagePaths:图片路径列表(assets目录下的图片)
static Future<void> preloadAnimationImages(BuildContext context, List<String> imagePaths) async {
// 遍历图片路径,逐一预加载,await确保所有图片加载完成
for (final path in imagePaths) {
try {
await precacheImage(
AssetImage(path), // 加载assets目录下的图片
context,
sizeHint: ImageConfiguration(size: const Size(100, 100)), // 预设图片大小,提升加载效率
);
debugPrint('✅ Flutter动效图片预加载完成:$path'); // 日志验证
} catch (e) {
debugPrint('❌ Flutter动效图片预加载失败:$path,错误:$e'); // 异常处理
}
}
}
/// 2. 预初始化复杂动效组件(如SpinKit系列、自定义加载组件)
/// 提前初始化,避免首次触发时组件初始化耗时导致的延迟
static Future<void> preloadAnimationComponents() async {
// 预初始化Day1用到的SpinKitCircle动效(核心加载组件)
await Future.delayed(const Duration(milliseconds: 50)); // 短暂延迟,确保初始化完成
const SpinKitCircle(color: Colors.blue, size: 50.0); // 提前创建实例,缓存到内存
debugPrint('✅ Flutter动效组件预初始化完成');
}
/// 3. 全局统一预加载(应用启动时调用,一次性加载所有核心资源)
static Future<void> globalPreload(BuildContext context) async {
// 定义需要预加载的图片资源(根据项目修改路径)
const animationImages = [
'assets/loading_bg.png', // 加载动效背景图
'assets/skeleton.png', // 骨架屏图片
'assets/error_icon.png', // 错误动效图片
];
// 并行预加载,提升加载速度(图片 + 组件同时进行)
await Future.wait([
preloadAnimationImages(context, animationImages),
preloadAnimationComponents(),
]);
debugPrint('✅ Flutter全局动效资源预加载完成');
}
}
3.2 步骤2:配置预加载时机(关键,避免无效加载)
预加载时机直接影响效果,推荐2种方案(按需选择),均承接Day2项目结构:
🕒 时机1:应用启动时预加载(推荐,资源少场景)
修改lib/main.dart,在应用启动时调用全局预加载,确保页面显示前资源加载完成:
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'bloc/data_bloc.dart';
import 'widgets/common_loading.dart';
import 'utils/preload_utils.dart'; // 引入预加载工具类
void main() async {
// 确保Flutter绑定初始化完成,避免预加载资源异常
WidgetsFlutterBinding.ensureInitialized();
// 启动应用前执行全局预加载(核心步骤)
final app = const MyApp();
await runApp(
Builder(
builder: (context) {
PreloadUtils.globalPreload(context); // 执行预加载
return app;
},
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => DataBloc(), // Day2的Bloc,后续集成时序调度
child: MaterialApp(
title: 'Flutter预加载+时序调度(Day3)',
theme: ThemeData(primarySwatch: Colors.blue),
home: const AnimationOptimizePage(), // 新建动效优化演示页
),
);
}
}
🕒 时机2:页面显示前预加载(资源多场景)
若资源较多,在具体页面初始化时预加载,避免影响应用启动速度:
class AnimationOptimizePage extends StatefulWidget {
const AnimationOptimizePage({super.key});
State<AnimationOptimizePage> createState() => _AnimationOptimizePageState();
}
class _AnimationOptimizePageState extends State<AnimationOptimizePage> {
bool _isPreloaded = false; // 标记预加载是否完成
void initState() {
super.initState();
_preloadPageResources(); // 页面初始化时预加载
}
/// 页面级预加载:仅加载当前页面所需动效资源
Future<void> _preloadPageResources() async {
const pageImages = [
'assets/loading_bg.png',
'assets/skeleton.png',
];
await PreloadUtils.preloadAnimationImages(context, pageImages);
await PreloadUtils.preloadAnimationComponents();
setState(() => _isPreloaded = true); // 预加载完成,更新状态
}
Widget build(BuildContext context) {
// 预加载未完成时显示简单占位(避免空白)
if (!_isPreloaded) {
return const Scaffold(body: Center(child: Text('初始化中...⏳')));
}
// 预加载完成,显示正常页面(后续集成时序调度)
return const Scaffold(
appBar: AppBar(title: Text('Flutter动效优化(Day3)')),
body: Center(child: Text('预加载完成,可触发动效✅')),
);
}
}
3.3 步骤3:验证预加载效果(必做,确保生效)
通过3种方式验证预加载是否生效,新手必做:
- 🎯 视觉验证:首次点击“发起请求”,Loading动效应立即显示,无任何空白期(预加载前有100-300ms空白);
- 📝 日志验证:运行项目,查看控制台是否打印“✅ Flutter动效图片预加载完成”,且在动效触发前执行;
- 📈 性能验证:打开Flutter DevTools(View→Tool Windows→Flutter DevTools),查看Performance面板,首次渲染动效时帧率无明显下降(预加载前帧率波动,预加载后稳定)。
(二)鸿蒙端:动效资源预加载(ArkTS实操)📱
鸿蒙端预加载核心:通过resourceManager提前加载图片资源,Future预初始化组件,适配Stage模型的aboutToAppear生命周期,逻辑与Flutter完全一致。
3.1 步骤1:创建预加载工具类
在src/main_pages/utils目录下(新建utils文件夹),创建preload_utils.ets文件:
import resourceManager from '@ohos.resourceManager';
import { LoadingProgress } from '@ohos/ui';
/// 鸿蒙动效资源预加载工具类(Day3核心)
/// 适配Stage模型,提前加载动效资源,解决延迟问题
export class PreloadUtils {
/// 1. 预加载动效图片资源(骨架屏、加载背景等)
/// imageNames:图片名称列表(main_pages/media目录下)
static async preloadAnimationImages(imageNames: string[]): Promise<void> {
try {
const resMgr = await resourceManager.getResourceManager();
// 遍历图片名称,逐一预加载(缓存到内存)
for (const name of imageNames) {
await resMgr.getMediaContent(name);
console.log(`✅ 鸿蒙动效图片预加载完成:${name}`); // 日志验证
}
} catch (e) {
console.error(`❌ 鸿蒙动效图片预加载失败:${e}`); // 异常处理
}
}
/// 2. 预初始化复杂动效组件(如LoadingProgress、自定义动效)
static async preloadAnimationComponents(): Promise<void> {
// 短暂延迟确保初始化完成
await new Promise((resolve) => setTimeout(resolve, 50));
// 提前创建组件实例,缓存到内存
LoadingProgress().width(50).height(50).color(Color.Blue);
console.log('✅ 鸿蒙动效组件预初始化完成');
}
/// 3. 全局统一预加载(应用启动时调用)
static async globalPreload(): Promise<void> {
// 定义需要预加载的图片资源(media目录下)
const animationImages = [
'loading_bg.png',
'skeleton.png',
'error_icon.png',
];
// 并行预加载,提升速度
await Promise.all([
this.preloadAnimationImages(animationImages),
this.preloadAnimationComponents(),
]);
console.log('✅ 鸿蒙全局动效资源预加载完成');
}
}
3.2 步骤2:配置预加载时机(适配Stage模型)
🕒 时机1:应用启动时预加载(推荐)
修改src/main_pages/index.ets,在aboutToAppear(页面显示前)执行预加载:
import { PreloadUtils } from './utils/preload_utils';
import { DataModel, DataStatus } from './model/data_model'; // Day2的状态模型
import CommonLoading from './widgets/CommonLoading'; // Day1的Loading组件
import { Column, Text, FlexAlign, ItemAlign, Color } from '@ohos/ui';
@Entry
@Component
struct AnimationOptimizePage {
private dataModel = new DataModel(); // Day2的状态模型
@State isPreloaded: boolean = false; // 标记预加载完成
// Stage模型核心生命周期:页面显示前执行
async aboutToAppear() {
await PreloadUtils.globalPreload(); // 执行全局预加载
this.isPreloaded = true;
}
build() {
// 预加载未完成时显示占位
if (!this.isPreloaded) {
return Column()
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(ItemAlign.Center)
.children([Text('初始化中...⏳').fontSize(16)]);
}
// 预加载完成,显示正常页面
return Column() {
CommonLoading({ isLoading: $dataModel.dataStatus === DataStatus.LOADING })
Text('预加载完成,可触发动效✅')
.fontSize(18)
.margin({ bottom: 30 });
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(ItemAlign.Center);
}
}
🕒 时机2:页面级预加载(资源多场景)
async aboutToAppear() {
// 仅加载当前页面所需资源
await PreloadUtils.preloadAnimationImages(['loading_bg.png', 'skeleton.png']);
await PreloadUtils.preloadAnimationComponents();
this.isPreloaded = true;
}
3.3 步骤3:验证预加载效果(鸿蒙端必做)
- 🎯 视觉验证:首次触发动效无空白期,立即显示;
- 📝 日志验证:DevEco Studio终端打印“✅ 鸿蒙动效图片预加载完成”;
- 📈 性能验证:打开Profiler工具,查看CPU使用率,首次渲染动效时无明显飙升。
⚠️ 预加载避坑点(新手必看)
- 📁 资源路径错误:Flutter图片需在
pubspec.yaml配置,鸿蒙图片需放在media目录,否则预加载失败; - 🕒 时机不当:避免在
build(Flutter)/build(鸿蒙)方法中执行预加载(多次执行导致重复加载); - 📦 资源过多:资源多优先选择“页面级预加载”,避免应用启动缓慢;
- 🛡️ 异常处理:必须添加try-catch,避免预加载失败导致应用崩溃(上述代码已包含)。
三、智能时序调度(双端实战,解决时序混乱)⚙️
解决动效延迟后,针对「短请求一闪而过、长请求无反馈」问题,制定统一时序规则,集成到Day2的状态管理中。
📜 智能时序调度核心规则(双端通用,必记)
| 规则类型 | 数值 | 具体逻辑 | 用户体验目标 |
|---|---|---|---|
| 最短展示时长 | 300ms | 即使请求耗时<300ms,动效也展示满300ms | 避免一闪而过,让用户感知“正在加载” |
| 最长展示时长 | 3s | 请求耗时>3s,切换为简化动效+进度提示 | 减少用户焦虑,提供等待反馈 |
| 异常处理 | 3s超时 | 超时/失败触发错误动效+重试按钮;取消请求立即终止动效 | 引导用户操作,避免无意义等待 |
🚦 时序调度核心逻辑(双端统一)
- 🎬 请求开始:启动计时器记录开始时间,同时启动3s超时定时器;
- ⏱️ 请求过程:<300ms则等待至300ms;>3s则切换简化动效+进度提示;
- 🎯 请求结束:终止所有定时器,根据结果更新动效状态;
- ❌ 请求取消:立即终止定时器和动效,重置状态。
(一)Flutter端:时序调度集成(承接Day2的Bloc)
将时序调度工具类集成到Day2的DataBloc中,通过Event/State联动,不破坏原有逻辑。
3.1 步骤1:创建时序调度工具类
在lib/utils目录下,创建timing_scheduler.dart文件:
import 'package:flutter/material.dart';
/// Flutter智能时序调度工具类(Day3核心)
/// 控制动效展示时长、状态切换,解决时序混乱问题
class TimingScheduler {
DateTime? _startTime; // 请求开始时间
Timer? _timeoutTimer; // 3s超时定时器
bool _isTimeout = false; // 是否超时
Function? _onFinish; // 请求完成回调
Function? _onTimeout; // 超时回调
/// 1. 启动时序调度(请求开始时调用)
void start({required Function onFinish, required Function onTimeout}) {
_startTime = DateTime.now();
_onFinish = onFinish;
_onTimeout = onTimeout;
// 启动3s超时定时器
_timeoutTimer = Timer(const Duration(seconds: 3), () {
_isTimeout = true;
_onTimeout?.call(); // 触发超时逻辑
});
}
/// 2. 结束时序调度(请求完成/失败时调用)
Future<void> finish() async {
_timeoutTimer?.cancel(); // 取消超时定时器
if (_startTime == null) return;
// 计算请求耗时(毫秒)
final duration = DateTime.now().difference(_startTime!).inMilliseconds;
// 耗时<300ms,等待至300ms
if (duration < 300) {
await Future.delayed(Duration(milliseconds: 300 - duration));
}
_onFinish?.call(); // 触发完成回调
_reset(); // 重置状态
}
/// 3. 取消时序调度(用户取消请求时调用)
void cancel() {
_timeoutTimer?.cancel();
_reset();
}
/// 重置调度器状态
void _reset() {
_startTime = null;
_timeoutTimer = null;
_isTimeout = false;
_onFinish = null;
_onTimeout = null;
}
/// 判断是否超时
bool get isTimeout => _isTimeout;
}
3.2 步骤2:集成到DataBloc(承接Day2)
修改lib/bloc/data_bloc.dart,添加时序调度逻辑,不破坏原有状态同步:
import 'package:bloc/bloc.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import '../utils/timing_scheduler.dart'; // 引入时序调度工具类
// 保留Day2的枚举,新增timeout状态
enum DataStatus { initial, loading, success, failure, timeout }
class DataState {
final DataStatus status;
final dynamic data;
final String? errorMsg;
final bool isSimplifiedAnimation; // 新增:是否显示简化动效
DataState({
required this.status,
this.data,
this.errorMsg,
this.isSimplifiedAnimation = false,
});
factory DataState.initial() => DataState(status: DataStatus.initial);
DataState copyWith({
DataStatus? status,
dynamic data,
String? errorMsg,
bool? isSimplifiedAnimation,
}) {
return DataState(
status: status ?? this.status,
data: data ?? this.data,
errorMsg: errorMsg ?? this.errorMsg,
isSimplifiedAnimation: isSimplifiedAnimation ?? this.isSimplifiedAnimation,
);
}
}
abstract class DataEvent {}
class FetchDataEvent extends DataEvent {
final BuildContext context;
FetchDataEvent(this.context);
}
class CancelRequestEvent extends DataEvent {}
class TimeoutEvent extends DataEvent {} // 新增:超时事件
class DataBloc extends Bloc<DataEvent, DataState> {
late CancelToken _cancelToken;
final TimingScheduler _scheduler = TimingScheduler(); // 初始化调度器
DataBloc() : super(DataState.initial()) {
on<FetchDataEvent>(_onFetchData);
on<CancelRequestEvent>(_onCancelRequest);
on<TimeoutEvent>(_onTimeout); // 监听超时事件
}
// 处理数据请求(核心修改:添加时序调度)
Future<void> _onFetchData(FetchDataEvent event, Emitter<DataState> emit) async {
// 1. 触发加载状态
emit(state.copyWith(status: DataStatus.loading, isSimplifiedAnimation: false));
// 2. 启动时序调度
_scheduler.start(
onFinish: () {
if (_scheduler.isTimeout) emit(state.copyWith(status: DataStatus.timeout));
},
onTimeout: () => add(TimeoutEvent()), // 触发超时事件
);
_cancelToken = CancelToken();
try {
// 3. 发起网络请求(可修改超时时间测试)
final response = await Dio().get(
'https://api.example.com/data',
cancelToken: _cancelToken,
// options: Options(receiveTimeout: const Duration(milliseconds: 200)), // 短请求
// options: Options(receiveTimeout: const Duration(seconds: 4)), // 长请求
);
// 4. 请求成功:等待时序调度完成(确保300ms)
await _scheduler.finish();
emit(state.copyWith(
status: DataStatus.success,
data: response.data,
isSimplifiedAnimation: false,
));
// 成功提示(保留Day2逻辑)
if (event.context.mounted) {
ScaffoldMessenger.of(event.context).showSnackBar(
const SnackBar(content: Text('✅ 请求成功!'), backgroundColor: Colors.green),
);
}
} catch (e) {
// 5. 请求失败:终止时序调度
_scheduler.cancel();
emit(state.copyWith(
status: DataStatus.failure,
errorMsg: e.toString(),
isSimplifiedAnimation: false,
));
// 失败提示(保留Day2逻辑)
if (event.context.mounted) {
ScaffoldMessenger.of(event.context).showSnackBar(
SnackBar(content: Text('❌ 请求失败:${e.toString()}'), backgroundColor: Colors.red),
);
}
}
}
// 处理超时事件(切换简化动效)
Future<void> _onTimeout(TimeoutEvent event, Emitter<DataState> emit) async {
emit(state.copyWith(
status: DataStatus.timeout,
isSimplifiedAnimation: true,
));
// 超时提示
if (event is FetchDataEvent && event.context.mounted) {
ScaffoldMessenger.of(event.context).showSnackBar(
const SnackBar(content: Text('⏳ 加载中...已耗时3s,点击重试可刷新'), backgroundColor: Colors.orange),
);
}
}
// 处理取消请求(新增时序调度取消)
Future<void> _onCancelRequest(CancelRequestEvent event, Emitter<DataState> emit) async {
_cancelToken.cancel('用户主动取消');
_scheduler.cancel(); // 取消调度器
emit(DataState.initial());
}
}
3.3 步骤3:UI层适配时序调度(修改Loading+页面)
第一步:修改CommonLoading组件(支持简化动效)
修改lib/widgets/common_loading.dart,添加isSimplified参数:
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
class CommonLoading extends StatelessWidget {
final bool isLoading;
final Widget child;
final Color color;
final double size;
final bool isSimplified; // 新增:简化动效标记
const CommonLoading({
super.key,
required this.isLoading,
required this.child,
this.color = Colors.blue,
this.size = 50.0,
this.isSimplified = false,
});
Widget build(BuildContext context) {
return Stack(
children: [
child,
if (isLoading)
Container(
width: double.infinity,
height: double.infinity,
color: Colors.black.withOpacity(0.3),
child: Center(
// 根据标记显示不同动效
child: isSimplified
? // 简化动效:轻量旋转,减少性能消耗
CircularProgressIndicator(
color: color,
strokeWidth: 2,
)
: // 正常动效:Day1的SpinKitCircle
SpinKitCircle(
color: color,
size: size,
),
),
)
],
);
}
}
第二步:修改页面,绑定时序状态
修改lib/main.dart中的AnimationOptimizePage:
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'bloc/data_bloc.dart';
import 'widgets/common_loading.dart';
import 'utils/preload_utils.dart';
class AnimationOptimizePage extends StatelessWidget {
const AnimationOptimizePage({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter预加载+时序调度(Day3)'),
centerTitle: true,
),
body: BlocBuilder<DataBloc, DataState>(
builder: (context, state) {
return CommonLoading(
isLoading: state.status == DataStatus.loading || state.status == DataStatus.timeout,
isSimplified: state.isSimplifiedAnimation, // 绑定简化动效状态
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 根据状态显示内容
if (state.status == DataStatus.initial)
const Text('点击按钮发起请求⏯️', style: TextStyle(fontSize: 18)),
if (state.status == DataStatus.success)
Text('✅ 请求结果:${state.data}', style: TextStyle(fontSize: 16)),
if (state.status == DataStatus.failure)
Text('❌ 错误:${state.errorMsg}', style: TextStyle(color: Colors.red, fontSize: 16)),
if (state.status == DataStatus.timeout)
const Text('⏳ 加载超时,可点击重试', style: TextStyle(color: Colors.orange, fontSize: 16)),
const SizedBox(height: 30),
// 发起请求按钮
ElevatedButton(
onPressed: () => context.read<DataBloc>().add(FetchDataEvent(context)),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 12),
),
child: const Text('发起数据请求'),
),
const SizedBox(height: 15),
// 取消请求按钮
ElevatedButton(
onPressed: (state.status == DataStatus.loading || state.status == DataStatus.timeout)
? () => context.read<DataBloc>().add(CancelRequestEvent())
: null,
style: ElevatedButton.styleFrom(backgroundColor: Colors.grey),
child: const Text('取消请求'),
),
const SizedBox(height: 15),
// 重试按钮
ElevatedButton(
onPressed: (state.status == DataStatus.failure || state.status == DataStatus.timeout)
? () => context.read<DataBloc>().add(FetchDataEvent(context))
: null,
style: ElevatedButton.styleFrom(backgroundColor: Colors.orange),
child: const Text('重试请求'),
),
],
),
),
);
},
),
);
}
}
3.4 步骤4:验证时序调度效果(必做)
测试4种核心场景,确保时序规则生效:
- ⚡ 短请求(<300ms):动效展示满300ms后隐藏,不一闪而过;
- 🕒 长请求(>3s):3s后切换为简化动效,弹出“加载中…已耗时3s”提示;
- ❌ 超时/失败:显示简化动效+错误提示,点击“重试”可重新请求;
- 🚫 取消请求:动效立即隐藏,状态重置为初始值。
(二)鸿蒙端:时序调度集成(承接Day2的DataModel)
鸿蒙端时序调度逻辑与Flutter完全一致,集成到Day2的DataModel中,通过AppStorage绑定状态。
3.1 步骤1:创建时序调度工具类
在src/main_pages/utils目录下,创建timing_scheduler.ets文件:
/// 鸿蒙智能时序调度工具类(Day3核心)
/// 逻辑与Flutter端完全一致,适配ArkTS语言
export class TimingScheduler {
private startTime: number | null = null; // 请求开始时间(毫秒)
private timeoutTimer: number | null = null; // 3s超时定时器
private isTimeout: boolean = false; // 是否超时
private onFinish: Function | null = null; // 完成回调
private onTimeout: Function | null = null; // 超时回调
/// 1. 启动时序调度
start({ onFinish, onTimeout }: { onFinish: Function, onTimeout: Function }): void {
this.startTime = new Date().getTime();
this.onFinish = onFinish;
this.onTimeout = onTimeout;
// 启动3s超时定时器
this.timeoutTimer = setTimeout(() => {
this.isTimeout = true;
this.onTimeout?.call();
}, 3000);
}
/// 2. 结束时序调度
async finish(): Promise<void> {
if (this.timeoutTimer) clearTimeout(this.timeoutTimer);
if (!this.startTime) return;
// 计算耗时,不足300ms则等待
const duration = new Date().getTime() - this.startTime;
if (duration < 300) {
await new Promise((resolve) => setTimeout(resolve, 300 - duration));
}
this.onFinish?.call();
this._reset();
}
/// 3. 取消时序调度
cancel(): void {
if (this.timeoutTimer) clearTimeout(this.timeoutTimer);
this._reset();
}
/// 重置状态
private _reset(): void {
this.startTime = null;
this.timeoutTimer = null;
this.isTimeout = false;
this.onFinish = null;
this.onTimeout = null;
}
/// 获取超时状态
getIsTimeout(): boolean {
return this.isTimeout;
}
}
3.2 步骤2:集成到DataModel(承接Day2)
修改src/main_pages/model/data_model.ets:
import axios from '@ohos/axios';
import promptAction from '@ohos.promptAction';
import { TimingScheduler } from '../utils/timing_scheduler';
// 新增timeout状态
export enum DataStatus {
INITIAL = 'initial',
LOADING = 'loading',
SUCCESS = 'success',
FAILURE = 'failure',
TIMEOUT = 'timeout'
}
export class DataModel {
@StorageLink('dataStatus') dataStatus: DataStatus = DataStatus.INITIAL;
@StorageLink('data') data: any = '';
@StorageLink('errorMsg') errorMsg: string = '';
@StorageLink('isSimplifiedAnimation') isSimplifiedAnimation: boolean = false; // 简化动效状态
private cancelToken: any;
private scheduler: TimingScheduler = new TimingScheduler(); // 初始化调度器
// 发起数据请求(集成时序调度)
async fetchData() {
// 1. 重置状态
this.dataStatus = DataStatus.LOADING;
this.isSimplifiedAnimation = false;
this.cancelToken = axios.CancelToken.source();
// 2. 启动时序调度
this.scheduler.start({
onFinish: () => {
if (this.scheduler.getIsTimeout()) {
this.dataStatus = DataStatus.TIMEOUT;
}
},
onTimeout: () => {
// 超时:切换简化动效+提示
this.dataStatus = DataStatus.TIMEOUT;
this.isSimplifiedAnimation = true;
promptAction.showToast({
message: '⏳ 加载中...已耗时3s,点击重试可刷新',
backgroundColor: '#FF9800'
});
}
});
try {
// 3. 发起请求(可修改超时时间测试)
const response = await axios.get('https://api.example.com/data', {
cancelToken: this.cancelToken.token,
// timeout: 200, // 短请求
// timeout: 4000, // 长请求
});
// 4. 请求成功:等待时序调度完成
await this.scheduler.finish();
this.dataStatus = DataStatus.SUCCESS;
this.data = response.data;
promptAction.showToast({ message: '✅ 请求成功!', backgroundColor: '#00C853' });
} catch (e) {
// 5. 请求失败:终止调度器
this.scheduler.cancel();
this.dataStatus = DataStatus.FAILURE;
this.errorMsg = e.toString();
promptAction.showToast({ message: `❌ 请求失败:${e}`, backgroundColor: '#FF4444' });
}
}
// 取消请求(新增调度器取消)
cancelRequest() {
this.cancelToken.cancel('用户主动取消');
this.scheduler.cancel();
this.dataStatus = DataStatus.INITIAL;
this.isSimplifiedAnimation = false;
}
// 重试请求(新增)
retryRequest() {
this.fetchData();
}
}
3.3 步骤3:UI层适配时序调度
第一步:修改CommonLoading组件
修改src/main_pages/widgets/CommonLoading.ets:
import { Stack, Column, LoadingProgress, Color, FlexAlign, ItemAlign } from '@ohos/ui';
@Component
export default struct CommonLoading {
@Link isLoading: boolean;
color: Color = Color.Blue;
size: number = 50;
@Link isSimplified: boolean; // 双向绑定简化动效状态
build() {
Stack() {
if (this.isLoading) {
Column() {
// 根据状态显示不同动效
this.isSimplified
? // 简化动效:轻量、低性能消耗
LoadingProgress()
.width(30)
.height(30)
.color(this.color)
.strokeWidth(2)
: // 正常动效:Day1的样式
LoadingProgress()
.width(this.size)
.height(this.size)
.color(this.color);
}
.width('100%')
.height('100%')
.backgroundColor(Color.Black.opacity(0.3))
.justifyContent(FlexAlign.Center)
.alignItems(ItemAlign.Center);
}
}
}
}
第二步:修改页面,绑定时序状态
修改src/main_pages/index.ets:
import { PreloadUtils } from './utils/preload_utils';
import { DataModel, DataStatus } from './model/data_model';
import CommonLoading from './widgets/CommonLoading';
import { Column, Text, Button, FlexAlign, ItemAlign, Color } from '@ohos/ui';
@Entry
@Component
struct AnimationOptimizePage {
private dataModel = new DataModel();
@State isPreloaded: boolean = false;
async aboutToAppear() {
await PreloadUtils.globalPreload(); // 预加载
this.isPreloaded = true;
}
build() {
if (!this.isPreloaded) {
return Column()
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(ItemAlign.Center)
.children([Text('初始化中...⏳').fontSize(16)]);
}
return Column() {
// 绑定Loading组件
CommonLoading({
isLoading: $dataModel.dataStatus === DataStatus.LOADING || $dataModel.dataStatus === DataStatus.TIMEOUT,
isSimplified: $dataModel.isSimplifiedAnimation,
color: Color.Blue
})
// 状态文本
if (this.dataModel.dataStatus === DataStatus.INITIAL) {
Text('点击按钮发起请求⏯️')
.fontSize(18)
.margin({ bottom: 30 });
}
if (this.dataModel.dataStatus === DataStatus.SUCCESS) {
Text(`✅ 请求结果:${this.dataModel.data}`)
.fontSize(16)
.margin({ bottom: 30 });
}
if (this.dataModel.dataStatus === DataStatus.FAILURE) {
Text(`❌ 错误:${this.dataModel.errorMsg}`)
.fontSize(16)
.fontColor(Color.Red)
.margin({ bottom: 30 });
}
if (this.dataModel.dataStatus === DataStatus.TIMEOUT) {
Text('⏳ 加载超时,可点击重试')
.fontSize(16)
.fontColor(Color.Orange)
.margin({ bottom: 30 });
}
// 操作按钮
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 })
.backgroundColor(Color.Grey)
.fontColor(Color.White)
.enabled(this.dataModel.dataStatus === DataStatus.LOADING || this.dataModel.dataStatus === DataStatus.TIMEOUT);
Button('重试请求')
.onClick(() => this.dataModel.retryRequest())
.width(200)
.height(50)
.margin({ top: 15 })
.backgroundColor(Color.Orange)
.fontColor(Color.White)
.enabled(this.dataModel.dataStatus === DataStatus.FAILURE || this.dataModel.dataStatus === DataStatus.TIMEOUT);
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(ItemAlign.Center);
}
}
3.4 步骤4:验证时序调度效果(鸿蒙端必做)
与Flutter端测试场景一致,逐一验证:
- ⚡ 短请求:动效展示满300ms;
- 🕒 长请求:3s后切换简化动效+超时提示;
- ❌ 失败/超时:显示错误提示,重试按钮可重新请求;
- 🚫 取消请求:动效立即隐藏,状态重置。
四、Day3关键总结(必记)📌
- 🎯 核心目标:解决「动效延迟」(预加载)和「时序混乱」(智能调度)两大痛点,提升跨端动效的流畅性和用户感知;
- 📦 预加载核心:提前加载动效图片/组件,时机优先选择“应用启动前”或“页面显示前”,避免重复加载和启动缓慢;
- ⚙️ 时序调度核心:记住3条统一规则(最短300ms、最长3s、超时处理),双端逻辑一致,仅API不同,集成到状态管理中;
- 🔗 代码关联:所有实操均承接Day1 Loading组件、Day2状态管理逻辑,体现“增量开发”思路,贴合真实项目;
- ⚠️ 避坑重点:资源路径配置、预加载时机、定时器取消(避免内存泄漏),务必添加异常处理;
- 🛠️ 与Day4关联:Day3的“简化动效”是Day4“低性能设备降级”的核心铺垫,低性能设备可默认使用简化动效减少消耗。
📖 补充说明(新手友好)
- 🧩 代码复用:所有代码可直接复制,仅需修改图片路径、接口地址即可适配自己的项目;
- 📌 版本兼容:适配Flutter 3.22+、鸿蒙API 9+,避免版本兼容问题;
- 🚨 报错解决:若遇预加载失败、时序异常,可查看日志定位问题(路径错误/定时器未取消是高频问题);
- 📱 双端选择:纯Flutter/鸿蒙开发者可忽略另一端内容,不影响单独学习;
- 📈 性能优化:预加载减少运行时IO消耗,简化动效降低CPU/GPU占用,兼顾体验与性能。
🚀 明日预告(Day18)
Day4将聚焦系列教程最后一个核心痛点——「低性能设备适配」(企业开发必做):
- 📊 性能检测:Flutter/鸿蒙端实现低性能设备检测工具,自动识别设备性能等级;
- 🎨 动效降级:根据设备性能,自动切换“完整动效/简化动效/无动效”;
- 🛡️ 稳定性保障:解决低性能设备动效卡顿、闪退问题,实现“一套代码适配全设备”。
所有方案均基于前3天的代码逻辑扩展,完成跨端动效全流程优化闭环!
更多推荐




所有评论(0)