文章脉络与 commit 的程序码变化是一致的,可以对照着看
若文章有任何错误、不合台湾用语,欢迎指正,感谢
- 了解 Flutter 基本知识
- 了解 Navigation v2
下图为流程
RouteInformation
,该类别含有网址与状态RouteInformationParser
解析 RouteInformation
,并转换成需要资料结构(在此范例是使用 自订 的 MyRouteConfig
)MyRouteConfig
),传递给 RouterDelegate
RouterDelegate
可以根据 MyRouteConfig
决定路由的内容,并呈现出来以下内容从 commit 讯息为 starter
开始
- 如果 commit 讯息出现此图示 「?」,代表该 commit 无法编译成功或会有期他问题,请接着往下读
- 在各个章节或小节开头会列出该节完成後的 commit 的连结
MyRouteConfig
commit create my_route_config
navigation/my_route_config.dart
https://www.xxx.com/products/123
,path 就是 /products/123
import 'package:equatable/equatable.dart';
class MyRouteConfig extends Equatable {
// 1. 储存路径
final Uri uri;
// 2. 私有建构子
// 因为每个路径都会需要 path 的参数,所以写一个共用的建构子
MyRouteConfig._(String path): uri = Uri(path: path);
// 3. 各个路径设定
MyRouteConfig.home(): this._('/');
MyRouteConfig.about(): this._('/about');
MyRouteConfig.contact(): this._('/contact');
MyRouteConfig.unknown(): this._('/unknown');
// 4. 覆写 Equatable 的方法
// 两个 config 在比较的时候,只需要使用 `==` 就可以比较路径来判断相等性
@override
List<Object> get props => [ uri.path ];
}
navigation/my_route_information_parser.dart
import 'package:flutter/material.dart';
import 'my_route_config.dart';
class MyRouteInformationParser extends RouteInformationParser<MyRouteConfig> {
// 1. 如函式名称,就是解析 RouteInformation(存有网址与状态)
@override
Future<MyRouteConfig> parseRouteInformation(
RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.location);
final pathSegments = uri.pathSegments;
final pathSegmentsCount = pathSegments.length;
// Handle `/`
if (pathSegmentsCount == 0) {
return MyRouteConfig.home();
}
// 其他路由...
return MyRouteConfig.unknown();
}
// 之後提,先当作没看见
@override
RouteInformation restoreRouteInformation(MyRouteConfig routeConfig) {
return RouteInformation(location: routeConfig.uri.path);
}
}
parseRouteInformation
负责解析路由资讯uri.pathSegments
资料类型为 List<String>
,会将网址切开,例如:
/
,pathSegments
为 []
(空串列)/about
,pathSegments
为 [ 'about' ]
(长度为 1)final uri = Uri.parse(routeInformation.location);
final pathSegments = uri.pathSegments;
final pathSegmentsCount = pathSegments.length;
RouteInformation
转换成 RouteConfig
,将其回传(後续会传给 RouterDelegate
使用)// Handle `/`
if (pathSegmentsCount == 0) {
// 这里代表 `pathSegments` 为空,网址为 `/`
// 只有 `RouteConfig.home()` 符合,回传此 `RouteConfig`
return RouteConfig.home();
}
补上其他路由後的完整程序码:
import 'package:flutter/material.dart';
import 'my_route_config.dart';
class MyRouteInformationParser extends RouteInformationParser<MyRouteConfig> {
@override
Future<MyRouteConfig> parseRouteInformation(
RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.location);
final pathSegments = uri.pathSegments;
final pathSegmentsCount = pathSegments.length;
// Handle `/`
if (pathSegmentsCount == 0) {
return MyRouteConfig.home();
}
// Handle `/xxx`
if (pathSegmentsCount == 1) {
if (pathSegments[0] == MyRouteConfig.about().uri.pathSegments[0]) {
return MyRouteConfig.about();
}
if (pathSegments[0] == MyRouteConfig.contact().uri.pathSegments[0]) {
return MyRouteConfig.contact();
}
if (pathSegments[0] == MyRouteConfig.unknown().uri.pathSegments[0]) {
return MyRouteConfig.unknown();
}
}
return MyRouteConfig.unknown();
}
@override
RouteInformation restoreRouteInformation(MyRouteConfig routeConfig) {
return RouteInformation(location: routeConfig.uri.path);
}
}
commit ? create my_router_delegate
navigation/my_router_delegate.dart
MyRouterDelegate
继承自 RouterDelegate<MyRouteConfig>
import 'package:flutter/material.dart';
import 'package:tutorial_flutter_navigation_v2/screens/simple_screen.dart';
import 'my_route_config.dart';
class MyRouterDelegate extends RouterDelegate<MyRouteConfig>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<MyRouteConfig> {
@override
GlobalKey<NavigatorState> get navigatorKey => throw UnimplementedError();
@override
MyRouteConfig get currentConfiguration => throw UnimplementedError();
@override
Widget build(BuildContext context) {
throw UnimplementedError();
}
@override
Future<void> setNewRoutePath(MyRouteConfig newState) async {
throw UnimplementedError();
}
}
新增 _currentConfiguration
储存目前的 MyRouteConfig
class MyRouterDelegate extends RouterDelegate<MyRouteConfig>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<MyRouteConfig> {
// 1. 新增变数:目前的 `MyRouteConfig`
MyRouteConfig _currentConfiguration;
// ...
// 2. 覆写取用 `MyRouteConfig` 的方法
@override
MyRouteConfig get currentConfiguration => _currentConfiguration;
// ...
// 3. 如果有新的 MyRouteConfig,就更新一下吧
@override
Future<void> setNewRoutePath(MyRouteConfig newMyRouteConfig) async {
_currentConfiguration = newMyRouteConfig;
return;
}
}
commit ? build Navigator
建立一个 Navigator 负责处理主画面的路由
class MyRouterDelegate extends RouterDelegate<MyRouteConfig>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<MyRouteConfig> {
// 1. 宣告
final GlobalKey<NavigatorState> _navigatorKey;
// ...
// 2. 在建构的时候初始化
MyRouterDelegate(): _navigatorKey = GlobalKey<NavigatorState>();
// 3. 覆写 `navigatorKey`,让外部取得 `navigatorKey`
@override
GlobalKey<NavigatorState> get navigatorKey => _navigatorKey;
// ...
@override
Widget build(BuildContext context) {
// 4. 建立 Navigator,并将 `key` 传入
return Navigator(
key: _navigatorKey,
);
}
// ...
}
建立 onPopPage
并传入 Navigator
bool onPopPage(Route<dynamic> route, result) {
return route.didPop(result);
}
return Navigator(
key: _navigatorKey,
onPopPage: onPopPage,
);
根据现有的 MyRouteConfigs
,传入 Navigator 需要的 pages
,决定显示哪些页面
class MyRouterDelegate extends RouterDelegate<MyRouteConfig>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<MyRouteConfig> {
// ...
@override
Widget build(BuildContext context) {
// ...
return Navigator(
key: _navigatorKey,
pages: _buildPages(), // 3. 传给 Navigator
onPopPage: onPopPage,
);
}
// 1. 建立 Pages
List<Page<dynamic>> _buildPages() {
final List<Page<dynamic>> pages = [
MaterialPage(
key: ValueKey('Home'),
name: 'Home',
child: SimpleScreen(text: 'Home')
),
];
// 2. 根据不同路径,加入不同 Page
if (_currentConfiguration == MyRouteConfig.contact())
pages.add(MaterialPage(
key: ValueKey('Contact'),
name: 'Contact',
child: SimpleScreen(text: 'Contact')
));
if (_currentConfiguration == MyRouteConfig.about())
pages.add(MaterialPage(
key: ValueKey('About'),
name: 'About',
child: SimpleScreen(text: 'About')
));
if (_currentConfiguration == MyRouteConfig.unknown())
pages.add(MaterialPage(
key: ValueKey('Unknown'),
name: 'Unknown',
child: SimpleScreen(text: 'Unknown')
));
return pages;
}
@override
Future<void> setNewRoutePath(MyRouteConfig newMyRouteConfig) async {
_currentConfiguration = newMyRouteConfig;
return;
}
}
刚刚写的 MyRouterDelegate
、MyRouteInformationParser
拿来使用
因为左侧侧边栏位固定,只需要对主内容进行路由管理
因此,将 Router
(为了美观,被 Exapnded
包住)放在 BaseSideBar
旁边
现在应该会是 About
的页面
layout/base_layout.dart
// ...
class _BaseLayoutState extends State<BaseLayout> {
// 提供初始的路由资讯(initialRouteInformation)
PlatformRouteInformationProvider _routeInformationProvider;
MyRouterDelegate _routerDelegate;
MyRouteInformationParser _routeInformationParser = MyRouteInformationParser();
@override
void initState() {
super.initState();
_routeInformationProvider = PlatformRouteInformationProvider(
initialRouteInformation: RouteInformation(
location: '/about',
)
);
_routerDelegate = MyRouterDelegate();
}
@override
void dispose() {
_routeInformationProvider.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
BaseSideBar(),
Expanded(
child: Router(
routerDelegate: _routerDelegate,
routeInformationParser: _routeInformationParser,
routeInformationProvider: _routeInformationProvider,
)
),
],
),
);
}
}
commit add some jumping page method
新增一些跳转网页的方法
navigation/my_router_delegate.dart
void goHome() {
_currentConfiguration = MyRouteConfig.home();
notifyListeners();
}
void goAbout() {
_currentConfiguration = MyRouteConfig.about();
notifyListeners();
}
void goContact() {
_currentConfiguration = MyRouteConfig.contact();
notifyListeners();
}
SimpleScreen
都是在 MyRouterDelegate
中的 Navigator
的 pages
因此可以透过 context
取得 MyRouterDelegate
screens/simple_screen.dart
TextButton(
onPressed: () {
final delegate = Router.of(context).routerDelegate as MyRouterDelegate;
delegate.goAbout();
},
child: Text('About us >'),
),
将初始路径改为 about
以外,再点击按钮,跳转即可成功
(详细看 commit 内容)
BaseSideBar
没有被 Router
包住,所以没办法透过 context
取得 MyRouterDelegate
因此透过建构子传递 MyRouterDelegate
给 BaseSideBar
使用
先将 _routerDelegate
传给 BaseSideBar
layout/base_layout.dart
BaseSideBar(
myRouterDelegate: _routerDelegate,
),
import 'package:flutter/material.dart';
import 'package:tutorial_flutter_navigation_v2/navigation/my_router_delegate.dart';
class BaseSideBar extends StatefulWidget {
final MyRouterDelegate myRouterDelegate;
// 1. 传入 `MyRouterDelegate`
BaseSideBar({
@required this.myRouterDelegate
});
@override
_BaseSideBarState createState() => _BaseSideBarState();
}
class _BaseSideBarState extends State<BaseSideBar> {
MyRouterDelegate _myRouterDelegate;
@override
void initState() {
super.initState();
_myRouterDelegate = widget.myRouterDelegate;
}
@override
Widget build(BuildContext context) {
return Container(
// ...
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_title(),
_link(
text: 'Home',
onPressed: () {
_myRouterDelegate.goHome(); // 2. 跳转至首页
},
),
_link(
text: 'About',
onPressed: () {
_myRouterDelegate.goAbout(); // 2. 跳转至关於页面
},
),
_link(
text: 'Contact',
onPressed: () {
_myRouterDelegate.goContact(); // 2. 跳转至联络页面
},
),
],
),
);
}
// ...
}
#
(井字号)」commit setPathUrlStrategy
现在的网址都是 localhost:xxxx/#/
先把网址变成 localhost:xxxx/
利於我们後面处理网址
main.dart
import 'package:flutter/material.dart';
import 'package:url_strategy/url_strategy.dart'; // 加上这行
import 'app/app.dart';
void main() {
setPathUrlStrategy(); // 加上这行
runApp(MyApp());
}
当第一次进入页面、或重整,如果网址是 localhost:xxx
那就没问题,
可是如果是 localhost:xxx/yyy
就会有问题
因为没有设定 App 的路由系统,
以目前需求来说,不需要在 App 那边做路由
所以让任何路径都回传 BaseLayout
即可
commit allow all paths
app.dart
import 'package:flutter/material.dart';
import 'package:tutorial_flutter_navigation_v2/layout/base_layout.dart';
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Navigation',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
onGenerateRoute: (_) {
// 不做任何判断,直接回传
return PageRouteBuilder(
transitionDuration: const Duration(milliseconds: 0),
reverseTransitionDuration: const Duration(milliseconds: 0),
// 都回传 `BaseLayout`
pageBuilder: (__, ___, ____) => BaseLayout()
);
}
);
}
}
commit init location by parsing url
到目前为止,PlatformRouteInformationProvider
的 initialRouteInformation
的 location
初始化都是固定的
透过套件取得网址、解析、再传给 PlatformRouteInformationProvider
即可
layout/base_layout.dart
@override
void initState() {
super.initState();
_routeInformationProvider = PlatformRouteInformationProvider(
initialRouteInformation: RouteInformation(
location: _getInitialRoute(),
)
);
// ...
}
String _getInitialRoute() {
var initialRoute = "/";
if (kIsWeb) {
final origin = html.window.location.origin;
final href = html.window.location.href;
initialRoute = href.substring(origin.length);
}
return initialRoute;
}
若使用浏览器的上下页按钮,
会发现首页会出现一段时间,然後才被目前的页面盖住
这里采用简单的做法:「给 MyRouterDelegate
初始化的 MyRouteConfig
」
MyRouteConfig
」方法放於公用程序(Utility)为了後续程序码整洁
把 MyRouteInformationParser
中的 parseRouteInformation
程序码抽出
放於 utilities/route_utility.dart
import 'package:flutter/material.dart';
import 'package:tutorial_flutter_navigation_v2/navigation/my_route_config.dart';
class RouteUtility {
RouteUtility._();
static MyRouteConfig getRouteConfig(RouteInformation routeInformation) {
final uri = Uri.parse(routeInformation.location);
final pathSegments = uri.pathSegments;
final pathSegmentsCount = pathSegments.length;
// Handle `/`
if (pathSegmentsCount == 0) {
return MyRouteConfig.home();
}
// Handle `/xxx`
if (pathSegmentsCount == 1) {
if (pathSegments[0] == MyRouteConfig.about().uri.pathSegments[0]) {
return MyRouteConfig.about();
}
if (pathSegments[0] == MyRouteConfig.contact().uri.pathSegments[0]) {
return MyRouteConfig.contact();
}
if (pathSegments[0] == MyRouteConfig.unknown().uri.pathSegments[0]) {
return MyRouteConfig.unknown();
}
}
return MyRouteConfig.unknown();
}
}
MyRouteInformationParser
的 parseRouteInformation
则使用 RouteUtility.getRouteConfig()
navigation/my_route_information_parser.dart
import 'package:flutter/material.dart';
import 'package:tutorial_flutter_navigation_v2/utilities/route_utility.dart';
import 'my_route_config.dart';
class MyRouteInformationParser extends RouteInformationParser<MyRouteConfig> {
@override
Future<MyRouteConfig> parseRouteInformation(
RouteInformation routeInformation) async {
return RouteUtility.getRouteConfig(routeInformation);
}
@override
RouteInformation restoreRouteInformation(MyRouteConfig routeConfig) {
return RouteInformation(location: routeConfig.uri.path);
}
}
MyRouterDelegate
中的 _currentConfiguration
根据初始化的路径,产生对应的 MyRouteConfig
,并传给 MyRouterDelegate
,让一开始就已经决定好路径,而不会重新侦测并转场,避免上述的问题
layout/base_layout.dart
class _BaseLayoutState extends State<BaseLayout> {
// ...
@override
void initState() {
super.initState();
final routeInformation = RouteInformation(
location: _getInitialRoute(),
);
_routeInformationProvider = PlatformRouteInformationProvider(
initialRouteInformation: routeInformation
);
final routeConfig = RouteUtility.getRouteConfig(routeInformation);
_routerDelegate = MyRouterDelegate(myRouteConfig: routeConfig);
}
// ...
}
修改 MyRouterDelegate
的建构子,使其接收 MyRouteConfig
layout/base_layout.dart
MyRouterDelegate({
MyRouteConfig myRouteConfig,
}): _currentConfiguration = myRouteConfig,
_navigatorKey = GlobalKey<NavigatorState>();
网页可以储存历史纪录与其状态,使用上一页或下一页的时候状态仍会存在
详情可参考 MDN Web Docs: 操控浏览器历史纪录
待更新...
如果上面的成果仍不满意,可以尝试从以下角度继续完善专案
LayoutBuilder
或自己制作一些特别的 Widget 或机制若文章有任何错误、不合台湾用语,欢迎指正,感谢您的阅读
我的 Github Page
喜欢 Flutter ❤️
>>: [Day 40] 心情随笔後台及前台(二) - 新增心情随笔资料
Logstash 本篇介绍何谓Logstash以及他的功用,并从安装到使用基本的pipeline L...
这个问题描述了常见的SQL注入场景,该场景采用了像1 = 1这样的所谓“身份方程序”。攻击者可以输入...
了解了 Django 的运作之後,相信大家一步步对 Django 的操作更佳的熟悉,在前面我们介绍了...
前言: 前两遍的基础activity 和 fragment 就可以作出不错的app了,但功能愈来愈多...
Timecop 可以帮助你在测试时将时间冻住,因为有些讯息中会带有时间,如果要确保时间一致就必须要将...