Skip to content

[go_router] GoRouter breaks when dependency changes using complex extra value #137248

@mikeroneer

Description

@mikeroneer

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.

Metadata

Metadata

Assignees

Labels

P1High-priority issues at the top of the work listp: go_routerThe go_router packagepackageflutter/packages repository. See also p: labels.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions