csells / go_router

The purpose of the go_router for Flutter is to use declarative routes to reduce complexity, regardless of the platform you're targeting (mobile, web, desktop), handling deep linking from Android, iOS and the web while still allowing an easy-to-use developer experience.
https://gorouter.dev
441 stars 96 forks source link

GoRoute and GoRouteState may be generic #305

Closed topperspal closed 2 years ago

topperspal commented 2 years ago

The simplest way to navigate to a page is Navigator.of(context).push(MaterialPageRoute(builder: (context) => MyHomePage()));. I know there is some simple methods like If you are using getx then u can simply navigate using Get.to('MyHomePage()'). This is also fine. In both of the above methods we are using dart's typed_language_feature . We can define as many parameters as we can either required or optional. This way Dart enforces us to provide specific arguments it needs to build the widget. But think about this:-

I have a UserForm that accepts an options argument of type User. If the User is null then it will create a new user else it will update the old user. So using go_router, I can define userForm route like this-

GoRoute(
    path: '/userForm',
    builder: (context, state) {
      return UserForm(user: state.extra==null ? null : state.extra as User);
    },
),

and to navigate it I am using

context.go('/userForm');
context.go('/userForm', extra: User(...));

This is not good, because I can also use

context.go('/userForm', extra: "some string"); // throws error
context.go('/userForm', extra: []); // throws error
context.go('/userForm', extra: {}); // throws error

So you will see that it is not enforcing the routes to provide specific typed data. Here we loosing the type_language_feature of dart that is not a good practice. Suppose that I have registered 100 routes and one of the route is

GoRoute(
    path: '/someRoute/:familyId/:memberId/:childId',
    builder: (context, state) {
      return SomePage(state.params['familyId'], state.params['memberId'], state.params['childId'], );
    },
),

and we still need to provide extra and some queryParams then how can I remember how many params I am using for a specific route and especially the order of params. I can go to routes file and easily see what params and queryParams it is accepting but that is annoying.

How we can get rid of all these problems?

Here comes the source_gen package to rescue us. Here, I, for my own apps, build a TypedRoute class as

class TypedRoute {
  final String path;
  final Object? extra;
  final Map<String, String>? queryParams;

  TypedRoute._(this.path, this.extra, this.queryParams);

  String get fullPath => queryParams == null ? path : Uri(path: path, queryParameters: queryParams).toString();

  void push(BuildContext context) => GoRouter.of(context).push(fullPath, extra: extra);

  void replace(BuildContext context) => GoRouter.of(context).go(fullPath, extra: extra);
}

then for each route, I create a relevent named constructor like this

// router.dart
GoRoute(
    path: '/userForm',
    builder: (context, state) => const UserForm(),
   ),
  GoRoute(
    path: '/posts/:userId',
    builder: (context, state) => const PostsList(state.params["userId"]),
),

// typed_route.dart
class TypedRoute {
  ...

  TypedRoute.userForm({User? user})
      : path = '/userForm',
        extra = user,
        queryParams = null;

  TypedRoute.posts({required String userId, this.queryParams})
      : path = '/posts/$userId',
        extra = null;
}

Now I can easily navigate using

TypedRoute.userForm().push(context);

I create TypedRoute class manually but this can be generated using code generation. To use code generation GoRoute and GoRouteState need to be changed slightly. GoRoute may be generic and GoRouteState.extra inference the type of object from GoRoute instead of hardcoded Object? like

GoRoute<User>(
    // STEP 1 : Make GoRoute generic ex. class GoRoute<E extends Object> ...
    path: '/userForm',
    builder: (context, state) {
      return UserForm(user: state.extra); // Step 2 : Type of GoRouteState.extra may be inferenced from GoRoute generic instead of Object?
    }, //
)

Now parse the route , extract the params,and generate some relevant code. This way you can generate named constructors in TypedData class for each and every route.

This also simplifies this

GoRoute(
    path: '/userForm',
    builder: (context, state) {
      // return UserForm(user: state.extra==null ? null : state.extra as User);
     return UserForm(user: state.extra); // because the type is infered
    },
),

This make code more maintainable, readable, and very importantly less prone to errors because here we are using dart's typed_language_feature again. 😎

I wish I can help you achieving this. But I can share TypedRouteClass for each and every scenario. THANKS FOR READING IT CAREFULLY.

csells commented 2 years ago

I agree that such a thing would be nice. That's why we have a proposal: https://gorouter.dev/typed-routing. @kevmoo has it implemented in a PR: https://github.com/csells/go_router/pull/246