一、需求来源
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
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}//完美传值