nav_bar_config.dart 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:go_router/go_router.dart';
  4. /// 按 BuildContext 缓存 token:同 State 多次 build 复用同一 token,
  5. /// 新 State(context 对象不同)自动分配新 token,确保 onBack 闭包不被旧 State 覆盖
  6. final Expando<int> _contextToken = Expando<int>();
  7. int _globalNavToken = 0;
  8. int _tokenForContext(BuildContext context) =>
  9. _contextToken[context] ??= ++_globalNavToken;
  10. /// NavBar 配置,由各页面在 build 中设置,AppShell 统一渲染
  11. class NavBarConfig {
  12. final String title;
  13. final bool showBack;
  14. final bool showRight;
  15. final Widget? rightWidget;
  16. final VoidCallback? onBack;
  17. final IconData? leadingIcon;
  18. final bool hasFilter;
  19. final int filterVersion;
  20. /// 页面实例标识:每个 State 实例生成唯一 token,确保新 State 的 onBack 闭包不被
  21. /// 旧 State 的 config(值相等但闭包指向已销毁 State)覆盖
  22. final int? token;
  23. const NavBarConfig({
  24. required this.title,
  25. this.showBack = false,
  26. this.showRight = false,
  27. this.rightWidget,
  28. this.onBack,
  29. this.leadingIcon,
  30. this.hasFilter = false,
  31. this.filterVersion = 0,
  32. this.token,
  33. });
  34. /// 根页面默认配置(无返回按钮)
  35. static const home = NavBarConfig(title: 'TBOSS 工作台', showBack: false);
  36. static const messages = NavBarConfig(title: '消息', showBack: false);
  37. static const profile = NavBarConfig(title: '我的', showBack: false);
  38. /// 带返回按钮的页面快捷构造
  39. factory NavBarConfig.withBack(String title, {VoidCallback? onBack}) {
  40. return NavBarConfig(title: title, showBack: true, onBack: onBack);
  41. }
  42. /// 带返回按钮 + 右侧按钮的页面快捷构造
  43. factory NavBarConfig.withRight(
  44. String title, {
  45. required Widget rightWidget,
  46. VoidCallback? onBack,
  47. }) {
  48. return NavBarConfig(
  49. title: title,
  50. showBack: true,
  51. showRight: true,
  52. rightWidget: rightWidget,
  53. onBack: onBack,
  54. );
  55. }
  56. @override
  57. bool operator ==(Object other) =>
  58. other is NavBarConfig &&
  59. other.title == title &&
  60. other.showBack == showBack &&
  61. other.showRight == showRight &&
  62. other.hasFilter == hasFilter &&
  63. other.filterVersion == filterVersion &&
  64. other.token == token;
  65. // token 确保不同 State 实例的 config 不相等,onBack/rightWidget 通过 token 间接区分
  66. @override
  67. int get hashCode =>
  68. Object.hash(title, showBack, showRight, hasFilter, filterVersion, token);
  69. }
  70. /// NavBar 配置变更器,内部做相等判断避免无限重建
  71. ///
  72. /// 通过 [Future.microtask] 将状态更新推迟到事件循环下一个 tick,
  73. /// 避免子页面在 build 中更新 provider 时与 AppShell watch 冲突。
  74. class NavBarConfigNotifier extends StateNotifier<NavBarConfig> {
  75. NavBarConfigNotifier() : super(NavBarConfig.home);
  76. void update(NavBarConfig config) {
  77. if (state != config) {
  78. Future.microtask(() {
  79. if (mounted) state = config;
  80. });
  81. }
  82. }
  83. }
  84. /// NavBar 配置的 Provider —— 各页面在 build 中更新,AppShell 统一消费
  85. final navBarConfigProvider =
  86. StateNotifierProvider<NavBarConfigNotifier, NavBarConfig>(
  87. (ref) => NavBarConfigNotifier(),
  88. );
  89. /// 更新导航栏配置。使用 GoRouter 判断当前页面是否为可见路由:
  90. /// - 页面自身的 GoRouterState.uri == GoRouter 注册的当前 uri → 允许更新
  91. /// - 不匹配 → 被覆盖的旧页面(GoRouter 级联重建导致),跳过
  92. ///
  93. /// 此方法替代了 ModalRoute.isCurrent,因为缓存引擎下旧路由残留会导致
  94. /// ModalRoute 误判;GoRouter 的状态不受 Widget 树生命周期影响,结果更可靠。
  95. void setNavBarTitle(BuildContext context, WidgetRef ref, NavBarConfig config) {
  96. try {
  97. final pageUri = GoRouterState.of(context).uri.toString();
  98. final currentUri = GoRouter.of(context).state.uri.toString();
  99. if (pageUri != currentUri) return;
  100. } catch (_) {
  101. // GoRouter 不可用时直接放行
  102. }
  103. // 注入 context 绑定的 token:同 State 多次 build 共享,换 State 自动换新 token
  104. final token = _tokenForContext(context);
  105. final effective = config.token != null ? config : NavBarConfig(
  106. title: config.title,
  107. showBack: config.showBack,
  108. showRight: config.showRight,
  109. rightWidget: config.rightWidget,
  110. onBack: config.onBack,
  111. leadingIcon: config.leadingIcon,
  112. hasFilter: config.hasFilter,
  113. filterVersion: config.filterVersion,
  114. token: token,
  115. );
  116. ref.read(navBarConfigProvider.notifier).update(effective);
  117. }