[go_router] triggering a pop and a redirect (that returns null) causes a route to reopen
- Dominant language
- Dart
- Stars
- 179k
- Forks
- 31.1k
- PR merge metrics
- PR metrics pending
Description
### Steps to reproduce
1. hit navigate button
2. hit pop button
### Expected results
the route pops despite the triggered redirect
### Actual results
the route is not popped
### Code sample
Code sample
```dart
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
void main() => runApp(App());
class InheritedUser extends InheritedWidget {
const InheritedUser({
required this.isLoggedIn,
required this.counter,
required super.child,
super.key,
});
final bool isLoggedIn;
final int counter;
static InheritedUser of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType()!;
@override
bool updateShouldNotify(InheritedUser oldWidget) =>
isLoggedIn != oldWidget.isLoggedIn || counter != oldWidget.counter;
}
class InheritedUserMutator extends StatefulWidget {
const InheritedUserMutator({super.key, required this.child});
final Widget child;
@override
State createState() => _InheritedUserMutatorState();
}
class _InheritedUserMutatorState extends State {
bool _isLoggedIn = false;
int _counter = 0;
void setLoggedInStatus(bool isLoggedIn) {
_isLoggedIn = isLoggedIn;
setState(() {});
}
void doSomeOtherUpdate() {
_counter++;
setState(() {});
}
@override
Widget build(BuildContext context) {
return InheritedUser(
isLoggedIn: _isLoggedIn,
counter: _counter,
child: widget.child,
);
}
}
/// The main app.
class App extends StatelessWidget {
/// Creates an [App].
App({super.key});
/// The title of the app.
static const String title = 'GoRouter Example: Redirection';
// add the login info into the tree as app state that can change over time
@override
Widget build(BuildContext context) => InheritedUserMutator(
child: MaterialApp.router(
routerConfig: _router,
title: title,
debugShowCheckedModeBanner: false,
),
);
late final GoRouter _router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/home',
builder: (BuildContext context, GoRouterState state) =>
const HomeScreen(),
),
GoRoute(
path: '/login',
builder: (BuildContext context, GoRouterState state) =>
const LoginScreen(),
routes: [
GoRoute(
path: 'home_popup_route',
builder: (_, __) => const HomePopupRoute(),
),
],
),
],
// redirect to the login page if the user is not logged in
redirect: (BuildContext context, GoRouterState state) {
// if the user is not logged in, they need to login
final bool loggedIn = InheritedUser.of(context).isLoggedIn;
final bool loggingIn = state.matchedLocation.startsWith('/login');
if (!loggedIn && loggingIn) {
return null;
}
if (!loggedIn) {
return '/login';
}
// if the user is logged in but still on the login page, send them to
// the home page
if (loggingIn) {
return '/home';
}
// no need to redirect at all
return null;
},
// changes on the listenable will cause the router to refresh it's route
debugLogDiagnostics: true,
);
}
/// The login screen.
class LoginScreen extends StatelessWidget {
/// Creates a [LoginScreen].
const LoginScreen({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text(App.title)),
body: Center(
child: Column(
children: [
ElevatedButton(
onPressed: () {
context
.findAncestorStateOfType<_InheritedUserMutatorState>()!
.setLoggedInStatus(true);
},
child: const Text('Login'),
),
ElevatedButton(
onPressed: () {
context.push('/login/home_popup_route');
},
child: const Text('Navigate'),
),
Text(GoRouterState.of(context).fullPath ?? '')
],
),
),
);
}
/// The home screen.
class HomeScreen extends StatelessWidget {
/// Creates a [HomeScreen].
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(App.title),
actions: [
IconButton(
onPressed: () => context
.findAncestorStateOfType<_InheritedUserMutatorState>()!
.setLoggedInStatus(false),
tooltip: 'Logout:',
icon: const Icon(Icons.logout),
)
],
),
body: const Center(
child: Text('HomeScreen'),
),
);
}
}
class HomePopupRoute extends StatelessWidget {
const HomePopupRoute({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [
TextButton(
onPressed: () async {
context
.findAncestorStateOfType<_InheritedUserMutatorState>()!
.doSomeOtherUpdate();
context.pop(true);
},
child: const Text('Pop'),
)
],
),
body: const Center(
child: Text('HomeScreen'),
),
);
}
}
```
### Screenshots or Video
Screenshots / Video demonstration
https://github.com/flutter/flutter/assets/40719830/5a7636a3-aa3a-4d00-ba3e-47e2aff33aa2
### Logs
Logs
```console
Launching lib/main.dart on sdk gphone64 arm64 in debug mode...
✓ Built build/app/outputs/flutter-apk/app-debug.apk.
Connecting to VM Service at ws://127.0.0.1:56745/KNJkTiWcLvY=/ws
[GoRouter] Full paths for routes:
=> /home
=> /login
=> /login/home_popup_route
[GoRouter] setting initial location /
[GoRouter] No initial matches: /
[GoRouter] redirecting to RouteMatchList(/login)
[GoRouter] Using MaterialApp configuration
W/Parcel ( 9200): Expecting binder but got null!
[GoRouter] pushing /login/home_popup_route
W/OnBackInvokedCallback( 9200): OnBackInvokedCallback is not enabled for the application.
W/OnBackInvokedCallback( 9200): Set 'android:enableOnBackInvokedCallback="true"' in the application manifest.
D/EGL_emulation( 9200): app_time_stats: avg=83748.22ms min=171.92ms max=167324.52ms count=2
[GoRouter] popping /login
W/OnBackInvokedCallback( 9200): OnBackInvokedCallback is not enabled for the application.
W/OnBackInvokedCallback( 9200): Set 'android:enableOnBackInvokedCallback="true"' in the application manifest.
D/EGL_emulation( 9200): app_time_stats: avg=29.22ms min=0.93ms max=609.39ms count=25
[GoRouter] popping /login
W/OnBackInvokedCallback( 9200): OnBackInvokedCallback is not enabled for the application.
W/OnBackInvokedCallback( 9200): Set 'android:enableOnBackInvokedCallback="true"' in the application manifest.
D/EGL_emulation( 9200): app_time_stats: avg=22.87ms min=1.21ms max=459.58ms count=33
[GoRouter] popping /login
W/OnBackInvokedCallback( 9200): OnBackInvokedCallback is not enabled for the application.
W/OnBackInvokedCallback( 9200): Set 'android:enableOnBackInvokedCallback="true"' in the application manifest.
[GoRouter] popping /login
W/OnBackInvokedCallback( 9200): OnBackInvokedCallback is not enabled for the application.
W/OnBackInvokedCallback( 9200): Set 'android:enableOnBackInvokedCallback="true"' in the application manifest.
D/EGL_emulation( 9200): app_time_stats: avg=22.71ms min=9.49ms max=209.79ms count=44
D/EGL_emulation( 9200): app_time_stats: avg=23.78ms min=1.13ms max=479.28ms count=34
D/EGL_emulation( 9200): app_time_stats: avg=9882.42ms min=1.84ms max=39523.18ms count=4
[GoRouter] pushing /login/home_popup_route
W/OnBackInvokedCallback( 9200): OnBackInvokedCallback is not enabled for the application.
W/OnBackInvokedCallback( 9200): Set 'android:enableOnBackInvokedCallback="true"' in the application manifest.
D/EGL_emulation( 9200): app_time_stats: avg=67.67ms min=2.32ms max=990.84ms count=20
[GoRouter] popping /login
W/OnBackInvokedCallback( 9200): OnBackInvokedCallback is not enabled for the application.
W/OnBackInvokedCallback( 9200): Set 'android:enableOnBackInvokedCallback="true"' in the application manifest.
D/EGL_emulation( 9200): app_time_stats: avg=23.78ms min=1.13ms max=509.49ms count=38
[GoRouter] popping /login
W/OnBackInvokedCallback( 9200): OnBackInvokedCallback is not enabled for the application.
W/OnBackInvokedCallback( 9200): Set 'android:enableOnBackInvokedCallback="true"' in the application manifest.
[GoRouter] popping /login
W/OnBackInvokedCallback( 9200): OnBackInvokedCallback is not enabled for the application.
W/OnBackInvokedCallback( 9200): Set 'android:enableOnBackInvokedCallback="true"' in the application manifest.
D/EGL_emulation( 9200): app_time_stats: avg=29.52ms min=9.78ms max=440.26ms count=34
[GoRouter] popping /login
W/OnBackInvokedCallback( 9200): OnBackInvokedCallback is not enabled for the application.
W/OnBackInvokedCallback( 9200): Set 'android:enableOnBackInvokedCallback="true"' in the application manifest.
D/EGL_emulation( 9200): app_time_stats: avg=27.52ms min=11.51ms max=404.93ms count=37
[GoRouter] popping /login
W/OnBackInvokedCallback( 9200): OnBackInvokedCallback is not enabled for the application.
W/OnBackInvokedCallback( 9200): Set 'android:enableOnBackInvokedCallback="true"' in the application manifest.
```
### Flutter Doctor output
Doctor output
```console
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.16.2, on macOS 13.2.1 22D68 darwin-arm64, locale en-UZ)
[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
[✓] Xcode - develop for iOS and macOS (Xcode 14.3.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2022.3)
[✓] VS Code (version 1.85.1)
[✓] Connected device (4 available)
! Error: Alex’s phone is busy: Waiting to connect and unlock the device. Xcode will continue when Alex’s phone is
finished. (code -10)
[✓] Network resources
```
Contributor guide
Research direction
Start with the supplied Dart reproduction at main() and trace go_router's redirect refresh after doSomeOtherUpdate() runs before context.pop(true). Verify the route behavior across the navigate and pop steps; done means the route remains popped even when the redirect returns null.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- dart, flutter
- Domain
- mobile-dev
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100