katana_router 2.4.4 katana_router: ^2.4.4 copied to clipboard
A package to automatically create routing configurations with build_runner to enable type-safe routing.
Katana Router
[GitHub] | [YouTube] | [Packages] | [Twitter] | [Threads] | [LinkedIn] | [mathru.net]
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), initialTransitionQuery
(transition from the startup screen to the first page), onError
(processing on error).
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
void onError(BuildContext context, Object error, StackTrace stackTrace) {
Modal.alert(
context,
submitText: "Quit",
title: "Error",
text: "Initialization failed. \n\n$error\n$stackTrace",
);
}
@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: (index) {
router.push(
InnerPageType.values[index].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 | ○ | ○ |
GitHub Sponsors #
Sponsors are always welcome. Thank you for your support!