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 分钟前
ES6 扩展运算符与 Rest 参数
前端·ecmascript·es6
purpleseashell_Lili18 分钟前
TypeScript
前端·typescript
哎呦你好26 分钟前
CSS 盒子模型:一文了解padding和margin,使用内边距、外边距和边框随心所欲实现布局!
前端·css
前端 贾公子36 分钟前
小程序使用web-view 修改顶部标题 && 安全认证文件部署在nginx
开发语言·前端·javascript
胖墩会武术1 小时前
通过Auto平台与VScode搭建远程开发环境(以Stable Diffusion Web UI为例)
前端·vscode·stable diffusion
未来之窗软件服务2 小时前
封装拍照模块,拓展功能边界—仙盟创梦IDE
前端·javascript·html·摄像头·仙盟创梦ide
源力祁老师3 小时前
Odoo: Owl Props 深度解析技术指南
前端·javascript·网络
Liu.7743 小时前
vue3样式穿透用法
前端·vue.js·elementui
SYKMI5 小时前
@JsonFormat时区问题
java·前端·数据库
海盐泡泡龟7 小时前
web常见的攻击方式有哪些?如何防御?
前端·vue.js·webpack