You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter+Beamer混合标签导航遇GlobalKey重复及焦点错误

问题:Flutter Beamer 导航中返回键触发 GlobalKey 重复及焦点断言错误

我在Flutter中用Beamer实现了如下导航逻辑:部分路由页面显示底部标签栏,其余路由隐藏标签栏。正常跳转时功能正常,但点击Android返回键时,会抛出「Multiple widgets used the same GlobalKey」和焦点管理相关的断言错误。

错误日志片段

The following assertion was thrown during a scheduler callback:
Multiple widgets used the same GlobalKey.

The key [GlobalKey<NavigatorState>] was used by multiple widgets. The parents of those widgets were:
- _FocusMarker
- _FocusMarker
...
Another exception was thrown: Assertion failed: file:///.../focus_manager.dart:1442:12
!_debugLosingFocus.contains(node) is not true

问题根源

这种错误通常是因为动态切换显示/隐藏底部导航栏时,同一个GlobalKey被重复绑定到不同的Navigator(或依赖GlobalKey的组件)实例。当返回键触发路由回退时,Beamer重新构建页面树,之前的Navigator实例未被正确销毁,导致GlobalKey冲突;同时焦点管理器因组件树异常变化,触发焦点断言错误。

解决方案

采用Beamer官方推荐的嵌套BeamerDelegate + IndexedStack实现底部导航栏路由,分离带底部栏的路由和全屏路由,避免手动管理GlobalKey带来的冲突。

修复后的完整代码

import 'package:beamer/beamer.dart';
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerDelegate: BeamerDelegate(
        locationBuilder: BeamerLocationBuilder(
          beamLocations: [
            HomeLocation(),
            DetailsLocation(),
          ],
        ),
      ),
      routeInformationParser: BeamerParser(),
    );
  }
}

// 带底部导航栏的主路由组
class HomeLocation extends BeamLocation<BeamState> {
  @override
  List<String> get pathPatterns => ['/', '/tab1', '/tab2'];

  @override
  List<BeamPage> buildPages(BuildContext context, BeamState state) {
    final pages = [
      BeamPage(
        key: const ValueKey('home-shell'),
        child: const HomeShell(),
      ),
      if (state.uri.path == '/tab1')
        BeamPage(
          key: const ValueKey('tab1-page'),
          child: const Tab1Page(),
        ),
      if (state.uri.path == '/tab2')
        BeamPage(
          key: const ValueKey('tab2-page'),
          child: const Tab2Page(),
        ),
    ];
    return pages;
  }
}

// 无底部导航栏的详情路由组
class DetailsLocation extends BeamLocation<BeamState> {
  @override
  List<String> get pathPatterns => ['/details/:id'];

  @override
  List<BeamPage> buildPages(BuildContext context, BeamState state) {
    return [
      BeamPage(
        key: ValueKey('details-${state.pathParameters['id']}'),
        child: DetailsPage(id: state.pathParameters['id']!),
      ),
    ];
  }
}

// 底部导航栏容器(Shell)
class HomeShell extends StatefulWidget {
  const HomeShell({super.key});

  @override
  State<HomeShell> createState() => _HomeShellState();
}

class _HomeShellState extends State<HomeShell> {
  int _currentIndex = 0;
  final _beamers = [
    BeamerDelegate(
      initialPath: '/tab1',
      locationBuilder: SimpleLocationBuilder(
        routes: {'/tab1': (context, state) => const Tab1Page()},
      ),
    ),
    BeamerDelegate(
      initialPath: '/tab2',
      locationBuilder: SimpleLocationBuilder(
        routes: {'/tab2': (context, state) => const Tab2Page()},
      ),
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _currentIndex,
        children: [
          Beamer(routerDelegate: _beamers[0]),
          Beamer(routerDelegate: _beamers[1]),
        ],
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (index) {
          setState(() => _currentIndex = index);
          _beamers[index].beamToNamed(_beamers[index].initialPath!);
        },
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Tab 1'),
          BottomNavigationBarItem(icon: Icon(Icons.settings), label: 'Tab 2'),
        ],
      ),
    );
  }
}

// 测试页面
class Tab1Page extends StatelessWidget {
  const Tab1Page({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Tab 1')),
      body: Center(
        child: ElevatedButton(
          onPressed: () => context.beamToNamed('/details/1'),
          child: const Text('Go to Details'),
        ),
      ),
    );
  }
}

class Tab2Page extends StatelessWidget {
  const Tab2Page({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Tab 2')),
      body: Center(child: const Text('Tab 2 Content')),
    );
  }
}

class DetailsPage extends StatelessWidget {
  final String id;
  const DetailsPage({super.key, required this.id});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Details $id')),
      body: Center(child: Text('Details Content $id')),
    );
  }
}

关键修复点

  1. 分离路由组:将带底部栏的路由和全屏路由拆分为独立的BeamLocation,避免路由树混合导致的组件冲突。
  2. 独立导航状态:每个Tab使用独立的BeamerDelegate管理导航状态,返回键会优先处理当前Tab内的路由,再回退到上一级。
  3. 唯一组件Key:给每个BeamPage分配唯一的ValueKey,帮助Flutter正确识别组件身份,避免焦点管理错误。

内容的提问来源于stack exchange,提问作者andr2k

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 19:17:38