Skip to content
This repository has been archived by the owner on Feb 25, 2022. It is now read-only.

Generated extension methods for goNamed and pushNamed #66

Closed
Tracked by #174
craiglabenz opened this issue Oct 7, 2021 · 21 comments · Fixed by #216
Closed
Tracked by #174

Generated extension methods for goNamed and pushNamed #66

craiglabenz opened this issue Oct 7, 2021 · 21 comments · Fixed by #216
Assignees
Labels
enhancement New feature or request

Comments

@craiglabenz
Copy link
Contributor

This is an exploratory feature request to add a build_runner layer for generating type-safe wrappers around goNamed and pushNamed.

I'm imagining something like this:

@Generate(int fid)
GoRoute(
  name: 'family',
  path: '/family/:fid',
  pageBuilder: (context, state) {...},
)

Which would create something like this:

extension on GoRouter {
  void pushFamily(int fid) =>
    push(_lookupNamedRoute('family', {'fid': fid}));

  void goFamily(int fid) =>
    go(_lookupNamedRoute('family', {'fid': fid}));
}

Thoughts?

@csells
Copy link
Owner

csells commented Oct 7, 2021

My plan had been to wait until the static meta programming feature in Dart was available to add typesafe routing but I like the idea to add it optionally now. Do you have a PR to share?

@craiglabenz
Copy link
Contributor Author

No PR to share yet. I think this would be a significant amount of work, so I'd want to brainstorm it together and get full buy-in before breaking any ground.

@csells
Copy link
Owner

csells commented Oct 7, 2021

Having a generated file that provided for type safe wrappers for named routes would make an excellent addition to go_router.

@csells csells added the enhancement New feature or request label Oct 7, 2021
@csells
Copy link
Owner

csells commented Oct 29, 2021

@craiglabenz and I did some brainstorming and came up w/ the following core abstraction to noddle on:

// go_router provides
abstract class MaterialGoRoute {
  Widget builder(BuilderContext context, GoRouterState state) =>
    MaterialPage(key: state.pageKey, child: buildPage(context, state));

  Widget buildPage(BuildContext context, GoRouterState state);
}

// user defines a top-level route
@goroute('home', path: '/')
class HomeRoute extends MaterialGoRoute {
  @override
  Widget buildPage(BuildContext context, GoRouterState state) => HomePage();
}

// user defines a sub-route (under HomeRoute)
@goroute('family', path: 'family/:fid', parent: HomeRoute)
class FamilyRoute extends MaterialGoRoute {
  @goparam('fid') final String fid;
  FamilyRoute(this.fid);
  
  @override
  Widget buildPage(BuildContext context, GoRouterState state) {
    final family = Families.family(fid);
    return FamilyPage(family);
  }
}

// user defines a sub-route (under FamilyRoute)
@goroute('person', path: 'person/:pid', parent: Family)
class PersonRoute extends MaterialGoRoute {
  @goparam('fid') final String fid;
  @goparam('pid') final String pid;
  PersonRoute(this.fid, this.pid);
  
  @override
  Widget buildPage(BuildContext context, GoRouterState state) {
    final family = Families.family(fid);
    final person = family.person(pid);
    return PersonPage(family, person);
  }
}

//... codegen magic happens...

// user can call static push/go methods; compiler enforces the right params
HomeRoute.go(context);
FamilyRoute.go(context, fid: family.id);
PersonRoute.go(context, fid: family.id, pid: person.id);
PersonRoute.push(context, fid: family.id, pid: person.id);

@csells
Copy link
Owner

csells commented Nov 1, 2021

cc @kevmoo

@csells csells mentioned this issue Nov 21, 2021
9 tasks
@lulupointu
Copy link
Contributor

lulupointu commented Nov 21, 2021

Here is my opinion on typed routing and its corresponding documentation.

Do bear in mind that I have strong opinions regarding code gen and routing so feel free to ignore my remark and please do not feel offended if I express a different opinion than yours.

Goal

I love what this is trying to solve.

About the docs, the issue is well explained but the example is quite untruthful imo:

final selectedAuthor = libraryInstance.allAuthors.firstWhereOrNull((a) => a.id == authorId);

Would still be needed even with code gen. I would remove that part and write AuthorDetailsScreen(authorId: authorId).

Route tree

I like the fact that all @TypedGoRoute don’t have to be at the same place if they can be defined in different files.

In my opinion:

  • If all @TypedGoRoute have to be in the same file, I would have one file with the GoRouteData, and another with the @TypedGoRoute. In this case I am not certain that being able to create multiple @TypedGoRoute is useful.
  • If they can be in separate files, then that’s awesome

In any case, this should be mentioned in the docs.

Also in the doc I find the order of the route in the example quite confusing at first. Since this is so early in the doc my first read took some effort to see which things wereGoRouteData and which was TypeGoRoute.

Error page builder

Is there a reason why we cannot have errorPageBuilder: $appErrorPageBuilder ? This would match the routes: $appRoutes construction used for the routes.

Navigation

I like the compiler error, this is what this entire thing is built for so that’s great 👍

I’m not sure about the .go method. On the one hand I find it fancy (in a good way), on the other hand I don’t like the concept:

  • GoRouter depends on GoRoute
  • GoRoute does NOT depend on GoRoute (to avoid cycling dependencies)

I guess it doesn’t matter here but it still bugs me in a way.

The solution would be to be able to use context.goTyped(MyGoRouteData()). This could be an extension generated by code gen so that if you don't use code gen you don't see it?

Query and path parameters

Optional parameter = Query Parameters | Required parameters = Path Parameters is very opinionated. I think this is a great default behavior however what if you have an optional parameter with a default value ? Or a required parameter which can be null ?

Extra parameters

About $extra

I am quite confused by this but maybe it’s my ignorance about code gen: could we not directly use this.person, instead of forcing it to be $extra (with the ever so confusing $)?
The rule (if not using $extra) would be that if a parameter is of a type which is not supported by GoRouter, it won’t be part of the path.

Also are named extra parameters allowed ({required this.$extra})? I love named parameters ^^

About the concept of extra parameters

I won’t hide the fact that I don’t like extra parameters (VRouter bears witness), but this might be a chance to solve the issues it has. Mainly the fact that when navigating with the url to a GoRouteData with an extra parameter what will happen? This is an error that can stay silent for a long time (since devs do not always check that typing a url works), being a really bad runtime error!

A solution could be to require them to have a default value, and check that they do during code gen (I don’t know if this is possible).

Redirection

No comment on the API, however the example is wrong I think:

// This is what is used
final loggingIn = state.subloc == HomeRoute().location

// But this should be used instead right?
final loggingIn = state.subloc == LoginRoute().location

Route-level redirection

Wait, do you mean that overriding build is never compulsory ? I don’t really like this because this does not play well with IDE. Instead I would have something like GoRedirectRouteData.

However this is in line with the current API so I understand the choice. I just don’t like it, neither here nor in the original GoRoute. As discussed with @csells I think having GoRoute.redirect which the IDE autocomplete with redirect and GoRoute which autocompletes with path and pageBuilder would be much better. (This could even allow the use of @depreciated which would not make it a breaking change). Anyhow this is not the subject here but if this was the direction the API was taking, I would introduce a GoRedirectRouteData for redirects.

Transition override

To change the page key, could we have a final Key? pageKey attribute that would be used in the page by default? This would make changing the key while keeping the default page much easier.

Note on the docs if this pageKey is possible: I would only keep the text of this section and merge it with “Custom transition”

Custom

I would give the child inbuildPage directly. Keep GoRouterState but give the child directly for make things easier:

Page<void> buildPage(BuildContext context, Widget child, GoRouterState state) => FancyPage(
     key: state.pageKey,
     child: child,
   );

That’s not vital but it took me some time to realize why build(context, state) was used and kept me from focusing on what mattered (i.e. the creation of a custom Page). This would solve this issue.

Documentation comments, hided because it is quite lenghty

Nits: you kept MaterialPage as the return type of buildPage. Use Page instead.

I would replace the example with a fully fledged one. Currently devs:

  • Have to go on gorouter.dev to learn how package specific custom transition can be implemented
  • Have to go find a medium article (see what I did there 😉 ?) to know how custom transition are implemented in vanilla flutter

Add to that the fact that devs are often confused by the Page API so they have a hard time differentiating what is package specific vs what is vanilla Flutter and that gives you a very poor experience.

Anyway here is how that could look like:

class FancyRoute extends GoRouteData {
 @override
 Widget build(BuildContext context, GoRouterState state) => FancyScreen();

 @override
  Page<void> buildPage(BuildContext context, Widget child, GoRouterState state) => FancyPage(
       key: state.pageKey,
       child: child,
     );
}

class FancyPage extends Page {
 FancyPage({required this.key, required this.child});

 final LocalKey key;
 final Widget child;

 @override
 Route createRoute(BuildContext context) {
   return PageRouteBuilder<dynamic>(
     pageBuilder: (_, animation, __) => FadeTransition(
       opacity: animation,
       child: child,
     ),
   );
 }
}

Naming conventions

I really don’t like the naming conventions. According to me in Flutter there is Screen > Route > Page > Page Stack (> indicate a composition dependency). The examples are already using the name Page for Screen. In this API we use Route for Page Stack. You can see how much confusing this is ^^

General remarks

I like what it is trying to solve.

I don’t like code generation, but I guess that’s better than runtime errors. If macro could solve this too once they land, that would be awesome (but I understand that you don’t want to wait for the dart language to change).

On that note, if the long term goal is to move to macros: Are macro mature enough that we could know if this could easily be a 1 to 1 replacement? I think the code gen API should strive to be replaceable by macro without feature change once they arrive.

What happens if a GoRouteData subclass is created but not part of the Route tree? The error will only show when SubClassGoRouteData.go(context) is called?
Conversely, what happens if a GoRouteData is put in the Route tree but does not have the right parameters?

Also the signature of GoRouteData.build method changes from Widget build(BuildContext context) to Widget build(BuildContext context, GoRouterState state) every few examples 😄

@csells
Copy link
Owner

csells commented Nov 21, 2021

@lulupointu thanks for the great feedback. I left some responses inline.

About the docs, the issue is well explained but the example is quite untruthful imo:

final selectedAuthor = libraryInstance.allAuthors.firstWhereOrNull((a) => a.id == authorId);

Would still be needed even with code gen. I would remove that part and write AuthorDetailsScreen(authorId: authorId).

That's true. The part I meant to highlight is the line of code above it, which is not code you have to write when using codegen:

final authorId = int.parse(state.params['authorId']!);
  • If all @TypedGoRoute have to be in the same file, I would have one file with the GoRouteData, and another with the @TypedGoRoute. In this case I am not certain that being able to create multiple @TypedGoRoute is useful.
  • If they can be in separate files, then that’s awesome

You need to annotate the top-level route, so the route tree needs to be in the same place as that.

Also in the doc I find the order of the route in the example quite confusing at first. Since this is so early in the doc my first read took some effort to see which things wereGoRouteData and which was TypeGoRoute.

Yep. Naming is hard. I'm open to suggestions.

Error page builder

Is there a reason why we cannot have errorPageBuilder: $appErrorPageBuilder ? This would match the routes: $appRoutes construction used for the routes.

What would it do? How would the codegen know what code to produce?

The solution would be to be able to use context.goTyped(MyGoRouteData()). This could be an extension generated by code gen so that if you don't use code gen you don't see it?

In a world where Dart allowed for overriding the go() method with different route types, that would certainly be an option.

Query and path parameters

Optional parameter = Query Parameters | Required parameters = Path Parameters is very opinionated. I think this is a great default behavior however what if you have an optional parameter with a default value ? Or a required parameter which can be null ?

This is already have go_router works. I don't want the typed routing to change the foundations of go_router; merely provide a boost in robustness and productivity.

I am quite confused by this but maybe it’s my ignorance about code gen: could we not directly use this.person, instead of forcing it to be $extra (with the ever so confusing $)? The rule (if not using $extra) would be that if a parameter is of a type which is not supported by GoRouter, it won’t be part of the path.

I want to be quite clear that $extra does not go through the location, making it worthless for deep and dynamic linking and, if you're going to use it, it's OK for it to be a little weird so that you'll have to think twice about using it.

The good news is that if you do use $extra and then change your mind, you can change your route classes and let the compiler guide you to all of the users of those routes for you to change.

Also are named extra parameters allowed ({required this.$extra})? I love named parameters ^^

Yes.

I won’t hide the fact that I don’t like extra parameters (VRouter bears witness), but this might be a chance to solve the issues it has. Mainly the fact that when navigating with the url to a GoRouteData with an extra parameter what will happen? This is an error that can stay silent for a long time (since devs do not always check that typing a url works), being a really bad runtime error!

I don't like them either but it unblocks some scenarios for folks that want to use go_router but don't care about deep and dynamic linking. I want to enable it but I don't want to make it without speed bumps.

// This is what is used
final loggingIn = state.subloc == HomeRoute().location

// But this should be used instead right?
final loggingIn = state.subloc == LoginRoute().location

Good catch!

Route-level redirection

Wait, do you mean that overriding build is never compulsory ? I don’t really like this because this does not play well with IDE. Instead I would have something like GoRedirectRouteData.

I hear you on using more types == letting the compiler be stricter. However, it also requires the user to go to the effort of switching types. By the time they do that, I feel like they could've implemented the redirect method.

Transition override

To change the page key, could we have a final Key? pageKey attribute that would be used in the page by default? This would make changing the key while keeping the default page much easier.

In my experience, switching keys is rare; If I can keep the number of concepts down, then that's a win overall. If it becomes a problem, it's easy enough to add over time.

Custom

I would give the child inbuildPage directly. Keep GoRouterState but give the child directly for make things easier:

Page<void> buildPage(BuildContext context, Widget child, GoRouterState state) => FancyPage(
     key: state.pageKey,
     child: child,
   );

The idea is that you override buildPage or build -- there's no real reason to override both or to have the result of one passed to the other.

That’s not vital but it took me some time to realize why build(context, state) was used and kept me from focusing on what mattered (i.e. the creation of a custom Page). This would solve this issue.

That was an earlier draft of the spec. The example code has been updated.

Naming conventions

I really don’t like the naming conventions. According to me in Flutter there is Screen > Route > Page > Page Stack (> indicate a composition dependency). The examples are already using the name Page for Screen. In this API we use Route for Page Stack. You can see how much confusing this is ^^

I agree that screen vs. route vs. page vs page stack is poorly understood in Flutter. I made a stab at naming in the existing go_router package and I don't thing changing the terminology for typed usage would be helpful for folks.

On that note, if the long term goal is to move to macros: Are macro mature enough that we could know if this could easily be a 1 to 1 replacement? I think the code gen API should strive to be replaceable by macro without feature change once they arrive.

That's the ideal for sure.

What happens if a GoRouteData subclass is created but not part of the Route tree? The error will only show when SubClassGoRouteData.go(context) is called? Conversely, what happens if a GoRouteData is put in the Route tree but does not have the right parameters?

go() will be missing from any GoRouteData-derived class that isn't part of the @TypedGoRoute() attribute, so it will be a compile-time error.

Also the signature of GoRouteData.build method changes from Widget build(BuildContext context) to Widget build(BuildContext context, GoRouterState state) every few examples 😄

Can you point those out to me? I looked again through the docs and couldn't find any instances of Widget build(BuildContext context, GoRouterState state). The signature can only be Widget build(BuildContext context).

@lulupointu
Copy link
Contributor

I will answer only what makes sense, but thanks for all the other answers as well.

That's true. The part I meant to highlight is the line of code above it, which is not code you have to write when using codegen

I know, what I am saying is that is would make more sense to change the example to:

GoRoute(
  path: ':authorId',
  pageBuilder: (context, state) {
    // require the authorId to be present and be an integer
    final authorId = int.parse(state.params['authorId']!);

    return MaterialPage<void>(
      key: state.pageKey,
      child: AuthorDetailsScreen(authorId: authorId),
    );
  },
),

Since this focuses on the part the codegen is trying to solve.

Also in the doc I find the order of the route in the example quite confusing at first. Since this is so early in the doc my first read took some effort to see which things wereGoRouteData and which was TypeGoRoute.

Yep. Naming is hard. I'm open to suggestions.

I was not talking about the naming here but rather the organization in the file. Here is what would make more sense to me:

// Define your [GoRouteData]s
class HomeRoute extends GoRouteData {...}
class FamilyRoute extends GoRouteData {...}
class PersonRoute extends GoRouteData {...}
class LoginRoute extends GoRouteData {...}

// Bind them to the path using [TypedGoRoute]
@TypedGoRoute<LoginRoute>(path: '/login');
@TypedGoRoute<HomeRoute>(
  path: '/',
  routes: [
    TypedGoRoute<FamilyRoute>(
      path: 'family/:fid',
      routes: [
        TypedGoRoute<PersonRoute>(
          path: 'person/:pid', 
        ),
      ], 
    ),
  ],
);

But that's just me. Maybe ask other what they think (some who have never read it before if possible)

All the part about query, path and extra params

I understand you approach and it makes sense. The only thing I am still unsure: If you use $extra and deeplink to that route, this will be a runtime error. Is that right ?

Route-level redirection
Wait, do you mean that overriding build is never compulsory ? I don’t really like this because this does not play well with IDE. Instead I would have something like GoRedirectRouteData.

I hear you on using more types == letting the compiler be stricter. However, it also requires the user to go to the effort of switching types. By the time they do that, I feel like they could've implemented the redirect method.

Agree to disagree 😉

Would you consider GoRoute.redirect though ? 😄

Naming
I really don’t like the naming conventions. According to me in Flutter there is Screen > Route > Page > Page Stack (> indicate a composition dependency). The examples are already using the name Page for Screen. In this API we use Route for Page Stack. You can see how much confusing this is ^^

I agree that screen vs. route vs. page vs page stack is poorly understood in Flutter. I made a stab at naming in the existing go_router package and I don't thing changing the terminology for typed usage would be helpful for folks.

Actually I was not arguing about GoRoute, my comments was for the use of ...Page in your example (which I stand by) and about the new name GoRouteData. Anyway you did not answer my question but I just understood why you use GoRouteData, all good (even if still unfortunate for the Route part)

Also the signature of GoRouteData.build method changes from Widget build(BuildContext context) to Widget build(BuildContext context, GoRouterState state) every few examples smile

Can you point those out to me? I looked again through the docs and couldn't find any instances of Widget build(BuildContext context, GoRouterState state). The signature can only be Widget build(BuildContext context).

I must have been confused with a buildPage then. I did check now and there is indeed no issue so in any case it's all good.

Also you did not comment on the "Complete transition example" and you did not update the doc in the regard. Is there a reason ? Or maybe you missing my comment because it was hidden in a summary ?

@csells
Copy link
Owner

csells commented Nov 21, 2021

I know, what I am saying is that is would make more sense to change the example to:

GoRoute(
  path: ':authorId',
  pageBuilder: (context, state) {
    // require the authorId to be present and be an integer
    final authorId = int.parse(state.params['authorId']!);

    return MaterialPage<void>(
      key: state.pageKey,
      child: AuthorDetailsScreen(authorId: authorId),
    );
  },
),

Since this focuses on the part the codegen is trying to solve.

Ah. Yes. I see. I just dropped in some code I copy/pasted from a sample, but you're right. Your code focuses on the issue better. I'll update the proposal.

I was not talking about the naming here but rather the organization in the file. Here is what would make more sense to me:

// Define your [GoRouteData]s
class HomeRoute extends GoRouteData {...}
class FamilyRoute extends GoRouteData {...}
class PersonRoute extends GoRouteData {...}
class LoginRoute extends GoRouteData {...}

// Bind them to the path using [TypedGoRoute]
@TypedGoRoute<LoginRoute>(path: '/login');
@TypedGoRoute<HomeRoute>(
  path: '/',
  routes: [
    TypedGoRoute<FamilyRoute>(
      path: 'family/:fid',
      routes: [
        TypedGoRoute<PersonRoute>(
          path: 'person/:pid', 
        ),
      ], 
    ),
  ],
);

But that's just me. Maybe ask other what they think (some who have never read it before if possible)

Unfortunately I believe that the limitation of codegen annotations is that they have to annotate something.

All the part about query, path and extra params

I understand you approach and it makes sense. The only thing I am still unsure: If you use $extra and deeplink to that route, this will be a runtime error. Is that right ?

Yes. That's the reason to avoid the $extra parameter.

Would you consider GoRoute.redirect though ? 😄

I considered that, but it seemed redundant. Without it, I write:

GoRouter(redirect: (_) => BooksRoute().location)

with it, I write:

GoRouter.redirect(redirect: (_) => BooksRoute().location)

It doesn't seem to be a win.

Actually I was not arguing about GoRoute, my comments was for the use of ...Page in your example (which I stand by) and about the new name GoRouteData. Anyway you did not answer my question but I just understood why you use GoRouteData, all good (even if still unfortunate for the Route part)

Were you suggesting a different name? Perhaps GoPage? GoPageData?

Also you did not comment on the "Complete transition example" and you did not update the doc in the regard. Is there a reason ? Or maybe you missing my comment because it was hidden in a summary ?

I missed your feedback there. Can you repeat it?

@lulupointu
Copy link
Contributor

lulupointu commented Nov 21, 2021

Unfortunately I believe that the limitation of codegen annotations is that they have to annotate something.

Are you sure you have read the code correctly ? I just changed the ordering of the declaration but not the code itself. What I wrote is not valid ?

Yes. That's the reason to avoid the $extra parameter.

I see. They maybe consider changing the documentation

FROM

The $extra parameter is still passed outside of the location, still defeats dynamic and deep linking and is still not recommended in general.

TO

The $extra parameter is still passed outside of the location, which means that dynamic or deep linking to its associated path not be possible. Please avoid to use it in general.

Or something along those line. The most important part being the impossibility to dynamic/deep linking. The word defeats is to vague and does not convey any meaning in itself

I considered that, but it seemed redundant. Without it, I write:

GoRouter(redirect: (_) => BooksRoute().location)

with it, I write:

GoRouter.redirect(redirect: (_) => BooksRoute().location)

It doesn't seem to be a win.

What about:

GoRouter.redirect((_) => BooksRoute().location)

Exactly the same but you harvest the power of the IDE

Were you suggesting a different name? Perhaps GoPage? GoPageData?

I was thinking about a different name but a think GoRouteData is as good as you can get.
The conceptual issue of GoRouteData is that it's both a Page defining thing and a Page Stack one (just like GoRoute), which is why GoRoute is ok I guess, it just have nothing to do with pages but is an hybrid thing that Flutter as no object for.

Why do I say Page Stack ? Well if you navigate to PersonRoute you will go to the stack of page [HomePage, FamillyPage, PersonPage] (which are MaterialPages but I renamed them to make my point, you get the idea).

This is why is confusing about those API as well, but there is not much to be done about it, as I said: GoRoute is as good of a name as any.

If I had any comment it would be about the examples, trying to as list make the naming consistent, this means changing:

  • HomeRoute to HomeGoRouteData
  • FamillyRoute to FamillyGoRouteData
  • etc.

Then people do what they want in their code but I think in the documentation this would help new comers keep track of what is what.

I missed your feedback there. Can you repeat it?

Sure:

Nits: you kept MaterialPage as the return type of buildPage. Use Page instead.

I would replace the example with a fully fledged one. Currently devs:

  • Have to go on gorouter.dev to learn how package specific custom transition can be implemented
  • Have to go find a medium article (see what I did there 😉 ?) to know how custom transition are implemented in vanilla flutter

Add to that the fact that devs are often confused by the Page API so they have a hard time differentiating what is package specific vs what is vanilla Flutter and that gives you a very poor experience.

Anyway here is how that could look like:

class FancyRoute extends GoRouteData {
 @override
 Widget build(BuildContext context, GoRouterState state) => FancyScreen();

 @override
  Page<void> buildPage(BuildContext context, Widget child, GoRouterState state) => FancyPage(
       key: state.pageKey,
       child: child,
     );
}

class FancyPage extends Page {
 FancyPage({required this.key, required this.child});

 final LocalKey key;
 final Widget child;

 @override
 Route createRoute(BuildContext context) {
   return PageRouteBuilder<dynamic>(
     pageBuilder: (_, animation, __) => FadeTransition(
       opacity: animation,
       child: child,
     ),
   );
 }
}

@benPesso
Copy link

Oh man, please don't go crazy with codegens! The main reason I switched to GoRouter was the lack of them. 😂

I get the value of the above, but if you do decide to add this, please make it optional. And please don't bake codegen into the "main workflow" of the package.

@csells
Copy link
Owner

csells commented Nov 22, 2021

Unfortunately I believe that the limitation of codegen annotations is that they have to annotate something.

Are you sure you have read the code correctly ? I just changed the ordering of the declaration but not the code itself. What I wrote is not valid ?

Before the @TypedGoRoute was annotating the top-level route. With your proposed reorganization, if I'm reading it correctly, that's no longer the case and the annotations don't actually annotate anything.

The most important part being the impossibility to dynamic/deep linking. The word defeats is to vague and does not convey any meaning in itself

I'll take that into account when/if this proposal ever turns into real docs for a real implementation.

What about:

GoRouter.redirect((_) => BooksRoute().location)

Exactly the same but you harvest the power of the IDE

I like syntax completion and letting the compiler tell me I did something wrong, but I'm sure this particular juice is worth the squeeze. For example, sometimes I want a conditional redirect and a pageBuilder. In that case, I have to eschew the GoRoute.redirect() ctor and choose the main GoRoute() ctor. That feels confusing to me.

If I had any comment it would be about the examples, trying to as list make the naming consistent, this means changing:

  • HomeRoute to HomeGoRouteData
  • FamillyRoute to FamillyGoRouteData
  • etc.

Then people do what they want in their code but I think in the documentation this would help new comers keep track of what is what.

That's too much typing, I think. I really like how HomeRoute().go(context) reads. I don't have the same fondness for HomeGoRouteData.go(context).

Nits: you kept MaterialPage as the return type of buildPage. Use Page instead.

I was relying on Dart covariance (contravariance?) and the fact that markdown is a very forgiving compilation environment. Once I have working samples and can turn this proposal into actual docs, I'll fix things like that.

I would replace the example with a fully fledged one.

class FancyRoute extends GoRouteData {
 @override
 Widget build(BuildContext context, GoRouterState state) => FancyScreen();

 @override
  Page<void> buildPage(BuildContext context, Widget child, GoRouterState state) => FancyPage(
       key: state.pageKey,
       child: child,
     );
}

class FancyPage extends Page {
 FancyPage({required this.key, required this.child});

 final LocalKey key;
 final Widget child;

 @override
 Route createRoute(BuildContext context) {
   return PageRouteBuilder<dynamic>(
     pageBuilder: (_, animation, __) => FadeTransition(
       opacity: animation,
       child: child,
     ),
   );
 }
}

I don't need to do all of that in my existing untyped routing example for custom transitions. Am I missing something in that sample?

@csells
Copy link
Owner

csells commented Nov 22, 2021

Oh man, please don't go crazy with codegens! The main reason I switched to GoRouter was the lack of them. 😂

Oh believe me. I get it. The codegen routing solutions I've seen are not worth the complexity of the build_runner, which is why I didn't start there with go_router and why I don't plan on making it the default usage. I'm hoping to leverage Dart macros when those are available, which should help make the codegen solution a lot easier, but we're not there yet.

I get the value of the above, but if you do decide to add this, please make it optional. And please don't bake codegen into the "main workflow" of the package.

That's the plan for sure.

@lulupointu
Copy link
Contributor

Before the @TypedGoRoute was annotating the top-level route. With your proposed reorganization, if I'm reading it correctly, that's no longer the case and the annotations don't actually annotate anything.

Do you mean that, because there is also some codegen for GoRouteData, even the GoRouteDatas would need to be in the same page and by defined after a @TypedGoRoute annotation, without line return ?

I don't need to do all of that in my existing untyped routing example for custom transitions. Am I missing something in that sample?

Yeah I don't know why your sample is so complex. Isn't PageRouteBuilder enough ? At least that's what I use in VRouter for custom transition under the hood and nobody ever came to me with an issue on this part.

@csells
Copy link
Owner

csells commented Nov 22, 2021

Before the @TypedGoRoute was annotating the top-level route. With your proposed reorganization, if I'm reading it correctly, that's no longer the case and the annotations don't actually annotate anything.

Do you mean that, because there is also some codegen for GoRouteData, even the GoRouteDatas would need to be in the same page and by defined after a @TypedGoRoute annotation, without line return ?

I'm not talking about formatting. I'm talking about my understanding of Dart annotations which I believe require something to annotate, e.g.

// this is OK
@foo
class Something {}

// this is not
class Something {}
@foo

@kevmoo is the authority here.

I don't need to do all of that in my existing untyped routing example for custom transitions. Am I missing something in that sample?

Yeah I don't know why your sample is so complex. Isn't PageRouteBuilder enough ? At least that's what I use in VRouter for custom transition under the hood and nobody ever came to me with an issue on this part.

Interesting. I think go_router's support for custom transitions is pretty simple.

@lulupointu
Copy link
Contributor

I'm not talking about formatting. I'm talking about my understanding of Dart annotations which I believe require something to annotate, e.g.

I see, then it's quite counter intuitive, I though I understood the use @TypedGoRoute but I wouldn't have thought that moving the GoRouteData away would break the codegen. I mean it makes sense now that you say it but being clueless about codegen I thought @TypedGoRoute was only needed at the top of a route tree

I suppose the alternative it to add a @GoRouteData annotation which you don't like right ? Ok wouldn't that even work ?

Interesting. I think go_router's support for custom transitions is pretty simple.

Actually I did some intense tests today and your approach is more bulletproof. For reasons explained here.

@csells
Copy link
Owner

csells commented Nov 22, 2021

Interesting. I think go_router's support for custom transitions is pretty simple.

Actually I did some intense tests today and your approach is more bulletproof. For reasons explained here.

Hey. I got lucky. : )

@kevmoo
Copy link
Collaborator

kevmoo commented Dec 15, 2021

Reverted in 696f7d7

@kevmoo kevmoo reopened this Dec 15, 2021
@YazeedAlKhalaf
Copy link

@csells I have a question about the approach of type safe routing. Will the way of defining GoRouter change? or the same way when meta programming is released?

@kevmoo
Copy link
Collaborator

kevmoo commented Jan 26, 2022

@YazeedAlKhalaf – no change is planned for folks doing hand-written routing. There are some new APIs added that are used by generated code so there is less boilerplate for you to write!

@csells csells closed this as completed Feb 25, 2022
Almighty-Alpaca added a commit to Almighty-Alpaca/packages that referenced this issue Mar 27, 2022
Fixes csells/go_router#66 

Adds two new classes to pkg:go_router
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants