-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Description
split from #99099 as per request of @chunhtai
According to https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/async_redirection.dart, one should use an inherited widget to perform async redirection. If the extra is of a complex type Foo and the state of the inherited widget changes, the following error occurs.
"type 'Null' is not a subtype of type 'Foo' in type cast"
From that point on, the GoRouter instance seems to be broken since no further calls would successfully switch the route (no matter if you use push or go). As a result, the user is stuck at the current route forever.
Code to reproduce this issue
see official example repository https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/async_redirection.dart or the even simpler snippet below
Code sample
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
void main() => runApp(AuthStateProvider(child: const App()));
final router = GoRouter(
initialLocation: '/',
initialExtra: Info('Pressing the button will break the app'),
routes: <RouteBase>[
GoRoute(
path: '/',
builder: (context, state) => InfoScreen(
info: state.extra as Info,
),
),
],
redirect: (context, state) async {
// accessing [AuthStateProvider] here leads to "type 'Null' is not a subtype of type 'Info' in type cast"
AuthStateProvider.of(context);
// no need to redirect for this example
return null;
},
);
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(routerConfig: router);
}
}
class InfoScreen extends StatelessWidget {
final Info info;
const InfoScreen({required this.info, super.key});
@override
Widget build(BuildContext context) {
final authStateProvider = AuthStateProvider.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('Async Redirection with complex extra parameter'),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Current info: ${info.note}'),
Text('isAuthenticated: ${authStateProvider.isAuthenticated}'),
if (authStateProvider.isAuthenticated)
ElevatedButton(
onPressed: authStateProvider.logout,
child: const Text('Logout'),
)
else
ElevatedButton(
onPressed: authStateProvider.login,
child: const Text('Login'),
),
],
),
),
);
}
}
class Info {
final String note;
Info(this.note);
}
class AuthStateProvider extends InheritedNotifier<AuthState> {
AuthStateProvider({super.key, required super.child}) : super(notifier: AuthState());
static AuthState of(BuildContext context) {
final result = context.dependOnInheritedWidgetOfExactType<AuthStateProvider>();
assert(result != null, 'No AuthState found in context');
return result!.notifier!;
}
}
class AuthState extends ChangeNotifier {
bool isAuthenticated = true;
void login() {
isAuthenticated = true;
notifyListeners();
}
void logout() {
isAuthenticated = false;
notifyListeners();
}
}
Current status of investigation
When the state of the inherited widget changes, the routers' didChangeDependencies is called since it's registered to the inherited widget in the redirect callback. Following this code path, the routeInformationProvider carries a wrong value with no extra argument in case the provided extra is not JSON serializable.
This is because non-serializable extra arguments are simply dropped during encoding in restoreRouteInformation (parser.dart). If it was serializable, we'd face the issue after deserialization with the difference that the decoded Map cannot be casted to the complex parameter type.
(?) It is not clear yet if and why the serialization is needed at that point.