Original address: medium.com/flutter-com…

Aliyazdi75.medium.com/

Release date: April 15, 2021

Do you know that Flutter Web has been stabilized? Do you want to support Flutter Web for your application? Are you struggling to support URL navigation like a real web application? Here’s your ticket for your super app support Navigator 2.

Bunker Hill, California, USA. Photo: Shabdro Photo

Really appreciate Dai Chunheng he helped me by reading this article and checking its quality.


This is an amazing feature of Gallery, and if you haven’t read my previous articles for Gallery, you can start your journey here.

This is a long article because I’ve gathered everything you might need to know about the Navigator 2 implementation. Just reading this article probably won’t help, so try a new project and take it one step at a time like this, and don’t forget to bring some snacks.

Let’s see the results first.

Sample navigator 2 in the browser

I hope you like the photos I chose

I love everything about Pian-pian, the people, the events, the community. That’s that’s why I made this gallery to show how cute they are. I like to add the album you requested to make this gallery even more beautiful. If you want to do so, check out this database.

As you can see, our gallery supports many situations.

  • Imperative navigation
  • Declarative navigation
  • Possible and impossible 404 navigation
  • Browser back button navigation
  • Browser forward button navigation

That’s really cool, that’s not a lot, that’s all there is to it! Let’s learn together.

Declarative and mandatory navigation Compulsive navigation

I’m sure most of you are familiar with declarative UI programming, as we’ve always used with Flutter widgets. For example, to define a widget called ViewB, you tell the framework.

// Declarative style
return ViewB(
  color: red,
  child: ViewC(...),
)
Copy the code

But in an imperative way.

// Imperative style
b.setColor(red)
b.clearChildren()
ViewC c3 = new ViewC(...)
b.add(c3)
Copy the code

Our gallery has two main screens that we want to present to the user. AlbumPage and MediaFullscreen. It also has a RootPage, which is actually our first photo album page.

I made this navigation easy with the GIF below.

Declarative and forced navigation

The first boy wants to find his friend’s photo, but he doesn’t know which album it belongs to, so he should start asking each dog representing the gallery album.

Our second boy! There was some information about his friend’s photo, and the first dog was told the specific address of which album the photo belonged to. Therefore, he should not waste time and go straight into the album.

The first boy does the same thing we do in mobile apps. We start our application from scratch and navigate page by page to other screens. However, things can be different in web applications, and we should use Navigator 2 to handle them.

Why Navigator 2?

The Navigator 2.0 API adds new classes to the framework to make the application screen a function of application state and to provide the ability to resolve routes from the underlying platform, such as network urls.

Navigator 2 introduces some new methods (you can read about them in this article) that make navigation more difficult, but not complicated.

It’s important that you don’t complicate your application by adding new packages for new features. That’s why I like the way Flutter gives you a lot of tools that you can use to support new features while still making your application easy to understand.

In this article, I will explain these.

  • Gallery Router service. Define routing and router state models and make parsers and states
  • Gallery Router Delegates: Fabrication of exterior and interior routers
  • Gallery Blocks: How do I change the state of my router based on block events

So, let’s get started.

Gallery navigation structure and data model

So far, we’ve learned our requirements and desires for cross-platform applications, and now we can start implementing this amazing feature. If you read the article I mentioned earlier, we need to define a new service called Routers_service (if you don’t want to read it, do all the classes where you want to), which we do with the following golden command.

$ cd services
$ mkdir routers_service
$ cd routers_service
$ flutter pub global run stagehand package-simple
Copy the code

We need to define a simple model for browser state, so we need model dependencies and gallery_service, but we don’t need Bloc dependencies here, because we can handle our router state changes via ChangeNotifiereasily, But I will use Bloc in the presentation layer to change the router state. Its dependencies look like this.

dependencies:
  flutter:
    sdk: flutter
  model_dependency_service:
    path: ../dependencies_service/model_dependency_service
  gallery_service:
    path: ../gallery_service
Copy the code

Finally, don’t forget to add this service to your application by adding it to pubspec.yaml.

dependencies:
  routers_service:
    path: services/routers_service
Copy the code

Define the routing

This service is responsible for the changes of the routers and defines all our routes in the gallery and resolves them to the display layer. So we have three files, state, routers and Parser. Routes extend from an abstract class called GalleryRoutePath, which carries everything you need to know about routing paths. For example, one of the full-screen paths /gallery/#/album/album2? View = Photo8 tells our application that the client wants to access an ‘album2’ album and see a ‘photo8’ photo, so MediaFullscreenPath should contain all of this.

class MediaFullscreenPath extends GalleryRoutePath {
  static const String kFullscreenPageLocation = 'view';

  const MediaFullscreenPath(this.albumPath, this.mediaPath);

  final String albumPath;
  final String mediaPath;
}
Copy the code

We did the same for the rest of the route. You can find them here.

How to make parser and browser state?

We’ll make a parser, a one-way bridge from the operating system to our application presentation layer. Thanks to the RouteInformationParser class, not only can we get the URI of the request, but we can also pass navigationState and get them from the browser for the next route. Before making the parser, let’s define the browser state.

Gallery page structure

Browser status for the necessary navigation, was supposed to be included from the beginning to the current line of what all pages need, as you can see in the gallery page structure, ‘album’ albums for path, ‘photos’ for full screen photo page, for example, to display the’ photo 8 ‘photos page, we need to have all below the photo gallery page, So we need a list of gallery paths we’ve visited so far, as well as current albums or photos. The current album or photos can be empty if they were obtained from a URL, and not empty if they were obtained from a previous album page.

To make this clear, if we are in an album, then we have got the details of the album or photo. Otherwise, we don’t have the same information as the urls we get from the browser, so they’re empty. Let’s conclude with this model.

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;
}
Copy the code

Remember, you have to pass browser state to the browser as JSON, so you need toJson and fromJson functions to handle the model.

What should the data model look like?

To provide data for the status model and details of albums and photos, you can get them from the API. I set up two API providers, the server and the Github content API. You can choose whatever you want. In this article.

Pay attention to. I’ll use the Github API as the provider to get images from the gallery-Asset library. In the browser, you’ll see a hexadecimal string like 2f616c62756d32 instead of the album name, because I need to get the full path to the album, like /album2/album3/image.jpg. But don’t worry, I’ll use the album and photos keywords throughout this tutorial to avoid blurring things. You can view a sample of all the responses here.

If you remember, we need two more requests from the gallery, which are.

  • Browser returns to button navigation
  • Browser forward button navigation

Therefore, we should change the gallery page structure to look like this.

The final gallery page structure

Therefore, you can conclude that we need every album to have parents and children’s albums. Let’s take a look at the model of the gallery album.

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) {
    returnserializers.fromJson(Gallery.serializer, serialized)! ; }static Serializer<Gallery> get serializer => _$gallerySerializer;
}
Copy the code

In this model, I use the keyword “Gallery” for the current Album and “Album” for its child albums to distinguish them, so don’t confuse them.

Why do we need to get the current path? To launch our application, the user and our application don’t know about the root album’s first appearance, so we need to get it from the server or provider to keep it in our gallery history, and parent is nullable because we don’t have a parent album for the root album.

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) {
    returnserializers.fromJson(Media.serializer, serialized)! ; }static Serializer<Media> get serializer => _$mediaSerializer;
}
Copy the code

We should do the same for media or photo models. We need a property called path to get information from the provider.

To sum up, you need two main Endpoints to get everything going.

  • Gallery details endpoint provider
  • Photo detail endpoint provider

Therefore, if you need to do the latter functionality, you must make sure it is compatible with your backend provider.

Gallery navigation parser

We should build all these things to do this parser. We need to extend and override two main functions from RouteInformationParser.

  • Use parseRouteInformation to get the URI and browser state from the operating system.
  • Use Restorerout Information to pass the URI and browser state to the operating system.

Parsing Routing Information

The definitions are obvious, and their implementation is super simple, but here’s a little tip: First, take a look at the Gallery’s 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) {
      finalnewState = 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];
          finalnewState = 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/${}/'
          returnSynchronousFuture<RouterConfiguration>( RouterConfiguration(AlbumPagePath(albumPath), newState)); }}/ / 404
    return SynchronousFuture<RouterConfiguration>(
        RouterConfiguration(constUnknownPagePath(), browserState)); }}Copy the code

I split the URI into three main sections by length, and the two sections are the two main pages. Album and full screen photos. We should use SynchronousFuture in a wrapper class called RouterConfiguration to return the gallery path and state for each fragment, because the results can be computed synchronously. This RouterConfiguration class is used by the router delegate in the demo I explain later.

What’s the trick here? The trick is that we should provide browser state data in two ways: here and from the presentation layer. Because for declarative navigation, we need browser state data, and this function is all the data we get from the URI, for example, albums and photo paths. Be careful to do that. For example, if we have a request from a user to navigate to an album and a full-screen page, we should do the following.

final albumPath = uri.pathSegments[1];
final filePath =
   uri.queryParameters[MediaFullscreenPath.kFullscreenPageLocation];
finalnewState = browserState.galleriesHistory.contains(albumPath) ? browserState : browserState.rebuild((b) => b.. galleriesHistory.add(albumPath));Copy the code

Pay attention to check whether this state before being added to the presentation layer in the browser status, through this code: browserState. GalleriesHistory. The contains (AlbumPath).

Another thing to note is that if none of the paths is acceptable, an unknown page should be returned.

/ / 404
return SynchronousFuture<RouterConfiguration>(
    RouterConfiguration(const UnknownPagePath(), browserState));
Copy the code

This is what I call impossible 404 navigation, because URI simply can not accept, such as/gallery/my/albums/album2 / hello/such a thing! For the possible, I’ll explain later, because it should be handled at the presentation level.

Recovering Route Information

The next method, restoreRouteInformation, must return RouteInformation, which contains location, which is the specific location path, and state, which is our browser state, passing it to JSON. The result is this.

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; }}Copy the code

Gallery navigation router status

This class should be used for state handling by the presentation layer, which means it holds the same BrowserState and notifies the presentation layer when it is changed by the presentation layer.

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(); }}}Copy the code

The routePath attribute is the current application path, which can be one of our routes, such as UnknownPagePath, LoginPagePath, RootPagePath, AlbumPagePath, and MediaFullscreenPath. We should change this whenever the path type of our application needs to be changed.

Ok, our router service is complete. Now we are ready to jump to the demo layer and work with the page presented to our lovely users.

Gallery navigation display layer

At this level, we should use the RouterDelegate provided by the Flutter framework. As I always say, we have to make our application easy to understand, because processing all the pages in one class at the same time would be hard to understand, wouldn’t it? So, let’s split our gallery presentation Router into two separate RouterDelegates, the Outer Router for our generic page, like LoginPage, The Inner Router is used for RootPage, AlbumPage, MediaFullscreen, and UnknownPage, all of which are screens for galleries. However, this simple question is skipped.

Why is it nested by?

  • As mentioned in flutter/uxr#35, this is useful for multiple teams working on the same project, built in a hierarchical manner, which makes the project less complex. For example, in the gallery, we have two main simple screens. LoginPage and GalleryShell, because there is no relationship between them, are separated in their directory presentation layer.

GalleryShell has several sub-pages. – RootPage (first album page). – Phase page – FullscreePage

  • We should think of changing our state as a related change in the onPopPage method, so it’s not a good idea to put all the changes in one router that are unrelated. For example, here’s the GalleryShell’s onPopPage method, which handles state changes for its child pages.

  • For the second reason, we sometimes need to define some observer, such as the HeroController, which is associated with the child page of the GalleryShell, and we must define it for the router. For example, this is the GalleryShell observer.

  • We can simply add the specific widgets we need for each router. For example, if we wanted to change the text ratio of the GalleryShell subpage, we could simply wrap the GalleryShell with this widget instead of wrapping each subpage with this widget.

MediaQuery(
   data: MediaQuery.of(context).copyWith(
     textScaleFactor: 4.0,
   ),
   child: GalleryShell(),
);
Copy the code

I think these are all good reasons to make nested routers at the start of a big project.

So the outer router should be located in/lib/presentation/routers, inner router should be located in/lib/presentation/screens/gallery.

If you noticed, I could have used UnknownPage in the generic page, but I didn’t, because 404 could happen when we get the data from the server provider, so we should process it in gallery Bloc, as mentioned later.

External router delegate

Gallery navigation structure

This is the Galley navigation structure with external and internal structures. We’ve built RouteInformationParser and GalleryRoutersState so far, and there are a few small steps.

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) {
          returnroute.didPop(result); },),); }}Copy the code

As you can see, the internal router interacts with the gallery shell. The external router should interact with the gallery shell just like another page. Let’s look at the gallery Router class on the outside.

First, it should listen for any changes from routerState and override some methods.

  • CurrentConfiguration: Returns RouterConfiguration based on routerState when route information may change.
  • SetNewRoutePath should be allocated from routerInformationProvider routerState data.
  • Build, returns the navigator with the page. This is where we should define our page in terms of routerState. In this case, if the path is LoginPagePath, the page contains LoginPage, otherwise. GalleryShell.

How do I connect an external router to an application? Simply change the MaterialApp to MaterialApp.router and add routerDelegate and routeInformationParser.

Internal router delegate

Navigator(
  key: navigatorKey,
  observers: [heroController],
  pages: [
    if (_routerState.routePath is UnknownPagePath)
      FadeAnimationPage(
        key: const ValueKey(UnknownPagePath),
        child: const UnknownPage(),
      )
    else. [if (_routerState.routePath isRootPagePath || _routerState.browserState! .galleriesHistory .contains(RootPagePath.kRootPageLocation)) FadeAnimationPage( key:constValueKey(RootPagePath), child: RootPage( albumPath: RootPagePath.kRootPageLocation, gallery: _routerState.browserState! .gallery, ), ),if (_routerState.routePath is AlbumPagePath ||
          _routerState.routePath isMediaFullscreenPath) ... [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 asMediaFullscreenPath) .mediaPath, media: _routerState.browserState! .media, ), ), ] ] ], ),Copy the code

This is the heart of the navigation, where we should define the page according to routerState. These situations are obvious.

  • If the request is 404, the 404 page is displayed.
  • Otherwise, if the request is RootPagePath or one of the galleries the user saw before was RootPagePath, the first page is RootPage
  • If the request is AlbumPagePath or MediaFullscreenPath, we need to display each album the user has seen on the following page
  • Finally, if the request is MediaFullscreenPath, we have a MediaFullscreenPage that displays the photos at the top of all the album pages.
  • At this point you should add your page transition animation by adding the following widget. This animation is a combination of FadeThroughTransitio and FadeTransition using animationPackage. This makes your web application more elegant and smooth in page transitions. You can test this animation in the gallery.
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);
        returnFadeTransition( opacity: animation.drive(curveTween), child: child, ); }); }}Copy the code

How do I connect an external router to an application? Simply wrap galleryshell. build with a Router and add an internal routerDelegate.

Add to the gallery and user interface

Finally, we reached the final step to make this navigation a success.

We need to add three states to the gallery album Bloc: successPushed, successPopped, and notFound. As we had planned, we needed to return or advance in any way, whether imperative or declarative. So when we click the app’s back button or click the album button, if we do it in an imperative way, we have our information. If not, we don’t and we should ask gallery Bloc to get pop and push on the last or next album. If the server returns a 404 status code, we should trigger an unfound status.

First, we need two more events.

class GalleryPushRequested extends GalleryEvent {
  const GalleryPushRequested(this.path);

  final String path;

  @override
  List<Object?> get props => [path];
}

class GalleryPopRequested extends GalleryEvent {
  const GalleryPopRequested();
}
Copy the code

Then, call the push event to Bloc, as if by clicking the album button.

FloatingActionButton.extended(
  heroTag: UniqueKey(),
  onPressed: () {
    context.read<GalleryBloc>().add(GalleryPushRequested(album.path));
  },
  icon: const Icon(Icons.photo_album),
  label: Text(album.name),
),
Copy the code

Then, for the push event in gallery Bloc, we have to get the new album information in 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 {
      yieldstate.copyWith(status: GalleryStatus.failure); }}Copy the code

We need the same thing to handle pop-up events. If you remember, we need another 404 navigation, what I call a possible 404 navigation. This is where a possible 404 occurs, for example, something like this. /gallery/album/post_malone? view=sunflower

Finally, you should use this listener on the album page to handle state changes.

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; }},Copy the code

Take a look at this code. You can figure out what I did. This is simple, just changing the routeState with the new information and the new routePath.

Let’s see.

  • On GalleryStatus. SuccessPushed first of all, we got the request of album information, and add it to the galleryHistory, Then we have to check whether the album page is RootPagePath or AlbumPagePath (the root page is the first album page whose path we don’t know), and finally, we can now change the GalleryRouterState with the new routePath and browserState.

  • In GalleryStatus. SuccessPopped, we can do it as before, just before a new page is page, if we don’t have that page, our Bloc will find it for us, now we should be deleted from the galleryHistory last page, and add a new page.

  • On galleryStatus.notFound, we simply tell the status that this is an unknown page path and clean up the browser state for us.

Here it is. This is the hardest one in the navigator example, we have the same screen for push and pop. For other screens like FullScreenPage, you only need to deal with notFound cases.

Two examples

Coercive way

/gallery/ -> /gallery/album/album2 -> /gallery/album/album2? view=photo8

GalleriesHistory =[“, ‘album2’] and our request is MediaFullscreenPath, so the page is.

RootPage or AlbumPage(Album1) -> AlbumPage(Album2) -> MediaFullscreenPage(Photo8)
Copy the code

In this case.

  • We don’t have any RootPage data, so browserState. gallery is empty, we should tell gallery Bloc to get them by calling GetGalleryRequested event.
//Inner Router
FadeAnimationPage(
  key: constValueKey(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));
}
Copy the code
  • After getting the first album data, the user clicks the Album2 button, then we should tell the gallery Bloc to get them by calling the GalleryPushRequested event and changing the GalleryRouterState in the listener, We then pass the data to its widget in the Inner Router.
//Listener
finalnewState = browserState.rebuild( (b) => b .. galleriesHistory.add(pushedGallery.current) .. gallery = pushedGallery.toBuilder(), ); GalleryRouterStateScope.of(context)! . routePath = newPath .. browserState = newState;//Inner RouterFadeAnimationPage( key: ValueKey(galleryPath), child: AlbumPage( albumPath: galleryPath, gallery:_routerState.browserState! .gallery,//this contains data),),Copy the code
  • Now we have information about Album2, so we need to skip the data in Bloc.
Stream<GalleryState> _mapGetGalleryRequestedToState(
    GetGalleryRequested event) async* {
  if(galleryRepository.gallery ! =null) {
    yield state.copyWith(
      status: GalleryStatus.success,
      gallery: galleryRepository.gallery!,
    );
    return; }}Copy the code
  • We also have information for Photo8, so BrowserState.media has data for Photo8, which we then pass to the widget in the Inner Router.
// on album button pressed
finalbrowserState = 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 asMediaFullscreenPath) .mediaPath, media: _routerState.browserState! .media,//this contains data),),Copy the code

Declarative mode

/gallery/album/album2? view=photo8

GalleriesHistory =[‘album2’] and our request is MediaFullscreenPath, so the page is.

AlbumPage(Album2) -> MediaFullscreenPage(Photo8)
Copy the code

In this case.

  • We just need to get the album2 and photo8 data. And we don’t have any data, because we don’t have any prior state, we call AlbumPage(Album2) and MediaFullscreenPage(Photo8) at the same time, so they call their events at the same time.
//Inner Router
//Album Page
FadeAnimationPage(
  key: constValueKey(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 asMediaFullscreenPath) .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,
  ));
}
Copy the code

The application now displays both pages to the user. Pay attention to. In this case, since we don’t have any information about RoutersState, if the user wants to go to the previous album in album 2, namely RootPage or AlbumPage(Album1), Bloc should first get the Album1 data from the parent album path, The user is then navigated to the previous album. However, in the first case, since we have the information and store it in RoutersState, we just need to pop up the page and display the previous album.

The last word

This article shows you how to implement a new navigation system and how powerful Flutter is. This tutorial was asked by you, who are interested in gallery projects. I hope you enjoy it and read it with great energy.

Honestly, the main reason that inspired me to go into every detail was that I spent nearly a week reading the framework code and trying to implement it the best way I could.

If there is something missing here or something that needs to be changed, please try a new discussion. I really enjoy interacting with this amazing Flutter community.

The source code and links to the web application are below. Try playing around with web apps, asking for different urls, and submitting a question if something is wrong.

All the relevant Navigator 2 code is for your convenience.

  • routers_service: routes, parser, state_model, routers_state
  • outer_router
  • app_router_widget
  • inner_router
  • Gallery_shell
  • Gallery_push: event, state, module, usage, listener
  • Gallery_pop: event, status, unit, usage, listener
  • Gallery_not_found: state, bloc, listener
  • Fullscreen_push: events, states, cells, direct use, indirect use
  • Fullscreen_not_found: state, bloc, listener

Stay safe and stay tuned for the next article.

Source: github.com/aliyazdi75/…

Products: aliyazdi. Tech/gallery /


Translation via www.DeepL.com/Translator (free version)