无需推送页面更新导航栈(以PageView为例)
问题描述
我有3个页面,示例代码如下:
List<Widget> pages = [ _Page1(), _Page2(), _Page3(), ];
我希望导航栈呈现为:
/page1 /page2 /page3
当处于第3页时,这3个页面都在同一个PageViewScreen中。我尝试了多种实现方式,但均导致UI变为白屏。是否有可行的实现方案?
注意:我不想使用嵌套导航,仅希望随PageView的更新同步更新导航栈。
日志信息
[log] 🚀 NAVIGATION: PUSHED traveler-details (PageRouteBuilder<dynamic>) (from booking (MaterialPageRoute<dynamic>)) [log] 📚 CURRENT STACK (5 routes): [log] 1. home (PageTransition<dynamic>) [log] 2. flight-search-result (MaterialPageRoute<dynamic>) [log] 3. review (MaterialPageRoute<dynamic>) [log] 4. booking (MaterialPageRoute<dynamic>) [log] 5. traveler-details (PageRouteBuilder<dynamic>) [log] Route updated: traveler-details (Virtual stack: [traveler-details])
其中booking是包含3个页面的PageView,第一个页面为TravellerDetails(),它已被推入栈中,但界面显示为白屏。
当前实现代码
BookingNavigationService
class BookingNavigationService { static final BookingNavigationService _instance = BookingNavigationService._internal(); /// 单例实例 factory BookingNavigationService({ bool flightAddon = false, bool flightPostAddon = false, }) { _instance.flightAddon = flightAddon; _instance.flightPostAddon = flightPostAddon; return _instance; } BookingNavigationService._internal(); bool flightAddon = false; bool flightPostAddon = false; /// 页面索引与路由名称的映射 final Map<int, String> _pageToRouteMap = { 0: AppRoutes.travelerDetailsPage, 1: AppRoutes.addOns, 2: AppRoutes.paymentPage, 3: AppRoutes.confirmation, }; /// 记录当前路由 String? _currentRoute; /// 记录虚拟路由栈 final List<String> _virtualRouteStack = []; /// 根据页面索引获取路由名称 String getRouteForPage(int pageIndex, {bool hasAddons = true}) { // 如果没有附加服务,调整索引2和3的路由映射 if (!hasAddons && pageIndex > 0) { // 跳过附加服务页面的路由映射 return _pageToRouteMap[pageIndex + 1] ?? AppRoutes.travelerDetailsPage; } return _pageToRouteMap[pageIndex] ?? AppRoutes.travelerDetailsPage; } bool _hasAddons(BuildContext context) { final bookingCubit = BookingCubit.cubit(context); final bookingState = bookingCubit.state; return /// 常规流程下的检查 (bookingState.bookingAddonsState?.data?.available == true) || (bookingState.postBookingAddonsState?.data?.available == true) || /// 从"我的预订"进入时的检查 flightAddon || flightPostAddon; } /// 更新导航栈中的路由 /// 该方法会将路由注册到实际的导航历史中 void updateRoute(BuildContext context, int pageIndex) { final bookingState = BookingStateProvider.state(context); // 检查上下文是否有效 if (!context.mounted) return; // 检查是否需要显示附加服务页面 final bool hasAddons = bookingState.isFlight && _hasAddons(context); final String newRoute = getRouteForPage(pageIndex, hasAddons: hasAddons); // 如果路由未变化,不更新 if (_currentRoute == newRoute) return; // 处理路由栈管理 if (_isNavigatingBack(pageIndex)) { // 向后导航,从虚拟栈中移除最后一个路由 if (_virtualRouteStack.isNotEmpty) { _virtualRouteStack.removeLast(); } // 如果导航器可以弹出,弹出一个路由 if (Navigator.of(context).canPop()) { Navigator.of(context).pop(); } } else { // 向前导航,添加到虚拟栈 _virtualRouteStack.add(newRoute); // 推送到实际导航器以注册到历史记录 _pushRouteGently(context, newRoute, pageIndex); } // 更新当前路由 _currentRoute = newRoute; // 打印路由变更日志用于调试 log('Route updated: $_currentRoute (Virtual stack: $_virtualRouteStack)'); } /// 平缓推送路由,在不干扰UI的情况下注册到导航历史 void _pushRouteGently(BuildContext context, String routeName, int pageIndex) { try { Navigator.of(context).pushNamed( routeName, arguments: { 'pageIndex': pageIndex, 'virtualRoute': true, // 标记为虚拟路由 }, ); } catch (e) { log('推送路由错误: $e'); // 如果推送失败,尝试替换路由 try { Navigator.of(context).pushReplacementNamed( routeName, arguments: { 'pageIndex': pageIndex, 'virtualRoute': true, }, ); } catch (e) { log('替换路由错误: $e'); } } } /// 根据页面索引判断是否为向后导航 bool _isNavigatingBack(int newPageIndex) { if (_virtualRouteStack.isEmpty) return false; // 从路由映射中获取当前页面索引 int? currentPageIndex; _pageToRouteMap.forEach((index, route) { if (route == _currentRoute) currentPageIndex = index; }); // 如果找到当前页面索引,检查是否向后导航 return currentPageIndex != null && newPageIndex < currentPageIndex!; } /// 可选:添加路由变更监听系统 final List<Function(String route, int pageIndex)> _routeChangeListeners = []; /// 添加路由变更监听器 void addRouteChangeListener(Function(String route, int pageIndex) listener) { _routeChangeListeners.add(listener); } /// 移除路由变更监听器 void removeRouteChangeListener( Function(String route, int pageIndex) listener) { _routeChangeListeners.remove(listener); } /// 通知监听器路由变更 void _notifyRouteChanged(String route, int pageIndex) { for (var listener in _routeChangeListeners) { listener(route, pageIndex); } } /// 导航到预订流程中的指定页面 void navigateToBookingPage(BuildContext context, int pageIndex) { final bookingState = BookingStateProvider.state(context); if (!context.mounted) return; // 先更新物理PageView bookingState.navigateTo(pageIndex); // 然后更新导航栈中的路由 updateRoute(context, pageIndex); } /// 获取虚拟路由历史 List<String> get routeHistory => List.unmodifiable(_virtualRouteStack); /// 获取当前路由 String? get currentRoute => _currentRoute; /// 处理返回导航 bool handleBackNavigation(BuildContext context, int currentPage) { final bookingState = BookingStateProvider.state(context); // 如果在第一页,执行默认返回导航 if (currentPage <= 0) { return true; } // 导航到上一页 final previousPage = currentPage - 1; bookingState.navigateTo(previousPage); // 更新虚拟路由 updateRoute(context, previousPage); // 已处理导航,返回false阻止默认返回行为 return false; } /// 清空路由栈 void resetRouteStack() { _virtualRouteStack.clear(); _currentRoute = null; } }
PageView(简化代码)
@override Widget build(BuildContext context) { final bookingCubit = BookingCubit.cubit(context); final validateFlightCubitState = ValidateWebReferenceCubit.c(context).state.validateBookingV2; final state = BookingStateProvider.state(context); final modalState = BookingModalState.state(context); final bookingAddonProvider = BookingAddOnsProvider.state(context); final steps = _getBookingSteps(state, bookingAddonProvider); if (state.isFlight) { _initializeFlightBookingDetails(bookingCubit, validateFlightCubitState); } return PopScope( canPop: false, onPopInvokedWithResult: (intent, result) { if (intent) return; _handleOnBackPressed(state, modalState, bookingCubit); }, child: GestureDetector( onTap: () => FocusScope.of(context).unfocus(), child: ScreenLayout( title: _buildScreenTitle(steps), onBackPressed: () => _handleOnBackPressed(state, modalState, bookingCubit), scaffoldBackgroundColor: AppTheme.c.background4, overlayBuilders: _getOverlayBuilders(), body: SafeArea( bottom: false, child: Column( children: [ Expanded( child: BlocConsumer<BookingCubit, BookingState>( listenWhen: (previous, current) => _shouldRebuildAddonState(previous, current), listener: (context, state) => _handleAddonStateChanges(state), buildWhen: (previous, current) => _shouldRebuildAddonState(previous, current), builder: (context, bookingState) { if (!hasHome && _isLoadingAddons(bookingState)) { return _buildLoadingSkeleton(); } final pageChildren = _buildPageChildren( bookingState, bookingAddonProvider, bookingCubit); if (_shouldDisplaySkeleton(state, pageChildren)) { return _buildLoadingSkeleton(); } return PageView( physics: const NeverScrollableScrollPhysics(), onPageChanged: (i) { state.setActivePage(i, context); _navigationService.updateRoute( context, i, ); }, controller: state.controller, children: pageChildren, ); }, ), ), ], ), ), ), ), ); }
Provider(ChangeNotifier控制)
PageController? controller; // 其他代码 void init() { controller = PageController( initialPage: activePage, keepPage: true, ); // 其他代码 void navigateTo(int page) { if (page == 1) { notifyListeners(); } controller!.animateToPage( page, duration: UIProps.duration, curve: Curves.ease, ); } void nextPage() { controller!.nextPage( duration: UIProps.duration2, curve: Curves.ease, ); } void prevPage() { controller!.previousPage( duration: UIProps.duration2, curve: Curves.ease, ); } }
解决方案
问题根源
白屏的核心原因是:你通过pushNamed跳转的traveler-details等路由对应的是单独的页面组件(如TravellerDetails),而非包含PageView的BookingPage。这导致导航栈中新增了一个独立页面,该页面缺少预订流程所需的状态依赖(如BookingCubit、各类Provider),因此无法正常渲染,最终显示白屏。
实现方案
要实现PageView切换时同步更新导航栈,同时避免白屏,需让所有"虚拟路由"都指向同一个包含PageView的BookingPage,并通过路由参数控制PageView的当前索引。
1. 调整路由配置
确保所有相关路由都指向BookingPage,并支持传递pageIndex参数:
MaterialApp( routes: { // 基础预订页面路由 AppRoutes.bookingPage: (context) => const BookingPage(), // 虚拟路由:旅客详情页 AppRoutes.travelerDetailsPage: (context) { final args = ModalRoute.of(context)?.settings.arguments as Map?; return BookingPage(initialPage: args?['pageIndex'] ?? 0); }, // 虚拟路由:附加服务页 AppRoutes.addOns: (context) { final args = ModalRoute.of(context)?.settings.arguments as Map?; return BookingPage(initialPage: args?['pageIndex'] ?? 1); }, // 虚拟路由:支付页 AppRoutes.paymentPage: (context) { final args = ModalRoute.of(context)?.settings.arguments as Map?; return BookingPage(initialPage: args?['pageIndex'] ?? 2); }, // 其他路由... }, );
2. 修改BookingPage初始化逻辑
在BookingPage中接收初始索引,并初始化PageController到对应页面:
class BookingPage extends StatefulWidget { final int initialPage; const BookingPage({super.key, this.initialPage = 0}); @override State<BookingPage> createState() => _BookingPageState(); } class _BookingPageState extends State<BookingPage> { late BookingStateProvider _stateProvider; @override void initState() { super.initState(); _stateProvider = BookingStateProvider.state(context); // 初始化PageController到指定页面 _stateProvider.controller = PageController( initialPage: widget.initialPage, keepPage: true, ); // 更新活跃页面索引 _stateProvider.setActivePage(widget.initialPage, context); } // 原build方法保持不变... }
3. 优化导航服务的路由推送逻辑
避免重复创建BookingPage实例,确保导航栈更新时页面状态一致:
void _pushRouteGently(BuildContext context, String routeName, int pageIndex) { try { // 使用pushNamed保持导航栈层级,确保返回时能正确回退 Navigator.of(context).pushNamed( routeName, arguments: { 'pageIndex': pageIndex, 'virtualRoute': true, }, ); } catch (e) { log('推送路由错误: $e'); // 推送失败时使用替换路由,避免栈冗余 Navigator.of(context).pushReplacementNamed( routeName, arguments: { 'pageIndex': pageIndex, 'virtualRoute': true, }, ); } }
4. 完善返回按钮逻辑
确保物理返回键和页面返回按钮都能同步PageView与导航栈:
// 在BookingPage的PopScope组件中 PopScope( canPop: false, onPopInvokedWithResult: (intent, result) { if (intent) return; final currentPage = _stateProvider.activePage; if (currentPage <= 0) { // 处于第一页时,返回上一个路由 Navigator.of(context).pop(); } else { // 切换到上一页并更新导航栈 _stateProvider.prevPage(); _navigationService.updateRoute(context, currentPage - 1); } }, // 其他代码... )
关键注意事项
- 状态共享:所有虚拟路由必须指向同一个
BookingPage,确保预订流程的状态(Cubit、Provider)在导航栈中共享,避免因状态缺失导致白屏。 - PageView状态保持:利用
PageController的keepPage属性,确保切换页面时不会重新创建页面组件,提升性能并保持用户输入状态。 - 导航栈测试:测试导航栈的层级,确保点击返回按钮时能正确回退到对应的PageView页面,符合用户预期。
内容的提问来源于stack exchange,提问作者Hamza
相关产品推荐
相关产品推荐

