重写BeamLocation的builder方法引发Provider相关状态错误
问题核心
你用嵌套Beamer+IndexedStack实现BottomNavbar标签状态保留,想给单个BeamLocation下的页面共享状态,于是用ChangeNotifierProvider包裹BeamLocation,但切换到其他BeamLocation时,被包裹的Location仍会触发页面构建,导致Provider销毁后引发错误。根源在于:
- 当BeamLocation的
builder直接返回传入的navigator时,Beamer内部会做优化,非活跃Location不会触发额外构建; - 一旦
builder返回嵌套了其他Widget(比如Container)的结构,即使Location不活跃,Beamer仍会调用该方法生成新Widget,进而触发navigator及内部页面的构建,引发Provider相关错误。
解决方案
方案1:将Provider放在BeamLocation的页面层级(推荐)
不要在BeamLocation的builder里包裹Provider,而是给该Location的所有页面创建一个根页面,把Provider放在这个根页面中,确保只有Location活跃时才会构建Provider:
class SharedStateLocation extends BeamLocation { @override List<BeamPage> buildPages(BuildContext context, BeamState state) { // 确定当前活跃的子页面索引 final activeIndex = state.pathSegments.isEmpty ? 0 : int.parse(state.pathSegments.first); return [ BeamPage( key: const ValueKey('shared-state-root'), child: ChangeNotifierProvider( create: (_) => SharedPageState(), // 共享状态 child: Scaffold( body: IndexedStack( index: activeIndex, children: const [ PageA(), PageB(), PageC(), ], ), ), ), ), ]; } @override Set<Pattern> get pathPatterns => {'/', '/:pageIndex'}; }
这样,只有当该BeamLocation处于活跃状态时,根BeamPage才会被加入Widget树,Provider也会随之创建;切换到其他Location后,根页面被移除,Provider会被正确销毁,不会出现多余构建。
方案2:在BeamLocation的builder中添加活跃判断
如果必须在BeamLocation的builder里包裹Provider,可以通过判断当前活跃的Location是否为当前实例,仅在活跃时构建带Provider的导航器:
class SharedStateLocation extends BeamLocation { @override Widget builder(BuildContext context, Widget navigator) { final currentActiveLocation = Beamer.of(context).currentBeamLocation; // 仅当当前活跃的是本Location时,才构建Provider和导航器 if (currentActiveLocation == this) { return ChangeNotifierProvider( create: (_) => SharedPageState(), child: navigator, ); } // 非活跃时返回空Widget,避免不必要的构建 return const SizedBox.shrink(); } @override List<BeamPage> buildPages(BuildContext context, BeamState state) { // 页面构建逻辑... } @override Set<Pattern> get pathPatterns => {/* 路径规则 */}; }
方案3:通过BeamerDelegate的builder全局控制
在主BeamerDelegate的builder中,根据当前活跃的BeamLocation类型,动态决定是否包裹Provider:
final mainBeamerDelegate = BeamerDelegate( locationBuilder: (context) { // 根据路由返回对应BeamLocation final route = Beamer.of(context).currentRouteInformation; if (route.location?.startsWith('/shared') ?? false) { return SharedStateLocation(); } return OtherLocation(); }, builder: (context, navigator) { final currentLocation = Beamer.of(context).currentBeamLocation; if (currentLocation is SharedStateLocation) { return ChangeNotifierProvider( create: (_) => SharedPageState(), child: navigator, ); } return navigator; }, );
关键原理
Beamer对builder直接返回navigator的情况做了优化,会复用已有导航器实例且避免非活跃Location的构建;而嵌套Widget会导致每次调用builder都生成新的Widget树,触发非活跃Location页面的不必要构建,进而引发Provider销毁后的错误。通过上述方案,可确保Provider仅在对应Location活跃时存在,切换后正确销毁。
内容的提问来源于stack exchange,提问作者jefrww

