【Flutter/鸿蒙跨端开发】动效与数据请求联动+低性能适配实战(Day3):资源预加载+智能时序调度

🔍 摘要

本文为Flutter/鸿蒙跨端动效开发系列教程Day3核心内容,针对Day1/Day2未解决的动效延迟(触发后空白期)和时序混乱(短请求一闪而过、长请求无反馈)两大高频体验痛点,实现双端统一的优化方案:

  1. 动效资源预加载:通过提前加载图片、初始化动效组件,解决动效触发后100-300ms空白期、首次渲染卡顿问题;
  2. 智能时序调度:制定「最短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操作+组件初始化耗时导致空白期。
核心方案:提前加载资源,在应用启动/页面显示前完成初始化,请求触发时直接复用。

📜 核心预加载资源清单(双端统一)

  1. 图片资源:骨架屏图片、加载指示器背景图、错误动效图片;
  2. 动效组件:提前初始化复杂加载动效(Flutter SpinKit/鸿蒙LoadingProgress),避免首次渲染卡顿;
  3. 配置资源:动效时序配置、颜色配置,提前解析减少运行时计算。

(一)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种方式验证预加载是否生效,新手必做:

  1. 🎯 视觉验证:首次点击“发起请求”,Loading动效应立即显示,无任何空白期(预加载前有100-300ms空白);
  2. 📝 日志验证:运行项目,查看控制台是否打印“✅ Flutter动效图片预加载完成”,且在动效触发前执行;
  3. 📈 性能验证:打开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:验证预加载效果(鸿蒙端必做)
  1. 🎯 视觉验证:首次触发动效无空白期,立即显示;
  2. 📝 日志验证:DevEco Studio终端打印“✅ 鸿蒙动效图片预加载完成”;
  3. 📈 性能验证:打开Profiler工具,查看CPU使用率,首次渲染动效时无明显飙升。

⚠️ 预加载避坑点(新手必看)

  1. 📁 资源路径错误:Flutter图片需在pubspec.yaml配置,鸿蒙图片需放在media目录,否则预加载失败;
  2. 🕒 时机不当:避免在build(Flutter)/build(鸿蒙)方法中执行预加载(多次执行导致重复加载);
  3. 📦 资源过多:资源多优先选择“页面级预加载”,避免应用启动缓慢;
  4. 🛡️ 异常处理:必须添加try-catch,避免预加载失败导致应用崩溃(上述代码已包含)。

三、智能时序调度(双端实战,解决时序混乱)⚙️

解决动效延迟后,针对「短请求一闪而过、长请求无反馈」问题,制定统一时序规则,集成到Day2的状态管理中。

📜 智能时序调度核心规则(双端通用,必记)

规则类型 数值 具体逻辑 用户体验目标
最短展示时长 300ms 即使请求耗时<300ms,动效也展示满300ms 避免一闪而过,让用户感知“正在加载”
最长展示时长 3s 请求耗时>3s,切换为简化动效+进度提示 减少用户焦虑,提供等待反馈
异常处理 3s超时 超时/失败触发错误动效+重试按钮;取消请求立即终止动效 引导用户操作,避免无意义等待

🚦 时序调度核心逻辑(双端统一)

  1. 🎬 请求开始:启动计时器记录开始时间,同时启动3s超时定时器;
  2. ⏱️ 请求过程:<300ms则等待至300ms;>3s则切换简化动效+进度提示;
  3. 🎯 请求结束:终止所有定时器,根据结果更新动效状态;
  4. ❌ 请求取消:立即终止定时器和动效,重置状态。

(一)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种核心场景,确保时序规则生效:

  1. ⚡ 短请求(<300ms):动效展示满300ms后隐藏,不一闪而过;
  2. 🕒 长请求(>3s):3s后切换为简化动效,弹出“加载中…已耗时3s”提示;
  3. ❌ 超时/失败:显示简化动效+错误提示,点击“重试”可重新请求;
  4. 🚫 取消请求:动效立即隐藏,状态重置为初始值。

(二)鸿蒙端:时序调度集成(承接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端测试场景一致,逐一验证:

  1. ⚡ 短请求:动效展示满300ms;
  2. 🕒 长请求:3s后切换简化动效+超时提示;
  3. ❌ 失败/超时:显示错误提示,重试按钮可重新请求;
  4. 🚫 取消请求:动效立即隐藏,状态重置。

四、Day3关键总结(必记)📌

  1. 🎯 核心目标:解决「动效延迟」(预加载)和「时序混乱」(智能调度)两大痛点,提升跨端动效的流畅性和用户感知;
  2. 📦 预加载核心:提前加载动效图片/组件,时机优先选择“应用启动前”或“页面显示前”,避免重复加载和启动缓慢;
  3. ⚙️ 时序调度核心:记住3条统一规则(最短300ms、最长3s、超时处理),双端逻辑一致,仅API不同,集成到状态管理中;
  4. 🔗 代码关联:所有实操均承接Day1 Loading组件、Day2状态管理逻辑,体现“增量开发”思路,贴合真实项目;
  5. ⚠️ 避坑重点:资源路径配置、预加载时机、定时器取消(避免内存泄漏),务必添加异常处理;
  6. 🛠️ 与Day4关联:Day3的“简化动效”是Day4“低性能设备降级”的核心铺垫,低性能设备可默认使用简化动效减少消耗。

📖 补充说明(新手友好)

  1. 🧩 代码复用:所有代码可直接复制,仅需修改图片路径、接口地址即可适配自己的项目;
  2. 📌 版本兼容:适配Flutter 3.22+、鸿蒙API 9+,避免版本兼容问题;
  3. 🚨 报错解决:若遇预加载失败、时序异常,可查看日志定位问题(路径错误/定时器未取消是高频问题);
  4. 📱 双端选择:纯Flutter/鸿蒙开发者可忽略另一端内容,不影响单独学习;
  5. 📈 性能优化:预加载减少运行时IO消耗,简化动效降低CPU/GPU占用,兼顾体验与性能。

🚀 明日预告(Day18)

Day4将聚焦系列教程最后一个核心痛点——「低性能设备适配」(企业开发必做):

  1. 📊 性能检测:Flutter/鸿蒙端实现低性能设备检测工具,自动识别设备性能等级;
  2. 🎨 动效降级:根据设备性能,自动切换“完整动效/简化动效/无动效”;
  3. 🛡️ 稳定性保障:解决低性能设备动效卡顿、闪退问题,实现“一套代码适配全设备”。
    所有方案均基于前3天的代码逻辑扩展,完成跨端动效全流程优化闭环!
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐