使用go_router包实现启动页的正确方式及报错修复
我希望用户打开应用时显示启动页(SplashScreen),同时加载数据,加载完成后根据数据跳转至对应页面。当前用go_router包实现了功能,但出现「Looking up a deactivated widget's ancestor is unsafe」错误,附上代码及错误日志,求问题原因与修复方法。
代码示例
import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { MyApp({Key? key}) : super(key: key); final GoRouter _router = GoRouter( initialLocation: '/', routes: <RouteBase>[ GoRoute( path: '/', builder: (context, state) => const SplashScreen(), ), GoRoute( path: '/a', builder: (context, state) => const ScreenA(), ) ], ); @override Widget build(BuildContext context) => MaterialApp.router(routerConfig: _router); } class SplashScreen extends StatelessWidget { const SplashScreen({super.key}); @override Widget build(BuildContext context) { Future.delayed(const Duration(seconds: 1)) .then((value) => GoRouter.of(context).go('/a')); // !!! return const Center(child: FlutterLogo()); } } class ScreenA extends StatelessWidget { const ScreenA({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return const Scaffold( body: Center( child: Text('Screen A'), ), ); } }
错误日志
Error: Looking up a deactivated widget's ancestor is unsafe. C:/b/s/w/ir/cache/builder/src/out/host_debug/dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dar t 266:49 throw_ packages/flutter/src/widgets/framework.dart 4241:9 <fn> packages/flutter/src/widgets/framework.dart 4254:14 [_debugCheckStateIsActiveForAncestorLookup] C:/b/s/w/ir/cache/builder/src/out/host_debug/dart-sdk/lib/async/future_impl.dart 557:7 [_complete] C:/b/s/w/ir/cache/builder/src/out/host_debug/dart-sdk/lib/async/future.dart 421:15 <fn> C:/b/s/w/ir/cache/builder/src/out/host_debug/dart-sdk/lib/_internal/js_dev_runtime/private/isolate_helper.dart 48:19 internalCallback
问题原因
问题出在SplashScreen的build方法里:当Future.delayed延迟1秒执行跳转时,原SplashScreen组件可能已经被Flutter标记为失活状态(deactivated)。因为路由跳转后,Flutter会重建widget树,旧的SplashScreen实例会被销毁或从widget树中移除,此时再通过GoRouter.of(context)获取路由,相当于尝试访问一个已失效widget的上下文,触发了Flutter的安全检查机制,从而抛出错误。
另外,build方法可能会被Flutter多次调用(比如屏幕旋转、父组件重建),把异步跳转放在build里还会导致重复触发跳转的问题。
修复方法
方案1:改用StatefulWidget+mounted检查
把SplashScreen改成StatefulWidget,将异步操作放在initState(只会执行一次)里,并在跳转前检查组件是否还挂载在widget树上:
class SplashScreen extends StatefulWidget { const SplashScreen({super.key}); @override State<SplashScreen> createState() => _SplashScreenState(); } class _SplashScreenState extends State<SplashScreen> { @override void initState() { super.initState(); // 异步操作放在initState,避免重复触发 Future.delayed(const Duration(seconds: 1)).then((_) { if (mounted) { // 检查组件是否仍处于活跃状态 GoRouter.of(context).go('/a'); } }); } @override Widget build(BuildContext context) { return const Center(child: FlutterLogo()); } }
方案2:直接传入GoRouter实例跳转
在MyApp中已经创建了_router实例,可以直接把它传递给SplashScreen,这样跳转时不需要依赖上下文,自然避免上下文失效问题:
class MyApp extends StatelessWidget { MyApp({Key? key}) : super(key: key); final GoRouter _router = GoRouter( initialLocation: '/', routes: <RouteBase>[ GoRoute( path: '/', builder: (context, state) => SplashScreen(router: _router), // 传递路由实例 ), GoRoute( path: '/a', builder: (context, state) => const ScreenA(), ) ], ); @override Widget build(BuildContext context) => MaterialApp.router(routerConfig: _router); } class SplashScreen extends StatelessWidget { const SplashScreen({super.key, required this.router}); final GoRouter router; @override Widget build(BuildContext context) { Future.delayed(const Duration(seconds: 1)) .then((value) => router.go('/a')); // 使用传入的路由实例跳转 return const Center(child: FlutterLogo()); } }
方案3:用FutureBuilder处理数据加载(推荐)
如果实际场景是加载数据,用FutureBuilder更符合Flutter的状态管理逻辑,同时能避免上下文失效:
class SplashScreen extends StatelessWidget { const SplashScreen({super.key}); // 模拟实际的数据加载逻辑 Future<void> _loadAppData() async { await Future.delayed(const Duration(seconds: 1)); // 这里可以添加数据解析、初始化等操作 } @override Widget build(BuildContext context) { return FutureBuilder( future: _loadAppData(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { // 加载完成后,在下一帧执行跳转 WidgetsBinding.instance.addPostFrameCallback((_) { GoRouter.of(context).go('/a'); }); } return const Center(child: FlutterLogo()); }, ); } }
内容的提问来源于stack exchange,提问作者Damien

