Flutter应用在FutureBuilder内添加动画后加载即冻结
问题原因
应用加载后直接冻结是几个逻辑错误叠加导致的:
FutureBuilder的future参数直接写在build方法里,传入了getMenuAndAccountInfo(pageLoader)的即时调用结果。只要组件触发重建(动画逐帧回调、setState调用、状态管理类更新都会触发重建),就会重新执行一遍异步拉取方法,请求完成后又会触发组件刷新,直接形成「重建→发请求→请求完成→重建」的无限死循环,占满UI线程导致应用无响应。- 动画触发逻辑顺序写反:按钮的
onPressed回调里先调用空的setState(() {}),之后才修改splashOpacity = 1。setState执行时会立刻读取当前的splashOpacity值(还是初始的0)标记刷新,后续修改splashOpacity不会触发新的重建,AnimatedOpacity根本拿不到新的透明度值,动画完全无法触发。 - 冗余代码问题:你混入了
TickerProviderStateMixin,初始化了splashController和splashAnimation但全程没用到,也没写dispose释放逻辑,会造成内存泄漏;另外声明的width、tabProperty、menuData变量从未使用,徒增build方法执行开销。
修复步骤
- 把
getMenuAndAccountInfo的调用提前到initState生命周期,存为状态变量再传给FutureBuilder,保证整个页面生命周期内异步请求只会执行一次,切断无限重建循环。 - 调整动画状态修改顺序:把
splashOpacity的赋值放到setState的回调内部,确保框架重建时能读取到最新的透明度值,正常触发动画。 - 清理所有未使用的冗余代码,如果不需要手动控制动画,直接删掉没用的Animation相关声明即可;如果要保留自定义动画控制器,记得补全dispose释放逻辑。
- 给半透明遮罩层加
IgnorePointer,避免遮罩显示时拦截下方页面的点击事件。
修复后可运行代码
class MainPageState extends State<MainPage> { GlobalKey imageKey = GlobalKey(); double splashOpacity = 0; // 提前存储future实例,避免重复触发请求 late Future<Map<String, dynamic>> pageLoadFuture; @override void initState() { super.initState(); // 初始化时发起一次数据请求,整个页面生命周期只执行一次 pageLoadFuture = getMenuAndAccountInfo(context.read<PageLoader>()); } @override Widget build(BuildContext context) { double height = MediaQuery.of(context).size.height; PageLoader pageLoader = context.watch<PageLoader>(); return WillPopScope( onWillPop: () async { bool login = false; await StorageManager.readData("loggedIn") .then((value) => login = value ?? false); if (login) { return false; } SystemNavigator.pop(); return false; }, child: FutureBuilder( future: pageLoadFuture, builder: ((context, snapshot) { // 数据加载中状态 if (!snapshot.hasData) { return Stack(children: [ SafeArea( child: Scaffold( body: pageLoader.currentPage, ), ), const ColoredBox( color: Color.fromARGB(100, 0, 0, 0), child: Center(child: CircularProgressIndicator()), ), ]); } // 数据加载完成状态 return SafeArea( child: RepaintBoundary( key: imageKey, child: Stack( children: [ Scaffold( body: pageLoader.currentPage, bottomNavigationBar: Container( height: height * 0.07, color: Colors.blue, child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( child: MaterialButton( padding: EdgeInsets.zero, splashColor: Colors.orange, highlightColor: Colors.transparent, onPressed: () { // 状态修改必须放在setState回调内 setState(() { splashOpacity = 1; }); }, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.bookmark, color: Colors.white, size: height * 0.035, ), Text( "Bookmark", style: TextStyle( color: Colors.white, fontSize: height * 0.02), ), ], )), ), Expanded( child: MaterialButton( padding: EdgeInsets.zero, splashColor: Colors.orange, highlightColor: Colors.transparent, onPressed: () { setState(() { splashOpacity = 1; }); }, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.account_circle_rounded, color: Colors.white, size: height * 0.035, ), Text( "Profile", style: TextStyle( color: Colors.white, fontSize: height * 0.02), ) ], )), ), ], ), ), ), Positioned.fill( // 加IgnorePointer避免遮罩层拦截下方组件点击事件 child: IgnorePointer( child: AnimatedOpacity( opacity: splashOpacity, duration: const Duration(milliseconds: 400), child: const ColoredBox( color: Color.fromARGB(155, 0, 0, 0), ), ), ), ) ], ), ), ); }), ), ); } }
补充说明:你之前测试AlertDialog内无法正常调用setState,本质是因为AlertDialog属于独立路由上下文,调用setState时没有指定正确的状态作用域才会失效,自定义遮罩的方案完全可行,只要避开FutureBuilder重复执行的问题就不会出现卡死。
内容的提问来源于stack exchange,提问作者bluends
相关产品推荐
相关产品推荐

