Flutter进阶:局部嵌套导航实现 Navigator

一、需求来源

iOS原生是支持局部嵌套导航实现的(半屏导航),就想在flutter中实现同样功能,今天灵光一闪,实现分享给大家。

二、使用示例

三、源码

less 复制代码
//
//  NestedNavigatorDemo.dart
//  flutter_templet_project
//
//  Created by shang on 2024/9/27 16:14.
//  Copyright © 2024/9/27 shang. All rights reserved.
//

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter_templet_project/extension/build_context_ext.dart';
import 'package:flutter_templet_project/extension/ddlog.dart';
import 'package:flutter_templet_project/pages/demo/CupertinoTabScaffoldDemo.dart';
import 'package:flutter_templet_project/routes/APPRouter.dart';
import 'package:get/get.dart';

class NestedNavigatorDemo extends StatefulWidget {
  const NestedNavigatorDemo({
    super.key,
    this.arguments,
  });

  final Map<String, dynamic>? arguments;

  @override
  State<NestedNavigatorDemo> createState() => _NestedNavigatorDemoState();
}

class _NestedNavigatorDemoState extends State<NestedNavigatorDemo> {

  final _scrollController = ScrollController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("$widget"),
      ),
      body: buildBody(),
    );
  }

  Widget buildBody() {
    return Scrollbar(
      controller: _scrollController,
      child: SingleChildScrollView(
        controller: _scrollController,
        child: Container(
          padding: EdgeInsets.all(10),
          child: Column(
            // crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              buildNavigatorBox(),
            ],
          ),
        ),
      ),
    );
  }

  Widget buildNavigatorBox() {
    onNext({required BuildContext context, required String title}) {
      Navigator.push(
        context,
        MaterialPageRoute(
          builder: (context) {
            return NestedNavigatorSubpage(
              appBar: AppBar(
                centerTitle: true,
                title: Text(title),
                actions: [
                  GestureDetector(
                    onTap: () {
                      DLog.d("error");
                    },
                    child: Icon(Icons.error_outline),
                  ),
                ]
                    .map((e) => Container(
                          padding: EdgeInsets.only(right: 8),
                          child: e,
                        ))
                    .toList(),
              ),
              child: Column(
                children: [
                  ElevatedButton(
                    onPressed: () {
                      onNext(context: context, title: title);
                    },
                    child: Text('next page'),
                  ),
                  ElevatedButton(
                    onPressed: () {
                      Navigator.pop(context);
                    },
                    child: Text('Go back'),
                  ),
                  Wrap(
                    spacing: 8,
                    runSpacing: 8,
                    children: [
                      ...List.generate(Random().nextInt(9), (index) {
                        final title = "选项_$index";

                        return OutlinedButton(
                          style: TextButton.styleFrom(
                            padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                            tapTargetSize: MaterialTapTargetSize.shrinkWrap,
                            minimumSize: Size(50, 18),
                            // primary: primary,
                          ),
                          onPressed: () {
                            DLog.d(title);
                          },
                          child: Text(title),
                        );
                      }),
                    ],
                  ),
                ],
              ),
            );
          },
        ),
      );
    }

    return Container(
      height: 400,
      child: Theme(
        data: ThemeData(
          appBarTheme: const AppBarTheme(
            backgroundColor: Colors.lightBlueAccent,
            elevation: 0,
            scrolledUnderElevation: 0,
            titleTextStyle: TextStyle(
              fontSize: 18,
              fontWeight: FontWeight.w500,
            ),
            toolbarTextStyle: TextStyle(
              fontSize: 16,
              fontWeight: FontWeight.w400,
            ),
            iconTheme: IconThemeData(
              color: Colors.white,
              size: 24.0,
              opacity: 0.8,
            ),
            actionsIconTheme: IconThemeData(
              color: Colors.white,
              size: 24.0,
              opacity: 0.8,
            ),
          ),
        ),
        child: Navigator(
          onGenerateRoute: (RouteSettings settings) {
            return MaterialPageRoute(
              builder: (context) {
                return NestedNavigatorSubpage(
                  appBar: AppBar(
                    centerTitle: true,
                    title: Text("嵌套导航主页面"),
                  ),
                  child: Column(
                    children: [
                      ElevatedButton(
                        onPressed: () {
                          onNext(context: context, title: "子页面");
                        },
                        child: Text('next page'),
                      )
                    ],
                  ),
                );
              },
            );
          },
        ),
      ),
    );
  }
}

/// 嵌套导航子视图
class NestedNavigatorSubpage extends StatefulWidget {
  const NestedNavigatorSubpage({
    super.key,
    this.appBar,
    required this.child,
  });

  final AppBar? appBar;

  final Widget child;

  @override
  State<NestedNavigatorSubpage> createState() => _NestedNavigatorSubpageState();
}

class _NestedNavigatorSubpageState extends State<NestedNavigatorSubpage> {
  final scrollController = ScrollController();

  @override
  void didUpdateWidget(covariant NestedNavigatorSubpage oldWidget) {
    super.didUpdateWidget(oldWidget);

    if (oldWidget.appBar != widget.appBar || oldWidget.child != widget.child) {
      setState(() {});
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: widget.appBar ??
          AppBar(
            title: Text("$widget"),
          ),
      body: buildBody(),
    );
  }

  Widget buildBody() {
    return Container(
      alignment: Alignment.center,
      decoration: BoxDecoration(
        // color: color,
        border: Border.all(color: Colors.blue),
      ),
      child: Column(
        children: [
          // NPickerToolBar(
          //   title: title,
          //   onCancel: onBack,
          //   onConfirm: onNext,
          // ),
          Expanded(
            child: Scrollbar(
              child: SingleChildScrollView(
                child: widget.child,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

Navigator 是 Flutter 中管理页面堆栈和路由的核心组件,它允许在应用中进行页面导航(推送、弹出、替换等)。Navigator通过一个栈来管理活动路由集合。通常当前屏幕显示的页面就是栈顶的路由。最核心的方法就是入栈和出栈:

1)Future push(BuildContext context, Route route)

将给定的路由入栈(即打开新的页面),返回值是一个Future对象,用以接收新路由出栈(即关闭)时的返回数据。

2)bool pop(BuildContext context, [ result ])

将栈顶路由出栈,result 为页面关闭时返回给上一个页面的数据。

最后、总结

1、Navigator 是 Flutter 中管理页面堆栈和路由的核心组件。即使你工作中使用的是第三方导航库,了解它的源码依然能提高你的核心能力。

1)架构是如何设计?

2)如何使用(是否掌握了所有的使用方法)?

3)局限性如何突破?

2、局限性突破示例:

我曾经困扰于 popUntil 无法传值的问题,期望它可以像 pop 一样返回值。今天突然发现只要加一个参数即可实现

2.1 源码修改

SDK 源码

javascript 复制代码
/// Calls [pop] repeatedly until the predicate returns true.
///
/// {@macro flutter.widgets.navigator.popUntil}
///
/// {@tool snippet}
///
/// Typical usage is as follows:
///
/// ```dart
/// void _doLogout() {
///   navigator.popUntil(ModalRoute.withName('/login'));
/// }
/// ```
/// {@end-tool}
void popUntil(RoutePredicate predicate) {
  _RouteEntry? candidate = _history.cast<_RouteEntry?>().lastWhere(
    (_RouteEntry? e) => e != null && _RouteEntry.isPresentPredicate(e),
    orElse: () => null,
  );
  while(candidate != null) {
    if (predicate(candidate.route)) {
      return;
    }
    pop();
    candidate = _history.cast<_RouteEntry?>().lastWhere(
      (_RouteEntry? e) => e != null && _RouteEntry.isPresentPredicate(e),
      orElse: () => null,
    );
  }
}

SDK 源码魔改版:

dart 复制代码
void popUntil<T extends Object?>(RoutePredicate predicate, [ T? result ]) {
  ...
    pop(result);
  ...
}
2.2 魔改 popUntil 使用 demo:

PageTwo->PageThree->PageFour->PageFive->PageTwo

dart 复制代码
//当前页面 PageFive
Navigator.of(context).popUntil(ModalRoute.withName("/PageTwo"), {" PageFive": "999"});
dart 复制代码
//当前页面 PageTwo
final result = await Navigator.of(context).push(MaterialPageRoute(
  builder: (context) => const PageThree(),
  settings: const RouteSettings(
    name: "/PageThree",
  ),
));
DLog.d("$widget result: $result");
//[log] DLog 2025-02-25 10:26:04.382607 PageTwo result: {PageFive: 999}//完美传值

github

相关推荐
一个专注写代码的程序媛1 小时前
vue组件间通信
前端·javascript·vue.js
一笑code1 小时前
美团社招一面
前端·javascript·vue.js
懒懒是个程序员1 小时前
layui时间范围
前端·javascript·layui
NoneCoder1 小时前
HTML响应式网页设计与跨平台适配
前端·html
凯哥19702 小时前
在 Uni-app 做的后台中使用 Howler.js 实现强大的音频播放功能
前端
烛阴2 小时前
面试必考!一招教你区分JavaScript静态函数和普通函数,快收藏!
前端·javascript
GetcharZp2 小时前
xterm.js 终端神器到底有多强?用了才知道!
前端·后端·go
JiangJiang2 小时前
🚀 React 弹窗还能这样写?手撸一个高质量 Modal 玩起来!
前端·javascript·react.js
吃炸鸡的前端2 小时前
el-transfer穿梭框数据量过大的解决方案
前端·javascript
高德开放平台2 小时前
文末有奖|高德MCP 2.0 出行领域首发打通大模型与高德地图APP互联
前端