问题
Flutter 跳转二级页面时,转场动画(~460ms)与 API 请求的同步处理(JSON 解析 + setState + Widget build)共用 UI 线程。请求响应回来后,同步处理抢占 UI 线程,导致动画丢帧、卡顿。
根因:Dart 是单线程,动画渲染、网络数据处理、Widget build 全在同一条线程上排队。每帧只有 16ms 预算,处理逻辑占用超时就会丢帧。
作用
封装TransitionGuard+LoadingOverlay两个组件,解决两个问题:
| 组件 | 解决什么 |
|---|---|
TransitionGuard | 请求和动画同时跑,响应快就等动画结束再 setState,响应慢就直接处理 |
LoadingOverlay | 请求慢时遮罩盖住整屏,防止用户看到空白/错乱页面,数据渲染后淡出 |
时间线:
请求 200ms 回来 → 动画还在播 → 等 260ms → setState → 不卡帧 请求 800ms 回来 → 动画早结束 → 直接 setState → 不卡帧(动画已结束)使用方式
模式一:无遮罩
适合:请求快、数据不是 UI 必需、空白一瞬间可接受。
classDetailPageextendsStatefulWidget{constDetailPage({super.key});@overrideState<DetailPage>createState()=>_DetailPageState();}class_DetailPageStateextendsState<DetailPage>{String?_data;bool _loading=true;@overridevoidinitState(){super.initState();_fetch();}void_fetch()async{awaitTransitionGuard.run(future:_request(),onDone:(data){if(!mounted)return;setState((){_data=data;_loading=false;});},);}Future<String>_request()async{awaitFuture.delayed(constDuration(milliseconds:200));return'数据';}@overrideWidgetbuild(BuildContextcontext){returnScaffold(appBar:AppBar(title:constText('详情')),body:Center(child:_loading?constText('加载中...'):Text(_data!),),);}}模式二:有遮罩
适合:请求慢、数据是 UI 必需、不能让用户看到空白页面。
classDetailPageextendsStatefulWidget{constDetailPage({super.key});@overrideState<DetailPage>createState()=>_DetailPageState();}class_DetailPageStateextendsState<DetailPage>{String?_data;bool _loading=true;@overridevoidinitState(){super.initState();_fetch();}void_fetch()async{awaitTransitionGuard.run(future:_request(),onDone:(data){if(!mounted)return;setState((){_data=data;_loading=false;});},);}Future<String>_request()async{awaitFuture.delayed(constDuration(milliseconds:800));return'数据';}@overrideWidgetbuild(BuildContextcontext){returnLoadingOverlay(isLoading:_loading,showLoading:true,child:Scaffold(appBar:AppBar(title:constText('详情')),body:Center(child:_loading?constSizedBox.shrink():Text(_data!),),),);}}两种模式的代码差异
// 无遮罩returnScaffold(body:_loading?Text('加载中...'):Text(_data!),);// 有遮罩returnLoadingOverlay(// ← 多包这一层isLoading:_loading,showLoading:true,// ← 这个参数child:Scaffold(body:_loading?SizedBox.shrink():Text(_data!),),);其余代码完全一样:initState、_fetch、TransitionGuard.run、onDone、setState。
组件代码
import'package:flutter/material.dart';/// 转场动画防卡顿工具。////// 请求立即发出,响应回来后根据耗时判断是否需要等待:/// - 请求耗时 < 动画时长 → 等待剩余时间,让动画播完再处理;/// - 请求耗时 ≥ 动画时长 → 动画已结束,直接处理。classTransitionGuard{TransitionGuard._();staticFuture<void>run<T>({requiredFuture<T>future,requiredvoidFunction(Tdata)onDone,voidFunction(Objecterror)?onError,int animationDurationMs=460,})async{finalstopwatch=Stopwatch()..start();try{finalresult=awaitfuture;stopwatch.stop();finalremainingMs=animationDurationMs-stopwatch.elapsedMilliseconds;if(remainingMs>0){awaitFuture<void>.delayed(Duration(milliseconds:remainingMs));}onDone(result);}catch(error){if(onError!=null){onError(error);}else{rethrow;}}}}/// Loading 遮罩 Widget,带淡入淡出效果。////// 包在 Scaffold 外层,isLoading + showLoading 为 true 时/// 自动叠加半透明遮罩 + 转圈,盖住整屏(含导航栏)。classLoadingOverlayextendsStatelessWidget{finalbool isLoading;finalbool showLoading;finalWidgetchild;finalDurationfadeDuration;constLoadingOverlay({super.key,requiredthis.isLoading,this.showLoading=true,requiredthis.child,this.fadeDuration=constDuration(milliseconds:300),});@overrideWidgetbuild(BuildContextcontext){if(!showLoading)returnchild;returnStack(children:[child,Positioned.fill(child:AnimatedOpacity(opacity:isLoading?1.0:0.0,duration:fadeDuration,curve:Curves.easeOut,child:IgnorePointer(ignoring:!isLoading,child:ColoredBox(color:Colors.black54,child:constCenter(child:CircularProgressIndicator()),),),),),],);}}关键设计决策
| 决策 | 原因 |
|---|---|
用Stopwatch计时 | 比DateTime.now().difference()更精确 |
| 异常不额外等待 | 错误处理通常很轻量,尽快让用户感知更重要 |
遮罩在Stack内部 | 跟着页面滑进来,不影响转场动画 |
AnimatedOpacity淡出 | 避免遮罩瞬间消失的突兀感 |
IgnorePointer | 淡出期间透明遮罩不拦截点击 |
showLoading参数 | 同一套组件,一个参数切换两种模式 |
适用边界
| JSON 大小 | 建议 |
|---|---|
| < 10KB | 本方案足够,不卡 |
| 10-100KB | 本方案可用,动画结束后的 build 可能感知到轻微延迟 |
| > 100KB | 建议用compute()把 JSON 解析丢到 Isolate,本方案治标不治本 |
为什么 UniApp 没这个问题
UniApp 的渲染层和逻辑层在不同线程——动画在原生/渲染线程,JSON 解析在 JS 线程,互不影响。Flutter 把所有事情放在同一个 UI 线程,拿到完全自定义能力的同时,需要自己处理重任务不卡线程。