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

相关推荐
恋猫de小郭25 分钟前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
一只大侠的侠5 小时前
Flutter开源鸿蒙跨平台训练营 Day 10特惠推荐数据的获取与渲染
flutter·开源·harmonyos
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
renke33648 小时前
Flutter for OpenHarmony:色彩捕手——基于HSL色轮与感知色差的交互式色觉训练系统
flutter
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端