flutter / flutter

Flutter makes it easy and fast to build beautiful apps for mobile and beyond
https://flutter.dev
BSD 3-Clause "New" or "Revised" License
165.02k stars 27.19k forks source link

[go_router_builder] Define routes in separate files #122258

Open mikuhl-dev opened 1 year ago

mikuhl-dev commented 1 year ago

Use case

It looks like with go_router_builder, you are supposed to define all your routes into this one gigantic file, this also leads to making two different classes that essentially represent the same thing, like a HomeRoute, and HomePage that you pass in the HomeRoutes build method. At this point the code generation only solves the compile time checking, and instead leaves you with even MORE boilerplate.

Proposal

We should be able to define route data in separate files.

dancamdev commented 1 year ago

I've given it a go, technically made it work but with a catch.

The only technical issue preventing us from having multiple definition files is that go_router_builder generates a list always called $appRoutes.

So, I went ahead and instead of it always being $appRoutes, I've set the variable name to be based on the file name the generation is starting from.

Therefore, given I have the following structure:

I can easily define the router as follows:

final GoRouter _router = GoRouter(
    routes: <GoRoute>[...$authRoutes, ...$unauthRoutes],
  );

The drawback I see here, is that it being file based you could potentially end up with long and weird names. Such as:

Happy to discuss this further and open a PR with these changes if you want to take a deeper look into it.

lmapii commented 1 year ago

I'm actually trying for the first time to use go_route_builder and my first attempt was to attach the routes to the screens since this made most sense to me. I was surprised that I had to place all my route classes in the same file as the router.

JasCodes commented 1 year ago

This is one of the obious things that make me pull out hair. Forcing to put all routes in single file. Don't know who thought it was great idea.

Look at https://pub.dev/packages/katana_router how single AOP based can be...

vykes-mac commented 10 months ago

I've given it a go, technically made it work but with a catch.

The only technical issue preventing us from having multiple definition files is that go_router_builder generates a list always called $appRoutes.

So, I went ahead and instead of it always being $appRoutes, I've set the variable name to be based on the file name the generation is starting from.

Therefore, given I have the following structure:

  • auth.dart part of auth.g.dart containing the /home route.
  • unauth.dart part of unauth.g.dart containing /login and /signup routes.

I can easily define the router as follows:

final GoRouter _router = GoRouter(
    routes: <GoRoute>[...$authRoutes, ...$unauthRoutes],
  );

The drawback I see here, is that it being file based you could potentially end up with long and weird names. Such as:

  • auth.dart -> $authRoutes
  • all_types.dart -> $all_typesRoutes
  • auth_routes.dart -> $auth_routesRoutes
  • PascalCaseRoutes.dart -> $pascalCaseRoutesRoutes

Happy to discuss this further and open a PR with these changes if you want to take a deeper look into it.

@dancamdev Where did you set it so it generate the variable based on filename? Did you manually change the variable names? does it gets overwritten whenever you re-run the build_runner?

HAlbertin commented 9 months ago

Hi! I don't know if it's the best way, but that's how I've been using it:

routes.dart

import 'package:go_router/go_router.dart';
import 'package:my_project/modules/login/login.dart';
import 'package:my_project/routes/wrapped_routes/wrapped_routes.dart'
    as wrapped_routes;
import 'package:my_project/modules/login/login.dart' as login_routes;
import 'package:my_project/modules/register/register.dart' as register_routes;

class Routes {
  static final router = GoRouter(
    routes: [
      ...login_routes.$appRoutes,
      ...register_routes.$appRoutes,
      ...wrapped_routes.$appRoutes,
    ],
    initialLocation: login_routes.LoginRoute().location,
    routerNeglect: true,
  );
}

The trick here is to import the route file "as something" to use the $appRoutes specifically from that .g file.

For common routes (without any Shell) login_route.dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:my_project/modules/login/login.dart';

part 'login_route.g.dart';

const loginRoute = '/login';

@TypedGoRoute<LoginRoute>(path: loginRoute)
class LoginRoute extends GoRouteData {
  @override
  Widget build(BuildContext context, GoRouterState state) {
    return const LoginPage();
  }
}

For a Shell route type, that's my "wrapper" (I named like that, but basically it's a ShellRoute definition): wrapped_routes.dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:my_project/modules/home/route/home_route.dart';
import 'package:my_project/modules/wrapper/wrapper.dart';

part 'wrapped_routes.g.dart';

@TypedShellRoute<WrapperRouteData>(
  routes: <TypedRoute<RouteData>>[
    TypedGoRoute<HomeRouteData>(path: homeRoute),
  ],
)
class WrapperRouteData extends ShellRouteData {
  const WrapperRouteData();

  @override
  Widget builder(
    BuildContext context,
    GoRouterState state,
    Widget navigator,
  ) {
    return WrapperPage(child: navigator);
  }
}

And then we can define only the GoRouteData for that route (since the Typed itself goes under the wrapped_routes.dart file: home_route.dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:my_project/modules/home/home.dart';

class HomeRouteData extends GoRouteData {
  const HomeRouteData();

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return const HomePage();
  }
}

const homeRoute = '/home';

I have a feeling that can still be improved, but please let me know if there's a better way.

PS: I've separated my project under modules, but that's totally optional.

EmanuelLamba commented 8 months ago

Recently I was building an app with two different sign in flows one for when you have a persisted account and one for normal sign in. I thought of leveraging the routes for the two different flows but I realised that I can't use the same route as child route for multiple routes because of code generation. So my solution is same as @HAlbertin one but it seems super counterintuitive as we have to be super careful which route we import and in cases where there is a name conflict also have to use import '...' as x which bloats the code.

Persisted Signin routes code:


part 'persisted_signin_routes.g.dart';

@TypedGoRoute<PersistedSignInFlowRoute>(
  path: '/persistedsignin',
  routes: [
    TypedGoRoute<SignInRoute>(path: 'signin'),
    TypedGoRoute<SignUpRoute>(path: 'signup'),
  ],
)
class PersistedSignInFlowRoute extends GoRouteData {
  const PersistedSignInFlowRoute();

  @override
  Widget build(final BuildContext context, final GoRouterState state) =>
      const PersistedSignInScreen();
}

class SignInRoute extends GoRouteData {
  const SignInRoute();

  @override
  Widget build(final BuildContext context, final GoRouterState state) =>
      const SignInScreen(
        isPersisted: true,
      );
}

class SignUpRoute extends GoRouteData {
  const SignUpRoute();

  @override
  Widget build(final BuildContext context, final GoRouterState state) =>
      const SignUpScreen();
}

Normal Signin Routes code:


part 'signin_routes.g.dart';

@TypedGoRoute<SignInFlowRoute>(
  path: '/signin',
  routes: [
    TypedGoRoute<SignUpRoute>(path: 'signup'),
  ],
)
class SignInFlowRoute extends GoRouteData {
  const SignInFlowRoute();

  @override
  Widget build(final BuildContext context, final GoRouterState state) =>
      const SignInScreen();
}

class SignUpRoute extends GoRouteData {
  const SignUpRoute();

  @override
  Widget build(final BuildContext context, final GoRouterState state) =>
      const SignUpScreen();
}

So here I got SignUpRoute in both files. Maybe I overlooked this issue and in fact you are able to use a route as child of multiple diffrent routes. If not this seems as a big deal.

If this is the only workaround it seems more confusing to use Type-safe routes than normal ones.

rlee1990 commented 7 months ago

Are there any updates on when this will be working?

AlexanderThiele commented 1 month ago

Just came across this in my new project and i was also surprised that there is no better solution.

Would love to see a more dynamic approach using e.g. macros.