flutter / flutter/flutter

[go_router] triggering a pop and a redirect (that returns null) causes a route to reopen

Open
#140,510 6 comments 3 reactions 0 assignees View on GitHub
found in release: 3.16 found in release: 3.18 has reproducible steps p: go_router P2 package team-ecosystem triaged-ecosystem
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.