发布时间:2021年4月15日
您知道 Flutter web 已经稳定了吗?您想为您的应用程序支持 Flutter web 吗?您是否在为支持像真正的网络应用一样的URL导航而奋斗?这是您的票,为您的超强应用支持导航器2。
美国加利福尼亚州邦克山,照片:Shabdro Photo
真正欣赏戴春恒他通过阅读这篇文章并检查其质量来帮助我。
这是图库的一个神奇功能,如果你没有读过我以前为图库写的文章,你可以从这里开始你的旅程。
这是一篇很长的文章,因为我收集了你可能需要知道的关于navigator 2实现的所有信息。光看这篇文章可能没有用,所以试着做一个新的项目,像这样一步一步地实现它,别忘了带些零食。
让我们先看看结果。
浏览器中的样本导航器2
我希望你喜欢我选择的照片
我喜欢关于翩翩的一切,它的人、事件、社区。这就是这就是为什么我做了这个画廊来展示他们有多可爱。我喜欢添加您要求的相册,以使这个画廊更加美丽。如果您想这样做,请查看这个资料库。
正如你所看到的,我们的画廊支持许多情况。
- 命令式导航
- 声明式导航
- 可能的和不可能的404导航
- 浏览器后退按钮导航
- 浏览器前进按钮导航
这真是太酷了,这不是很多,这就是所有的情况! 让我们一起来学习一下。
声明性与强制性导航 强迫性导航
我相信你们中的大多数人都熟悉声明式UI编程,就像我们在Flutter widget中一贯使用的那样,例如,要定义一个叫做ViewB的widget,你要告诉框架。
// Declarative style
return ViewB(
color: red,
child: ViewC(...),
)
但对于命令式的方式。
// Imperative style
b.setColor(red)
b.clearChildren()
ViewC c3 = new ViewC(...)
b.add(c3)
我们的画廊有两个主要的屏幕,我们想呈现给用户。AlbumPage和MediaFullscreen。它还有一个RootPage,实际上是我们的第一个相册页面。
我通过下面的GIF让这个导航变得简单。
声明式与强制式导航
第一个男孩想找到他朋友的照片,但他不知道照片属于哪个相册,所以他应该开始询问代表画廊相册的每只狗。
我们的第二个男孩!有一些关于他朋友的照片的信息,并告诉第一只狗,他回答说照片属于哪个相册的具体地址。因此,他不应该浪费时间,直接进入该相册。
第一个男孩所做的事情与我们在移动应用程序中所做的事情相同。我们从头开始我们的应用程序,并逐页导航到其他屏幕。然而,在网络应用中事情可能是不同的,我们应该用Navigator 2来处理它们。
为什么是Navigator 2?
Navigator 2.0 API为框架增加了新的类,以便使应用程序的屏幕成为应用程序状态的函数,并提供解析来自底层平台的路由(如网络URL)的能力。
Navigator 2引入了一些新的方法(你可以在这篇文章中读到它们),使导航处理更难,但不复杂。
你不要为了一些新的功能,通过添加一些新的包来使你的应用程序变得复杂,这一点太重要了。这就是为什么我喜欢Flutter这种方式,它为你提供了许多工具,你可以通过使用这些工具来支持新的功能,而你的应用程序仍然易于理解。
在这篇文章中,我将解释这些。
- Gallery Router服务。定义路由和路由器状态模型,制作解析器和状态
- Gallery Router Delegates: 制作外部和内部路由器
- 画廊区块:如何根据区块事件改变路由器的状态
那么,让我们开始吧。
画廊导航结构和数据模型
到目前为止,我们已经了解了我们对跨平台应用程序的要求和愿望,现在我们可以开始实现这个令人惊奇的功能了。如果你读过我之前提到的文章,我们需要定义一个新的服务,叫做routers_service(如果你不想读,就在你想的地方做所有的类),我们通过以下黄金命令来做。
$ cd services
$ mkdir routers_service
$ cd routers_service
$ flutter pub global run stagehand package-simple
我们需要为浏览器状态定义一个简单的模型,所以我们需要模型依赖和gallery_service,但我们在这里不需要Bloc依赖,因为我们可以通过ChangeNotifiereasily处理我们的路由器状态变化,但我将在表现层使用Bloc来改变路由器状态。它的依赖关系是这样的。
dependencies:
flutter:
sdk: flutter
model_dependency_service:
path: ../dependencies_service/model_dependency_service
gallery_service:
path: ../gallery_service
最后,别忘了把这个服务添加到你的应用程序中,把它添加到pubspec.yaml中。
dependencies:
routers_service:
path: services/routers_service
定义路由
这个服务负责routers的变化和定义我们在画廊中的所有路由,并将它们解析到展示层。所以我们有三个文件,分别是state、routers和parser。路由从一个叫GalleryRoutePath的抽象类中延伸出来,它们携带了所有你应该知道的关于路由路径的东西。例如,其中一个全屏路径/gallery/#/album/album2?view=photo8告诉我们的应用程序,客户想要进入一个'album2'的专辑,看到一张'photo8'的照片,所以MediaFullscreenPath应该包含所有这些。
class MediaFullscreenPath extends GalleryRoutePath {
static const String kFullscreenPageLocation = 'view';
const MediaFullscreenPath(this.albumPath, this.mediaPath);
final String albumPath;
final String mediaPath;
}
对于其余的路线,我们也是这样做的。你可以在这里找到它们。
如何制作解析器和浏览器状态?
我们将制作一个解析器,一个从操作系统到我们的应用程序表现层的单向桥梁。多亏了RouteInformationParser类,我们不仅可以得到请求的URI,而且还可以传递navigationstate,并从浏览器中得到它们,以便进行下一个路由。在制作解析器之前,让我们来定义这个浏览器状态。
图库页面结构
浏览器状态用于必要的导航,应该包含从开始到当前路线的所有页面所需要的东西,正如你在画廊的页面结构中看到的,'相册'用于相册路径,'照片'用于全屏照片页面,例如,要显示'照片8'照片页面,我们需要在下面有所有的相册页面,所以我们需要一个到目前为止我们访问过的画廊路径列表,以及当前的相册或照片。如果他们是通过URL获得的,那么当前的相册或照片可以是空的,如果他们是从上一个相册页面获得的,那么就不是空的。
为了明确这一点,如果我们在一个相册中,那么我们已经得到了相册或照片的详细信息。否则,我们就没有像从浏览器中获得的URL那样的信息,所以它们是空的。让我们在这个模型中做个总结。
import 'package:built_collection/built_collection.dart';
import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
import 'package:gallery_service/gallery_service.dart';
import 'serializers.dart';
part 'state.g.dart';
abstract class BrowserState
implements Built<BrowserState, BrowserStateBuilder> {
BuiltList<String> get galleriesHistory;
Gallery? get gallery;
Media? get media;
BrowserState._();
factory BrowserState([void Function(BrowserStateBuilder) updates]) =
_$BrowserState;
Map<String, dynamic> toJson() {
return serializers.serializeWith(BrowserState.serializer, this)
as Map<String, dynamic>;
}
static BrowserState fromJson(Map<String, dynamic> json) {
return serializers.deserializeWith(BrowserState.serializer, json);
}
static Serializer<BrowserState> get serializer => _$browserStateSerializer;
}
记住,你必须将浏览器状态以json形式传递给浏览器,所以你需要toJson和fromJson这两个函数来处理这个模型。
数据模型应该是什么样子的?
为了给状态模型和相册及照片的细节提供数据,你可以从API中得到它们。我建立了两个API提供者,服务器和Github内容API。你可以选择任何你想要的。在这篇文章中。
注意。我将使用Github API作为提供者,从画廊-资产库中获取图片。在浏览器中,你会看到一个像2f616c62756d32这样的十六进制字符串,而不是专辑名称,因为我需要获得专辑的完整路径,比如/album2/album3/image.jpg。但不要担心,我将在本教程中使用专辑和照片的关键字,以避免使事情变得模糊不清。你可以在这里查看所有的回复样本。
如果你还记得,我们还需要两个来自画廊的需求,那就是。
- 浏览器返回按钮导航
- 浏览器前进按钮导航
因此,我们应该把图库页面结构改成这样。
最终的图库页面结构
因此,你可以得出结论,我们需要每个相册都有父辈和子辈的相册。让我们来看看图库相册的模型。
abstract class Gallery implements Built<Gallery, GalleryBuilder> {
String get path;
String get current;
String? get parent;
BuiltList<Album> get albums;
BuiltList<Media> get medias;
Gallery._();
factory Gallery([void Function(GalleryBuilder) updates]) = _$Gallery;
static Gallery fromJson(String serialized) {
return serializers.fromJson(Gallery.serializer, serialized)!;
}
static Serializer<Gallery> get serializer => _$gallerySerializer;
}
在这个模型中,我用Gallery这个关键词来表示当前的相册,用Album来表示它的子相册,以区分它们,所以不要混淆了。
为什么我们需要获得当前路径?因为要启动我们的应用程序,用户和我们的应用程序不知道根相册的第一次出现,所以我们需要从服务器或提供者那里得到它,以保存在我们的画廊的历史记录中,而parent是可空的,因为我们没有根相册的父相册。
abstract class Media implements Built<Media, MediaBuilder> {
String get name;
String get path;
MediaType get type;
String get thumbnail;
String get url;
Media._();
factory Media([void Function(MediaBuilder) updates]) = _$Media;
static Media fromJson(String serialized) {
return serializers.fromJson(Media.serializer, serialized)!;
}
static Serializer<Media> get serializer => _$mediaSerializer;
}
我们应该为媒体或照片模型做同样的事情。我们需要一个叫做path的属性,通过这个路径从提供者那里获得信息。
总结一下,你需要两个主要的Endpoints来使所有的事情都顺利进行。
- 画廊细节端点提供者
- 照片细节端点提供者
因此,如果你需要做后面的功能,你必须确保与你的后端提供者兼容。
画廊导航解析器
我们应该建立所有这些东西来做这个解析器。我们需要从RouteInformationParser扩展并覆盖两个主要函数。
- 使用parseRouteInformation从操作系统中获取URI和浏览器状态。
- 使用restoreRouteInformation将URI和浏览器状态传递给操作系统。
解析路由信息
定义是显而易见的,它们的实现也超级简单,但有一个小技巧 首先,看一下画廊的parseRouteInformation。
import 'routes.dart';
class RouterConfiguration {
RouterConfiguration(this.path, this.browserState);
GalleryRoutePath? path;
BrowserState? browserState;
}
class GalleryRouteInformationParser
extends RouteInformationParser<RouterConfiguration> {
@override
Future<RouterConfiguration> parseRouteInformation(
RouteInformation routeInformation) {
final uri = Uri.parse(routeInformation.location!);
final state = routeInformation.state;
final browserState = state == null
? BrowserState()
: BrowserState.fromJson(state.toString());
// '/'
if (uri.pathSegments.isEmpty) {
final newState = browserState.galleriesHistory
.contains(RootPagePath.kRootPageLocation)
? browserState
: browserState.rebuild(
(b) => b..galleriesHistory.add(RootPagePath.kRootPageLocation));
return SynchronousFuture<RouterConfiguration>(
RouterConfiguration(const RootPagePath(), newState));
}
// '/${}/'
switch (uri.pathSegments.first) {
// '/login/'
case LoginPagePath.kLoginPageLocation:
return SynchronousFuture<RouterConfiguration>(
RouterConfiguration(const LoginPagePath(), browserState));
// '/album/${}'
case AlbumPagePath.kAlbumPageLocation:
if (uri.pathSegments.length > 1) {
final albumPath = uri.pathSegments[1];
final filePath =
uri.queryParameters[MediaFullscreenPath.kFullscreenPageLocation];
final newState = browserState.galleriesHistory.contains(albumPath)
? browserState
: browserState.rebuild((b) => b..galleriesHistory.add(albumPath));
// '/album/${}?view=${}'
if (filePath != null && filePath.isNotEmpty) {
return SynchronousFuture<RouterConfiguration>(RouterConfiguration(
MediaFullscreenPath(albumPath, filePath), newState));
}
// '/album/${}/'
return SynchronousFuture<RouterConfiguration>(
RouterConfiguration(AlbumPagePath(albumPath), newState));
}
}
// 404
return SynchronousFuture<RouterConfiguration>(
RouterConfiguration(const UnknownPagePath(), browserState));
}
}
我把URI按其长度分为三个主要段,两个段为两个主要页面。相册和全屏照片。我们应该在一个叫做RouterConfiguration的封装类中,使用SynchronousFuture为每个片段返回画廊的路径和状态,因为结果可以被计算出来同步地计算。这个RouterConfiguration类被我在后面解释的演示文稿路由器委托使用。
这里的诀窍是什么?诀窍是,我们应该通过两种方式提供浏览器状态数据:这里和来自表现层。因为对于声明性导航来说,我们需要浏览器状态数据,而这个函数是我们从URI中得到的所有数据,例如,相册和照片路径。要注意这样做。例如,如果我们有一个来自用户的请求,需要导航到相册和全屏页,我们应该做以下说明来完成。
final albumPath = uri.pathSegments[1];
final filePath =
uri.queryParameters[MediaFullscreenPath.kFullscreenPageLocation];
final newState = browserState.galleriesHistory.contains(albumPath)
? browserState
: browserState.rebuild((b) => b..galleriesHistory.add(albumPath));
注意检查这个状态是否在之前被添加到演示层的浏览器状态中,通过这个代码:browserState.galleriesHistory.contains(AlbumPath)。
还有一件值得注意的事情是,如果没有一个路径是可以接受的,应该返回一个未知的页面。
// 404
return SynchronousFuture<RouterConfiguration>(
RouterConfiguration(const UnknownPagePath(), browserState));
这就是我所说的不可能的404导航,因为URI根本不能接受,比如说像/gallery/my/albums/album2/hello/这样的东西! 对于可能的,我在后面解释,因为它应该在表现层处理。
恢复路线信息
下一个方法restoreRouteInformation,必须返回RouteInformation,其中包含location,也就是具体的位置路径和state,也就是我们的浏览器状态,要注意把它传给json。结果是这样的。
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:routers_service/src/models/index.dart';
import 'routes.dart';
class GalleryRouteInformationParser
extends RouteInformationParser<RouterConfiguration> {
@override
RouteInformation? restoreRouteInformation(RouterConfiguration configuration) {
if (configuration.path is UnknownPagePath) {
return RouteInformation(
location: '/' + UnknownPagePath.kUnknownPageLocation,
state: configuration.browserState!.toJson(),
);
}
if (configuration.path is RootPagePath) {
return RouteInformation(
location: '/' + RootPagePath.kRootPageLocation,
state: configuration.browserState!.toJson(),
);
}
if (configuration.path is LoginPagePath) {
return RouteInformation(
location: '/' + LoginPagePath.kLoginPageLocation,
state: configuration.browserState!.toJson(),
);
}
if (configuration.path is AlbumPagePath) {
final path = configuration.path as AlbumPagePath;
return RouteInformation(
location: '/' + AlbumPagePath.kAlbumPageLocation + '/' + path.albumPath,
state: configuration.browserState!.toJson(),
);
}
if (configuration.path is MediaFullscreenPath) {
final path = configuration.path as MediaFullscreenPath;
return RouteInformation(
location: '/' +
AlbumPagePath.kAlbumPageLocation +
'/' +
path.albumPath +
'?' +
MediaFullscreenPath.kFullscreenPageLocation +
'=' +
path.mediaPath,
state: configuration.browserState!.toJson(),
);
}
return null;
}
}
图库导航路由器状态
这个类应该用于表现层的状态处理,这意味着它持有相同的BrowserState,并在它被表现层改变时通知表现层。
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:routers_service/src/models/index.dart';
import 'routes.dart';
class GalleryRoutersState extends ChangeNotifier {
GalleryRoutersState();
GalleryRoutePath? get routePath => _routePath;
GalleryRoutePath? _routePath;
set routePath(GalleryRoutePath? value) {
if (value != _routePath) {
_routePath = value;
notifyListeners();
}
}
BrowserState? get browserState => _browserState;
BrowserState? _browserState;
set browserState(BrowserState? value) {
if (value != _browserState) {
_browserState = value;
notifyListeners();
}
}
}
routePath属性是当前的应用路径,可以是我们的路由之一,比如UnknownPagePath、LoginPagePath、RootPagePath、AlbumPagePath和MediaFullscreenPath。每当应用程序的路径类型需要改变时,我们就应该改变这个。
好了,我们的路由器服务已经完成。现在我们准备跳到演示层,处理呈现给我们可爱的用户的页面。
图库导航展示层
在这一层,我们应该使用Flutter框架提供的RouterDelegate。正如我常说的,我们必须使我们的应用程序易于理解,因为在一个类中同时处理所有的页面会使人难以理解,不是吗?所以,让我们把我们的画廊展示路由器分成两个独立的RouterDelegate,Outer Router用于我们的通用页面,如LoginPage,Inner Router用于RootPage、AlbumPage、MediaFullscreen和UnknownPage,这些都是用于画廊的屏幕。但是,跳过了这个简单的问题。
为什么是嵌套路由?
-
正如在flutter/uxr#35中提到的,这对于多个团队在同一个项目中工作是很有用的,以分层结构的方式来构建,这使得项目不那么复杂。例如,在画廊中,我们有两个主要的简单屏幕。LoginPage和GalleryShell,因为它们之间没有关系,所以在它们的目录展示层中被分开。 GalleryShell有一些子页面。
- RootPage(第一个相册页面)。
- 相册页
- FullscreePage
-
我们应该把改变我们的状态看作是onPopPage方法中的相关变化,所以把所有的变化放在一个路由器中,而它们之间并不相关,这不是一个好主意。例如,这是GalleryShell的onPopPage方法,它处理其子页面的状态变化。
-
像第二个原因,我们有时需要定义一些观察者,比如HeroController,这个观察者与GalleryShell的子页面有关,我们必须为这个路由器定义它。例如,这就是GalleryShell的观察者。
-
我们可以简单的为每个路由器添加我们需要的特定小部件。例如,如果我们想改变GalleryShell子页面的文本比例,我们可以简单地用这个部件来包装GalleryShell,而不是用这个部件来包装每个子页面。
MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaleFactor: 4.0,
),
child: GalleryShell(),
);
我认为这些都是在大项目开始时制作嵌套路由器的好理由。
所以外层路由器应该位于/lib/presentation/routers,内层路由器应该位于/lib/presentation/screens/gallery。
如果你注意到了,我可以在通用页面中使用UnknownPage,但我没有,因为当我们从服务器提供者那里获得数据时,可能会发生404,所以我们应该在gallery Bloc中处理它,正如后面提到的。
外部路由器委托
画廊导航结构
这是带有外部和内部结构的Galley导航结构。我们到现在为止已经建立了RouteInformationParser和GalleryRoutersState,还有一些小步骤。
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:gallery/presentation/animations/page.dart';
import 'package:gallery/presentation/screens/gallery/gallery.dart';
import 'package:gallery/presentation/screens/login/login.dart';
import 'package:routers_service/routers_service.dart';
class GalleryRouterDelegate extends RouterDelegate<RouterConfiguration>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<RouterConfiguration> {
GalleryRouterDelegate(this.routerState)
: navigatorKey = GlobalObjectKey<NavigatorState>(routerState) {
routerState.addListener(notifyListeners);
}
final GalleryRoutersState routerState;
@override
final GlobalObjectKey<NavigatorState> navigatorKey;
@override
void dispose() {
routerState.removeListener(notifyListeners);
super.dispose();
}
@override
RouterConfiguration get currentConfiguration {
return RouterConfiguration(routerState.routePath, routerState.browserState);
}
@override
Future<void> setNewRoutePath(RouterConfiguration configuration) {
routerState.routePath = configuration.path;
routerState.browserState = configuration.browserState;
return SynchronousFuture<void>(null);
}
@override
Widget build(BuildContext context) {
assert(routerState.routePath != null);
return GalleryRouterStateScope(
routerState: routerState,
child: Navigator(
key: navigatorKey,
pages: [
if (routerState.routePath is LoginPagePath)
FadeAnimationPage(
key: const ValueKey(LoginPagePath),
child: const LoginPage(),
)
else
MaterialPage<dynamic>(
child: GalleryShell(routersState: routerState)),
],
onPopPage: (route, dynamic result) {
return route.didPop(result);
},
),
);
}
}
正如你所看到的,内部路由器与画廊外壳互动。外部路由器应该像另一个页面一样与这个画廊外壳互动。让我们看看外侧的gallery router类。
首先,它应该监听来自routerState的任何变化并覆盖一些方法。
- currentConfiguration,当路由信息可能发生变化时,基于routerState返回RouterConfiguration。
- setNewRoutePath应该分配来自routerInformationProvider的routerState数据。
- build,返回带有页面的导航器。这就是我们应该根据routerState定义我们的页面的地方。在这里,如果路径是LoginPagePath,页面就包含LoginPage,否则。GalleryShell。
如何将外部路由器连接到应用程序?只需将MaterialApp改为MaterialApp.router并添加routerDelegate和routeInformationParser。
内部路由器委托
Navigator(
key: navigatorKey,
observers: [heroController],
pages: [
if (_routerState.routePath is UnknownPagePath)
FadeAnimationPage(
key: const ValueKey(UnknownPagePath),
child: const UnknownPage(),
)
else ...[
if (_routerState.routePath is RootPagePath ||
_routerState.browserState!.galleriesHistory
.contains(RootPagePath.kRootPageLocation))
FadeAnimationPage(
key: const ValueKey(RootPagePath),
child: RootPage(
albumPath: RootPagePath.kRootPageLocation,
gallery: _routerState.browserState!.gallery,
),
),
if (_routerState.routePath is AlbumPagePath ||
_routerState.routePath is MediaFullscreenPath) ...[
for (String galleryPath
in _routerState.browserState!.galleriesHistory.where(
(gallery) => gallery != RootPagePath.kRootPageLocation))
FadeAnimationPage(
key: ValueKey(galleryPath),
child: AlbumPage(
albumPath: galleryPath,
gallery: _routerState.browserState!.gallery,
),
),
if (_routerState.routePath is MediaFullscreenPath)
FadeAnimationPage(
key: const ValueKey(MediaFullscreenPath),
child: MediaFullscreenPage(
albumPath: (_routerState.routePath as MediaFullscreenPath)
.albumPath,
mediaPath: (_routerState.routePath as MediaFullscreenPath)
.mediaPath,
media: _routerState.browserState!.media,
),
),
]
]
],
),
这是导航的核心,我们应该在这里根据routerState定义页面。这些情况是显而易见的。
- 如果请求是404,就直接显示404页面。
- 否则,如果请求的是RootPagePath或者用户之前看到的画廊之一是RootPagePath,那么第一个页面就是RootPage
- 如果请求是AlbumPagePath或MediaFullscreenPath,对于用户看过的每一个相册,我们都需要在下面的页面上进行展示
- 最后,如果请求是MediaFullscreenPath,我们有一个MediaFullscreenPage,在所有相册页面的顶部显示照片。
- 这时你应该通过添加以下小部件来添加你的页面过渡动画。这个动画是使用animationpackage的FadeThroughTransitio和FadeTransition的组合。这使得你的网络应用在页面过渡时更加优雅和流畅。你可以在图库中测试这个动画。
import 'package:animations/animations.dart';
class FadeAnimationPage extends Page<dynamic> {
final Widget child;
FadeAnimationPage({LocalKey? key, required this.child}) : super(key: key);
@override
Route createRoute(BuildContext context) {
return PageRouteBuilder<dynamic>(
settings: this,
transitionDuration: transitionDuration,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
},
reverseTransitionDuration: transitionDuration,
pageBuilder: (context, animation, animation2) {
var curveTween = CurveTween(curve: Curves.easeIn);
return FadeTransition(
opacity: animation.drive(curveTween),
child: child,
);
},
);
}
}
如何将外部路由器连接到应用程序?只需用Router包装GalleryShell.build,并添加内部routerDelegate。
添加到图库和用户界面
最后,我们达到了使这个导航成功的最后一步。
我们需要为画廊相册Bloc增加三个状态:successPushed、successPopped和notFound。正如我们之前计划的那样,我们需要以任何方式返回或前进,无论是命令式还是声明式。因此,当我们点击应用程序的返回按钮或点击相册按钮时,如果我们是以命令式的方式,我们有我们的信息。如果不是,我们就没有,我们应该请求画廊Bloc获得上一张或下一张专辑的流行和推送。如果服务器返回404状态代码,我们应该触发一个未找到的状态。
首先,我们还需要两个事件。
class GalleryPushRequested extends GalleryEvent {
const GalleryPushRequested(this.path);
final String path;
@override
List<Object?> get props => [path];
}
class GalleryPopRequested extends GalleryEvent {
const GalleryPopRequested();
}
然后,调用推送事件到Bloc,就像通过点击专辑按钮。
FloatingActionButton.extended(
heroTag: UniqueKey(),
onPressed: () {
context.read<GalleryBloc>().add(GalleryPushRequested(album.path));
},
icon: const Icon(Icons.photo_album),
label: Text(album.name),
),
然后,对于画廊Bloc中的推送事件,我们必须在Bloc中获得新专辑信息。
Stream<GalleryState> _mapGalleryPushRequestedToState(
GalleryPushRequested event) async* {
yield state.copyWith(status: GalleryStatus.loading);
try {
final gallery = await galleryRepository.getGallery(path: event.path);
yield state.copyWith(
status: GalleryStatus.successPushed,
pushedGallery: gallery,
);
} on NotFoundException {
yield state.copyWith(status: GalleryStatus.notFound);
} on SocketException {
yield state.copyWith(status: GalleryStatus.failure);
} on Exception {
yield state.copyWith(status: GalleryStatus.failure);
}
}
我们需要同样的东西来处理弹出事件。如果你记得,我们需要另一个404导航,我称之为可能的404导航。这就是可能的404发生的地方,比如说像这样的东西。/gallery/album/post_malone?view=sunflower
最后,你应该在相册页面中用这个监听器处理状态变化。
listener: (context, state) async {
switch (state.status) {
case GalleryStatus.successPushed:
assert(state.pushedGallery != null);
final pushedGallery = state.pushedGallery!;
final newState = browserState.rebuild(
(b) => b
..galleriesHistory.add(pushedGallery.current)
..gallery = pushedGallery.toBuilder(),
);
final newPath =
pushedGallery.current == RootPagePath.kRootPageLocation
? const RootPagePath()
: AlbumPagePath(pushedGallery.current);
GalleryRouterStateScope.of(context)!
..routePath = newPath
..browserState = newState;
break;
case GalleryStatus.successPopped:
assert(state.poppedGallery != null);
final poppedGallery = state.poppedGallery!;
final newState = browserState.rebuild(
(b) => b
..galleriesHistory.removeLast()
..galleriesHistory.add(
poppedGallery.parent == null
? RootPagePath.kRootPageLocation
: poppedGallery.current,
)
..gallery = poppedGallery.toBuilder(),
);
final newPath = newState.galleriesHistory.last ==
RootPagePath.kRootPageLocation
? const RootPagePath()
: AlbumPagePath(poppedGallery.current);
GalleryRouterStateScope.of(context)!
..routePath = newPath
..browserState = newState;
break;
case GalleryStatus.notFound:
GalleryRouterStateScope.of(context)!
..browserState = BrowserState()
..routePath = const UnknownPagePath();
break;
default:
break;
}
},
看看这段代码吧。你可以弄清楚我做了什么。这很简单,只是用新的信息和新的routePath改变routeState。
让我们看看。
-
on GalleryStatus.successPushed 首先,我们得到了请求的专辑信息,并将其添加到galleryHistory中,然后我们必须检查专辑页是RootPagePath还是AlbumPagePath(根页面是第一个我们不知道其路径的专辑页),最后,我们现在可以用新的routePath和browserState改变GalleryRouterState。
-
在GalleryStatus.successPopped时,我们可以像以前那样做,只是新的页面是以前的页面,如果我们没有那个页面,我们的Bloc就会为我们找到它,现在我们应该从galleryHistory中删除最后的页面,并添加新的页面。
-
在GalleryStatus.notFound上,我们只需要告诉状态这是一个未知的页面路径,并为我们清理浏览器状态。
这就是了。这是导航仪例子中最难的一个,我们有相同的屏幕用于推送和弹出。对于其他屏幕如FullScreenPage,你只需要处理notFound的情况。
两个例子
强制性的方式
/gallery/ -> /gallery/album/album2 -> /gallery/album/album2?view=photo8
galleriesHistory=['', 'album2'] 而我们的请求是MediaFullscreenPath,所以页面是。
RootPage or AlbumPage(Album1) -> AlbumPage(Album2) -> MediaFullscreenPage(Photo8)
在这种情况下。
- 我们没有任何RootPage的数据,所以BrowserState.gallery是空的,我们应该通过调用GetGalleryRequested事件告诉gallery Bloc去获取它们。
//Inner Router
FadeAnimationPage(
key: const ValueKey(RootPagePath),
child: RootPage(
albumPath: RootPagePath.kRootPageLocation,
gallery: _routerState.browserState!.gallery, //this is null
),
),
// Album Page
if (state.status == GalleryStatus.initial) {
BlocProvider.of<GalleryBloc>(context)
.add(GetGalleryRequested(albumPath));
}
- 在获得第一张专辑数据后,用户点击Album2按钮,然后我们应该通过调用GalleryPushRequested事件和改变监听器中的GalleryRouterState来告诉画廊Bloc获得它们,然后我们将数据传递给它在Inner Router中的小部件。
//Listener
final newState = browserState.rebuild(
(b) => b
..galleriesHistory.add(pushedGallery.current)
..gallery = pushedGallery.toBuilder(),
);
GalleryRouterStateScope.of(context)!
..routePath = newPath
..browserState = newState;
//Inner Router
FadeAnimationPage(
key: ValueKey(galleryPath),
child: AlbumPage(
albumPath: galleryPath,
gallery:_routerState.browserState!.gallery, //this contains data
),
),
- 现在我们已经有了关于Album2的信息,所以我们需要跳过在Bloc中获取数据。
Stream<GalleryState> _mapGetGalleryRequestedToState(
GetGalleryRequested event) async* {
if (galleryRepository.gallery != null) {
yield state.copyWith(
status: GalleryStatus.success,
gallery: galleryRepository.gallery!,
);
return;
}
}
- 我们也已经有了Photo8的信息,所以BrowserState.media有Photo8的数据,然后我们把数据传递给Inner Router中的小部件。
// on album button pressed
final browserState =
GalleryRouterStateScope.of(context)!.browserState!;
GalleryRouterStateScope.of(context)!
..routePath = newPath
..browserState = newState;
//Inner Router
FadeAnimationPage(
key: const ValueKey(MediaFullscreenPath),
child: MediaFullscreenPage(
albumPath: (_routerState.routePath as MediaFullscreenPath)
.albumPath,
mediaPath: (_routerState.routePath as MediaFullscreenPath)
.mediaPath,
media: _routerState.browserState!.media, //this contains data
),
),
声明性方式
/gallery/album/album2?view=photo8
galleriesHistory=['album2'] 而我们的请求是MediaFullscreenPath,所以页面是。
AlbumPage(Album2) -> MediaFullscreenPage(Photo8)
在这种情况下。
- 我们只需要获取 album2 和 photo8 的数据。而我们没有任何数据,因为我们没有任何先前的状态,我们同时调用AlbumPage(Album2)和MediaFullscreenPage(Photo8),所以它们同时调用它们的事件。
//Inner Router
//Album Page
FadeAnimationPage(
key: const ValueKey(RootPagePath),
child: RootPage(
albumPath: RootPagePath.kRootPageLocation,
gallery: _routerState.browserState!.gallery, //this is null
),
),
//MediaFullscreen Page
FadeAnimationPage(
key: const ValueKey(MediaFullscreenPath),
child: MediaFullscreenPage(
albumPath: (_routerState.routePath as MediaFullscreenPath)
.albumPath,
mediaPath: (_routerState.routePath as MediaFullscreenPath)
.mediaPath,
media: _routerState.browserState!.media, //this is null
),
),
// Album Page
if (state.status == GalleryStatus.initial) {
BlocProvider.of<GalleryBloc>(context)
.add(GetGalleryRequested(albumPath));
}
//MediaFullscreen Page
if (state.status == FullscreenStatus.initial) {
BlocProvider.of<FullscreenBloc>(context)
.add(FullscreenPushRequested(
albumPath: widget.albumPath,
mediaPath: widget.mediaPath,
));
}
现在,应用程序向用户显示这两个页面。 注意。在这种情况下,因为我们没有任何关于RoutersState的信息,如果用户想去相册2的前一个相册,也就是RootPage或AlbumPage(Album1),Bloc应该首先通过父相册的路径获取Album1的数据,然后将用户导航到前一个相册。然而,在第一种情况下,由于我们有信息并将其存储在RoutersState中,我们只需要弹出页面并显示前一个专辑。
最后的话
这篇文章向你展示了实现一个新的导航系统的说明,以及Flutter的强大。这个教程是由你问的,你对画廊项目感兴趣。我希望你喜欢它,并以极大的精力阅读它。
说实话,激起我用每一个细节来说明的主要原因是,我花了近一周的时间来阅读框架代码,并试图用最好的方式来实现它。
如果这里有什么遗漏或者有什么需要改变的地方,请尝试做一个新的讨论。我真的很喜欢和这个神奇的flutter社区互动。
源代码和网络应用程序的链接在下面。试着玩玩网络应用程序,请求不同的URL,如果有什么不对,请提交一个问题。
所有相关的Navigator 2代码为您提供方便。
- routers_service: routes, parser, state_model, routers_state
- outer_router
- app_router_widget
- inner_router
- Gallery_shell
- gallery_push:事件、状态、模块、用法、监听器
- gallery_pop:事件、状态、单元、使用情况、监听者
- gallery_not_found:状态, bloc, 监听者
- fullscreen_push:事件、状态、单元格、直接使用、间接使用
- fullscreen_not_found:状态, bloc, 听众
注意安全,请继续关注下一篇文章。
通过www.DeepL.com/Translator(免费版)翻译