2022-10-17

[Flutter] Katana Router

Hello. My name is Masaru Hirose.

I tried using go_router and auto_route, and other packages with Flutter, but it didn’t feel right, so I created my own routing package.

In a nutshell,

This is an auto_route that allows you to write routing information in the same file as the widget for the page.

With less code, you can use build_runner to perform code generation and implement type-safe routing.

In addition, routing information can be included in the same file as the widget, so it is no longer necessary to add routing information to a dedicated file each time a new page is written.

I’ve put together some instructions on how to use it, so if you’re interested, go ahead and give it a try!

katana_router

A package to automatically create routing configurations with build_runner to enable type-safe routing.
https://pub.devhttps://pub.dev
title
Building system for katana router packages. Automatic Page route generation.
https://pub.devhttps://pub.dev
title

Introduction

Flutter's Routing and Navigator are easy to use, and you can take advantage of great packages like go_router and auto_route, but each has some inconveniences.

  • Deep linking cannot be used if routing is done using the push method or other methods in the Route class.
  • If you use deep linking (e.g. pushNamed), you need to write the routing path directly as a String. It is also necessary to know the parameters in advance.
  • Ability to redirect (e.g. AuthGuard) to the condition source.
  • After creating the widget for a page, it is necessary to add the settings for routing, which requires editing two Dart files to create one page.

Therefore, I created a package with Generator that even creates files for routing just by adding Annotation to the Widget for the page.

This package has the following features

  • Deep linking available.
    • Deep linking parameters available.
  • Navigation and parameters are available in type safe.
  • Widget can be used as is.
  • Widgets can be used as pages with a small number of lines.
  • All pages defined in the application can be understood and router classes can be automatically created.
  • Supports nested and tabbed navigation.
    • Various parameters can be updated according to the current page status.

This package can implement routing configuration as shown in the example below.

Create Page

// home.dart

import 'package:katana_router/katana_router.dart';
import 'package:flutter/material.dart';

part 'home.page.dart';

@PagePath("/")
class HomePage extends StatelessWidget {
  const HomePage();

  @pageRouteQuery
  static const query = _$HomePageQuery();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Home")),
      body: Center(
        child: Text("Home page"),
      ),
    );
  }
}

Router Creation

// main.dart

import 'package:katana_router/katana_router.dart';
import 'package:flutter/material.dart';

import 'main.router.dart';

@appRoute
final appRouter = AutoRouter();

void main() {
  runApp(const MainPage());
}

class MainPage extends StatelessWidget {
  const MainPage();

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: appRouter,
      title: "Test App",
    );
  }
}

Navigation

// push
context.router.push(HomePage.query());
// pop
context.router.pop();

Installation

Import the following package for code generation using build_runner.

flutter pub add katana_router
flutter pub add --dev build_runner
flutter pub add --dev katana_router_builder

Implementation

Create Page

Widget to display by page path/user/any user ID is implemented as follows.

part '(original filename).page.dart'; to import a Part file.

Define the Widget as a page in @PagePath("path name") Annotation.

You can create a query for page transitions by defining a query field by giving @pageRouteQuery annotation.

Specify _$(widget class name)Query for the query value.

Widget parameters can be defined as they are.

// user.dart

import 'package:katana_router/katana_router.dart';
import 'package:flutter/material.dart';

part 'user.page.dart';

@PagePath("/user/:user_id")
class UserPage extends StatelessWidget {
  const UserPage({
    @PageParam("user_id") required this.userId,
    super.key,
  });

  final String userId;

  @pageRouteQuery
  static const query = _$UserPageQuery();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("User")),
      body: Center(
        child: Text("User id: $userId"),
      ),
    );
  }
}

Router Creation

import '(original filename).router.dart'; to import library files.

To create a router, grant @appRoute Annotation with a top-level value.

Also, put the AutoRouter object in its value.

Give that value directly to the routerConfig in MaterialApp.router. This will automatically pass the routing information to the application.

// main.dart

import 'package:katana_router/katana_router.dart';
import 'package:flutter/material.dart';

import 'main.router.dart';

@appRoute
final appRouter = AutoRouter();

void main() {
  runApp(const MainPage());
}

class MainPage extends StatelessWidget {
  const MainPage({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: appRouter,
      title: "Test App",
    );
  }
}

Manual Router Use

Even if you do not create an AutoRouter, you can use AppRouter to create a router.

In this case, pass the query for the page you want to register to pages.

Use this function when automatic router creation does not work, or when you want to limit page registrations.

// main.dart

import 'package:katana_router/katana_router.dart';
import 'package:flutter/material.dart';
import 'pages/home.dart';
import 'pages/edit.dart';
import 'pages/detail.dart';

@appRoute
final appRouter = AppRouter(
  pages: [
    HomePage.query,
    EditPage.query,
    DetailPage.query,
  ],
);

void main() {
  runApp(const MainPage());
}

class MainPage extends StatelessWidget {
  const MainPage({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: appRouter,
      title: "Test App",
    );
  }
}

Navigation

To navigate to HomePage, do the following

The parameters defined in the widget can be described in the query as they are.

// push
context.router.push(UserPage.query(userId: "User id"));
// pop
context.router.pop();

Code Generation

Automatic code generation is performed by entering the following command.

flutter pub run build_runner build --delete-conflicting-outputs

Additional Usage

Initial Page Setup

The initial page when the application is launched is the page associated with the / path.

If you wish to change this, you can do so by specifying the initialPath when creating the created AppRouter or AutoRouter object.

@appRoute
final appRouter = AutoRouter(
  initialPath: "/landing"
);

If initialQuery is specified, it is possible to specify the initial page with RouteQuery itself.

This is safer.

@appRoute
final appRouter = AutoRouter(
  initialQuery: HomePage.query(),
);

Query parameters

If you are using the system on the Web, you may want to receive query parameters.

In such cases, the QueryParam annotation can be used to specify the query key.

In the following case, text is passed to searchQuery when accessed with https://myhost.com/search?q=text.

// search.dart

import 'package:katana_router/katana_router.dart';
import 'package:flutter/material.dart';

part 'search.page.dart';

@PagePath("/search")
class SearchPage extends StatelessWidget {
  const SearchPage({
    @QueryParam("q") required this.searchQuery,
    super.key,
  });

  final String searchQuery;

  @pageRouteQuery
  static const query = _$SearchPageQuery();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("User")),
      body: Center(
        child: Text("SearchQuery: $searchQuery"),
      ),
    );
  }
}

Redirect

For example, use RedirectQuery to display the login screen if you are not logged in, and the home page if you are logged in.

Implement methods of redirect by inheriting from RedirectQuery.

The original RouteQuery is passed to source, so if the transition is to the page you are trying to transition to, return source as is.

If you want to transition to another page, pass a RouteQuery for that page.

import 'package:katana_router/katana_router.dart';
import 'dart:async';

class LoginRequiredRedirectQuery extends RedirectQuery {
  const LoginRequiredRedirectQuery();
  @override
  FutureOr<RouteQuery?> redirect(
      BuildContext context, RouteQuery source) async {
    if (isSignedIn) {
      return source;
    } else {
      return Login.query();
    }
  }
}

When the RedirectQuery created here is passed by PagePath (for each individual page) or AppRouter or AutoRouter (for all pages), the redirection mechanism implemented there will be applied.

@PagePath(
  "/user/:user_id",
  redirect: [
    LoginRequiredRedirectQuery(),
  ]
)
class UserPage extends StatelessWidget {
  const UserPage({
    @PageParam("user_id") required this.userId,
    super.key,
  });

  final String userId;

  @pageRouteQuery
  static const query = _$UserPageQuery();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("User")),
      body: Center(
        child: Text("User id: $userId"),
      ),
    );
  }
}

Page when the application is launched

It is possible to define a splash page when launching the application.

This splash page can be used to perform the first data load and other processes necessary to launch the application.

Create a class inheriting from BootRouteQueryBuilder and define onInit (processing at startup), build (screen display at startup), and initialTransitionQuery (transition from the startup screen to the first page).

import 'package:katana_router/katana_router.dart';
import 'package:flutter/material.dart';

class AppBoot extends BootRouteQueryBuilder {
  const AppBoot();

  @override
  Future<void> onInit(BuildContext context) async {
    await Future.delayed(const Duration(milliseconds: 1000));
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      fit: StackFit.expand,
      children: const [
        Material(child: AppLogo()),
        CompanyLogo(),
      ],
    );
  }

  @override
  TransitionQuery get initialTransitionQuery => TransitionQuery.fade;
}

The AppBoot class defined here is given as an argument in the AppRouter() or AutoRouter() class.

@appRoute
final appRouter = AutoRouter(
  boot: const AppBoot(),
);

Nested Navigation

Nested navigation can be implemented by managing the created AppRouter with an underlying Widget.

Please keep the state of the created AppRouter with a StatefulWidget or Provider to prevent it from being modified.

InitialQuery is also available for nested navigation.

It is also possible to further restrict the pages used by the pages parameter.

By passing this AppRouter as it is in Router.withConfig, it is possible to implement navigation at a lower level.

@PagePath("/nested")
class NestedContainerPage extends StatefulWidget {
  const NestedContainerPage({
    super.key,
  });

  @pageRouteQuery
  static const query = _$NestedContainerPageQuery();

  @override
  State<StatefulWidget> createState() => _NestedContainerPageState();
}

class _NestedContainerPageState extends State<NestedContainerPage> {
  final router = AppRouter(
    initialQuery: InnerPage1.query(),
    pages: [
      InnerPage1.query,
      InnerPage2.query,
    ],
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("NestedPage")),
      body: Router.withConfig(config: router),
    );
  }
}

Nested pages basically do not require a path for deep linking.

Therefore, it is possible to use the NestedPage annotation exclusively.

@nestedPage()
class InnerPage1 extends StatelessWidget {
  const InnerPage1({super.key});

  static const query = _$InnerPage1Query();

  @override
  Widget build(BuildContext context) {
    final current = context.rootRouter.currentQuery;
    return Center(
      child: TextButton(
        onPressed: () {
          context.router.push(InnerPage2.query());
        },
        child: Text("To Innerpage2"),
      ),
    );
  }
}

@nestedPage()
class InnerPage2 extends StatelessWidget {
  const InnerPage2({super.key});

  static const query = _$InnerPage2Query();

  @override
  Widget build(BuildContext context) {
    return Center(
      child: TextButton(
        onPressed: () {
          context.router.push(InnerPage1.query());
        },
        child: Text("To Innerpage1"),
      ),
    );
  }
}

Specifying context.router within a nested page causes page transitions within the nested navigation.

Use context.rootRouter for full-screen page transitions. You can use the top-level AppRouter or AutoRouter to perform page transitions.

Tab Navigation

AppRouter and AutoRouter inherits from ChangeNotifier and notifies the user when a page transition is detected.

Therefore, by monitoring AppRouter or AutoRouter with addListener, it is possible to detect transitions that occur in the lower layers and update the widget itself.

Information on the current page can also be found at AppRouter.currentQuery.

Since key and name can be specified for each page, tab navigation can be implemented in a type-safe manner by, for example, "passing an enum value to key and changing the tab state based on that value”.

@PagePath("/nested", name: "nested")
class NestedContainerPage extends StatefulWidget {
  const NestedContainerPage({
    super.key,
  });

  @pageRouteQuery
  static const query = _$NestedContainerPageQuery();

  @override
  State<StatefulWidget> createState() => _NestedContainerPageState();
}

class _NestedContainerPageState extends State<NestedContainerPage> {
  final router = AppRouter(
    initialQuery: InnerPage1.query(),
    defaultTransitionQuery: TransitionQuery.fade,
    pages: [
      ...InnerPageType.values.map((e) => e.builder),
    ],
  );

  @override
  void initState() {
    super.initState();
    router.addListener(handledOnUpdate);
  }

  void handledOnUpdate() {
    setState(() {});
  }

  @override
  void dispose() {
    super.dispose();
    router.removeListener(handledOnUpdate);
    router.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final query = router.currentQuery;
    return Scaffold(
      appBar: AppBar(title: const Text("NestedPage")),
      body: Router.withConfig(config: router),
      bottomNavigationBar: BottomNavigationBar(
        onTap: (value) {
          router.push(
            InnerPageType.values[value].query,
          );
        },
        currentIndex: query?.key<InnerPageType>()?.index ?? 0,
        items: InnerPageType.values.map((type) {
          return BottomNavigationBarItem(
            icon: Icon(type.icon),
            label: type.label,
          );
        }).toList(),
      ),
    );
  }
}

@NestedPage(key: InnerPageType.type1)
class InnerPage1 extends StatelessWidget {
  const InnerPage1({super.key});

  static const query = _$InnerPage1Query();

  @override
  Widget build(BuildContext context) {
    final current = context.rootRouter.currentQuery;
    return Center(
      child: TextButton(
        onPressed: () {
          context.rootRouter.pop();
        },
        child: Text("To Innerpage2 ${current?.name}"),
      ),
    );
  }
}

@NestedPage(key: InnerPageType.type2)
class InnerPage2 extends StatelessWidget {
  const InnerPage2({super.key});

  static const query = _$InnerPage2Query();

  @override
  Widget build(BuildContext context) {
    return Center(
      child: TextButton(
        onPressed: () {
          context.router.push(InnerPage1.query());
        },
        child: const Text("To Innerpage1"),
      ),
    );
  }
}

enum InnerPageType {
  type1(
    icon: Icons.people,
    label: "people",
  ),
  type2(
    icon: Icons.settings,
    label: "settings",
  );

  const InnerPageType({
    required this.icon,
    required this.label,
  });

  final IconData icon;

  final String label;

  RouteQueryBuilder get builder {
    switch (this) {
      case InnerPageType.type1:
        return InnerPage1.query;
      case InnerPageType.type2:
        return InnerPage2.query;
    }
  }

  RouteQuery get query {
    switch (this) {
      case InnerPageType.type1:
        return InnerPage1.query();
      case InnerPageType.type2:
        return InnerPage2.query();
    }
  }
}

vs auto_route

There is a great existing package called auto_route that is a routing package that uses build_runner.

I will make a comparison with this package.

katana_router auto_route
How to describe routing Annotation directly in Widget. Distributed definition. Create a Router and list everything in Annotation. Concentrate and define.
Generated file One per page + One per router One with a router.
Type safe
Deep link
Deep Link Parameters
Redirect
Nested Navigation

Conclusion

I made it for my own use, but if you think it fits your implementation philosophy, by all means, use it!

Also, I’re releasing the source here, so issues and PullRequests are welcome!

If you have any further work requests, please contact me directly through my Twitter or website!

Offers app development and apps using Flutter and Unity. Includes information on music and videos created by the company. Distribution of images and video materials. We also accept orders for work.
https://mathru.nethttps://mathru.net
title

GitHub Sponsors

Sponsors are always welcome. Thank you for your support!

Developed the katana/masamune framework, which has dramatically improved the overall efficiency of Flutter-based application development.
https://github.comhttps://github.com
title
[Flutter] Katana Localization

Development Flutter Masamune Dart pub

◀︎ Next
Mathru/Kanimiso-P's Vocaloid music is made into free !

Blog mathru Vocaloid Music Lyric

▶︎ Previous