[Flutter翻译]让我们制作Flutter导航仪2

335 阅读21分钟

原文地址:medium.com/flutter-com…

原文作者:aliyazdi75.medium.com/

发布时间:2021年4月15日

您知道 Flutter web 已经稳定了吗?您想为您的应用程序支持 Flutter web 吗?您是否在为支持像真正的网络应用一样的URL导航而奋斗?这是您的票,为您的超强应用支持导航器2。

image.png

美国加利福尼亚州邦克山,照片:Shabdro Photo

真正欣赏戴春恒他通过阅读这篇文章并检查其质量来帮助我。


这是图库的一个神奇功能,如果你没有读过我以前为图库写的文章,你可以从这里开始你的旅程。

这是一篇很长的文章,因为我收集了你可能需要知道的关于navigator 2实现的所有信息。光看这篇文章可能没有用,所以试着做一个新的项目,像这样一步一步地实现它,别忘了带些零食。

让我们先看看结果。

1.gif

浏览器中的样本导航器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让这个导航变得简单。

2.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,并从浏览器中得到它们,以便进行下一个路由。在制作解析器之前,让我们来定义这个浏览器状态。

image.png

图库页面结构

浏览器状态用于必要的导航,应该包含从开始到当前路线的所有页面所需要的东西,正如你在画廊的页面结构中看到的,'相册'用于相册路径,'照片'用于全屏照片页面,例如,要显示'照片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。但不要担心,我将在本教程中使用专辑和照片的关键字,以避免使事情变得模糊不清。你可以在这里查看所有的回复样本。

如果你还记得,我们还需要两个来自画廊的需求,那就是。

  • 浏览器返回按钮导航
  • 浏览器前进按钮导航

因此,我们应该把图库页面结构改成这样。

image.png

最终的图库页面结构

因此,你可以得出结论,我们需要每个相册都有父辈和子辈的相册。让我们来看看图库相册的模型。

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中提到的,这对于多个团队在同一个项目中工作是很有用的,以分层结构的方式来构建,这使得项目不那么复杂。例如,在画廊中,我们有两个主要的简单屏幕。LoginPageGalleryShell,因为它们之间没有关系,所以在它们的目录展示层中被分开。 GalleryShell有一些子页面。

  • 我们应该把改变我们的状态看作是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中处理它,正如后面提到的。

外部路由器委托

image.png

画廊导航结构

这是带有外部和内部结构的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并添加routerDelegaterouteInformationParser

内部路由器委托

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, 听众

注意安全,请继续关注下一篇文章。

源码:github.com/aliyazdi75/…

产品:aliyazdi.tech/gallery/


通过www.DeepL.com/Translator(免费版)翻译