Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/go_router_builder/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 4.3.0

- Adds support for custom types through `TypedQueryParameter` annotation. The `encoder`, `decoder` and `compare` parameters allow specifying custom functions for encoding, decoding and comparing query parameters in `TypedGoRoute` constructors. For example, you can use a `DateTime` parameter with a custom encoder and decoder to convert it to and from a string representation in the URL.

## 4.2.0

- Adds supports for `TypedQueryParameter` annotation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,35 @@ part 'typed_query_parameter_example.g.dart';

void main() => runApp(App());

class CustomParameter {
const CustomParameter({required this.valueString, required this.valueInt});

final String valueString;
final int valueInt;

static String? encode(CustomParameter? parameter) {
if (parameter == null) {
return null;
}
return '${parameter.valueString},${parameter.valueInt}';
}

static CustomParameter? decode(String? value) {
if (value == null) {
return null;
}
final List<String> parts = value.split(',');
return CustomParameter(
valueString: parts[0],
valueInt: int.parse(parts[1]),
);
}
Comment on lines +27 to +36

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The decode method is not robust against malformed input and could lead to runtime exceptions. For example, if the value string doesn't contain a comma, accessing parts[1] will throw a RangeError. If the second part is not a valid integer, int.parse will throw a FormatException.

Since this is an example file, it's a good opportunity to demonstrate best practices for parsing. Consider adding checks for the number of parts and using int.tryParse for safer integer conversion.

  static CustomParameter? decode(String? value) {
    if (value == null) {
      return null;
    }
    final List<String> parts = value.split(',');
    if (parts.length != 2) {
      return null;
    }
    final int? valueInt = int.tryParse(parts[1]);
    if (valueInt == null) {
      return null;
    }
    return CustomParameter(
      valueString: parts[0],
      valueInt: valueInt,
    );
  }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is true. But that's only an example. So I'd think the current code is okay. Let me know if you think otherwise


static bool compare(CustomParameter a, CustomParameter b) {
return a.valueString != b.valueString || a.valueInt != b.valueInt;
}
}

class App extends StatelessWidget {
App({super.key});

Expand All @@ -27,34 +56,56 @@ class App extends StatelessWidget {
@TypedGoRoute<IntRoute>(path: '/int-route')
class IntRoute extends GoRouteData with $IntRoute {
IntRoute({
@TypedQueryParameter(name: 'intField') this.intField,
@TypedQueryParameter(name: 'int_field_with_default_value')
@TypedQueryParameter<int>(name: 'intField') this.intField,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like this may be a breaking change?

Copy link
Contributor Author

@ValentinVignal ValentinVignal Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered it a non-breaking change because existing usages of @TypedQueryParameter(name: 'my-name') will now be interpreted as @TypedQueryParameter<dynamic>S(name: 'my-name') which is okay. <T> only matters for compare, decode, encode.

But yes, ultimately, if the linter rules inference_failure_on_instance_creation is enabled, this will now create a lint that needs to be fixed.

What do you think? Should we flag it as a breaking change, and is it an acceptable one? Or is it okay to not flag it as a breaking change?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you decorate the class TypedQueryParameter with @optionalTypeArgs
https://api.flutter.dev/flutter/meta/optionalTypeArgs-constant.html

@TypedQueryParameter<int>(name: 'int_field_with_default_value')
this.intFieldWithDefaultValue = 1,
@TypedQueryParameter(name: 'int field') this.intFieldWithSpace,
@TypedQueryParameter<int>(name: 'int field') this.intFieldWithSpace,
@TypedQueryParameter<CustomParameter>(
encoder: CustomParameter.encode,
decoder: CustomParameter.decode,
)
this.customField,
@TypedQueryParameter<CustomParameter>(
encoder: CustomParameter.encode,
decoder: CustomParameter.decode,
compare: CustomParameter.compare,
)
this.customFieldWithDefaultValue = const CustomParameter(
valueString: 'default',
valueInt: 0,
),
});

final int? intField;
final int intFieldWithDefaultValue;
final int? intFieldWithSpace;
final CustomParameter? customField;
final CustomParameter customFieldWithDefaultValue;
@override
Widget build(BuildContext context, GoRouterState state) => Screen(
intField: intField,
intFieldWithDefaultValue: intFieldWithDefaultValue,
intFieldWithSpace: intFieldWithSpace,
customField: customField,
customFieldWithDefaultValue: customFieldWithDefaultValue,
);
}

class Screen extends StatelessWidget {
const Screen({
super.key,
required this.intField,
required this.intFieldWithDefaultValue,
this.intFieldWithSpace,
super.key,
this.customField,
required this.customFieldWithDefaultValue,
});

final int? intField;
final int intFieldWithDefaultValue;
final int? intFieldWithSpace;
final CustomParameter? customField;
final CustomParameter customFieldWithDefaultValue;

@override
Widget build(BuildContext context) => Scaffold(
Expand All @@ -75,6 +126,8 @@ class Screen extends StatelessWidget {
intField: newValue,
intFieldWithDefaultValue: intFieldWithDefaultValue,
intFieldWithSpace: intFieldWithSpace,
customField: customField,
customFieldWithDefaultValue: customFieldWithDefaultValue,
).go(context);
},
),
Expand All @@ -88,6 +141,8 @@ class Screen extends StatelessWidget {
intField: intField,
intFieldWithDefaultValue: newValue,
intFieldWithSpace: intFieldWithSpace,
customField: customField,
customFieldWithDefaultValue: customFieldWithDefaultValue,
).go(context);
},
),
Expand All @@ -101,6 +156,46 @@ class Screen extends StatelessWidget {
intField: intField,
intFieldWithDefaultValue: intFieldWithDefaultValue,
intFieldWithSpace: newValue,
customField: customField,
customFieldWithDefaultValue: customFieldWithDefaultValue,
).go(context);
},
),
ListTile(
title: const Text('customField:'),
subtitle: Text(CustomParameter.encode(customField) ?? ''),
trailing: const Icon(Icons.add),
onTap: () {
final newValue = CustomParameter(
valueString: '${customField?.valueString ?? ''}-',
valueInt: (customField?.valueInt ?? 0) + 1,
);
IntRoute(
intField: intField,
intFieldWithDefaultValue: intFieldWithDefaultValue,
intFieldWithSpace: intFieldWithSpace,
customField: newValue,
customFieldWithDefaultValue: customFieldWithDefaultValue,
).go(context);
},
),
ListTile(
title: const Text('customFieldWithDefaultValue:'),
subtitle: Text(
CustomParameter.encode(customFieldWithDefaultValue)!,
),
trailing: const Icon(Icons.add),
onTap: () {
final newValue = CustomParameter(
valueString: '${customFieldWithDefaultValue.valueString}-',
valueInt: customFieldWithDefaultValue.valueInt + 1,
);
IntRoute(
intField: intField,
intFieldWithDefaultValue: intFieldWithDefaultValue,
intFieldWithSpace: intFieldWithSpace,
customField: customField,
customFieldWithDefaultValue: newValue,
).go(context);
},
),
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/go_router_builder/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ dependencies:
collection: ^1.15.0
flutter:
sdk: flutter
go_router: ^17.1.0
go_router: ^17.2.0
provider: 6.0.5

dev_dependencies:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,23 @@ void main() {
);
expect(find.text('2'), findsOne);
});

testWidgets('It should modify the custom fields when tapped', (tester) async {
await tester.pumpWidget(App());

expect(find.text('customField:'), findsOne);
expect(find.text('customFieldWithDefaultValue:'), findsOne);

expect(find.text('default,0'), findsOne);

await tester.tap(find.text('customField:'));
await tester.pumpAndSettle();
expect(find.text('-,1'), findsOne);
expect(find.text('default,0'), findsOne);

await tester.tap(find.text('customFieldWithDefaultValue:'));
await tester.pumpAndSettle();
expect(find.text('-,1'), findsOne);
expect(find.text('default-,1'), findsOne);
});
}
4 changes: 1 addition & 3 deletions packages/go_router_builder/lib/src/route_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -323,9 +323,7 @@ mixin _GoRouteMixin on RouteBaseConfig {
if (param.type.isNullableType) {
throw NullableDefaultValueError(param);
}
conditions.add(
compareField(param, parameterName, param.defaultValueCode!),
);
conditions.add(compareField(param));
} else if (param.type.isNullableType) {
conditions.add('$selfFieldName.$parameterName != null');
}
Expand Down
Loading
Loading