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

无需推送页面更新导航栈(以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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 03:08:10