Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
63ef007
reproduce issue https://github.com/flutter/flutter/issues/150312
ahyangnb Dec 28, 2024
b92dc00
fix #150312
ahyangnb Dec 28, 2024
614663c
A new way to fix the #6953 issue.
ahyangnb Jan 9, 2025
ae86e7a
Unit test for issues/150312
ahyangnb Jan 22, 2025
15dd5b7
Merge branch 'main' into main
ahyangnb Jan 22, 2025
cad1f24
fix"info • example/test/test_stream_listener.dart:41:2 • Missing a ne…
ahyangnb Jan 22, 2025
6030676
license text used by all first-party files in this repository
ahyangnb Jan 22, 2025
545f133
Version 14.6.5: - Fix issue 150312.
ahyangnb Jan 22, 2025
9a53bdf
Merge branch 'main' into main
ahyangnb Jan 23, 2025
e208cca
Merge branch 'main' into main
ahyangnb Jan 24, 2025
99f7293
version 14.7.2: - Fix issue 150312.
ahyangnb Jan 24, 2025
653d58e
Merge branch 'main' into main
ahyangnb Jan 26, 2025
ec2be8d
Merge branch 'main' into main
ahyangnb Feb 3, 2025
0dae57a
14.7.3
ahyangnb Feb 3, 2025
8cc913b
Merge branch 'main' into main
ahyangnb Feb 4, 2025
00d392c
Merge branch 'main' into main
ahyangnb Feb 5, 2025
6ce16f8
Merge branch 'main' into main
ahyangnb Feb 6, 2025
31d8ddb
Merge branch 'main' into main
ahyangnb Feb 8, 2025
2345943
Merge branch 'main' into main
ahyangnb Feb 13, 2025
67ed112
test: restore() update currentConfiguration in pop()
ahyangnb Feb 14, 2025
3a02f82
Merge branch 'main' into main
ahyangnb Feb 14, 2025
de10e04
Merge branch 'main' into main
ahyangnb Feb 15, 2025
ec4e34f
Merge branch 'main' into main
ahyangnb Feb 18, 2025
b63a7d9
Merge branch 'main' into main
ahyangnb Feb 20, 2025
9440747
Merge branch 'main' into main
ahyangnb Feb 21, 2025
846fcfb
Merge branch 'main' into main
ahyangnb Feb 24, 2025
dda78e0
Merge branch 'main' into main
ahyangnb Mar 1, 2025
8c1e543
Merge branch 'main' into main
ahyangnb May 9, 2025
6fbb0e0
Merge branch 'flutter:main' into main
ahyangnb Jun 9, 2025
93ee871
* add change logs: Fixes Popping state and re-rendering scaffold at t…
ahyangnb Jun 9, 2025
96320b2
* remove: stream_listener_router.dart and test_stream_listener.dart
ahyangnb Jun 9, 2025
dd6dae6
## 15.1.4
ahyangnb Jun 10, 2025
137c5e3
Update version to 15.1.4 in pubspec.yaml
ahyangnb Jun 10, 2025
fe1bfae
Merge branch 'main' into main
ahyangnb Jun 11, 2025
c6cd8d4
Merge branch 'main' into main
Piinks Jun 18, 2025
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
1 change: 1 addition & 0 deletions packages/go_router/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

## 14.8.1

- Fix issue 150312.
- Secured canPop method for the lack of matches in routerDelegate's configuration.

## 14.8.0
Expand Down
318 changes: 318 additions & 0 deletions packages/go_router/example/lib/stream_listener_router.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,318 @@
// 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 'dart:async';
import 'dart:developer';

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

void main() {
GoRouter.optionURLReflectsImperativeAPIs = true;

WidgetsFlutterBinding.ensureInitialized();

runApp(const MyApp());
}

/// A counter stream that emits a new value when the counter is incremented.
class CounterStream {
int _counter = 0;

final StreamController<int> _streamController =
StreamController<int>.broadcast();

/// The stream that emits a new value when the counter is incremented.
Stream<int> get stateStream => _streamController.stream.asBroadcastStream();

/// Increments the counter and emits a new value.
void increment() {
_streamController.sink.add(++_counter);
}
}

/// A counter stream that emits a new value when the counter is incremented.
final CounterStream counterStream = CounterStream();

/// A listener that listens to a stream and refreshes the router when the stream emits a new value.
class StreamListener extends ChangeNotifier {
/// Creates a stream listener.
StreamListener(Stream<dynamic> stream) {
notifyListeners();

_subscription = stream.asBroadcastStream().listen((_) {
notifyListeners();
});
}

late final StreamSubscription<dynamic> _subscription;

@override
void notifyListeners() {
super.notifyListeners();
log('refreshing the router');
}

@override
void dispose() {
_subscription.cancel();
super.dispose();
}
}

/// The main application widget.
class MyApp extends StatefulWidget {
/// Creates the main application widget.
const MyApp({super.key});

@override
State<MyApp> createState() => _MyAppState();
}

final GlobalKey<NavigatorState> _rootNavigatorKey = GlobalKey<NavigatorState>();

final GoRouter _router = GoRouter(
initialLocation: '/',
navigatorKey: _rootNavigatorKey,
refreshListenable: StreamListener(counterStream.stateStream),
routes: <RouteBase>[
ShellRoute(
builder: (BuildContext context, GoRouterState state, Widget child) {
return GenericPage(child: child);
},
routes: <RouteBase>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) =>
const GenericPage(showPushButton: true, path: 'a'),
routes: <RouteBase>[
GoRoute(
path: 'a',
name: 'a',
builder: (BuildContext context, GoRouterState state) =>
const GenericPage(showPushButton: true, path: 'b'),
routes: <RouteBase>[
GoRoute(
path: 'b',
name: 'b',
builder: (BuildContext context, GoRouterState state) =>
const GenericPage(showBackButton: true),
),
],
),
],
),
],
),
],
);

class _MyAppState extends State<MyApp> {
late StreamSubscription<int> _stateSubscription;

/// The current state of the counter.
int _currentState = 0;

@override
void initState() {
super.initState();
_stateSubscription = counterStream.stateStream.listen((int state) {
setState(() {
_currentState = state;
log('$_currentState:: "try double place to listen"');
});
});
}

@override
void dispose() {
_stateSubscription.cancel();
super.dispose();
}

@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
routerConfig: _router,
);
}
}

/// A dialog test widget.
class DialogTest extends StatelessWidget {
/// Creates a dialog test widget.
const DialogTest({super.key});

@override
Widget build(BuildContext context) {
return Center(
child: Container(
width: 300,
height: 300,
alignment: Alignment.center,
child: Material(
color: Colors.white,
child: Column(
children:
<String>['Navigator::pop', 'GoRouter::pop'].map((String e) {
return InkWell(
child: SizedBox(
height: 60,
width: 300,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(e),
const Icon(Icons.close),
],
),
),
onTap: () {
if (e == 'GoRouter::pop') {
// WHEN THE USER PRESSES THIS BUTTON, THE URL
// DOESN'T CHANGE, BUT THE SCREEN DOES
counterStream
.increment(); // <- when removing this line the issue is gone
GoRouter.of(context).pop();
} else {
Navigator.of(context).pop();
}
},
);
}).toList(),
),
),
),
);
}
}

/// A generic page that can be used to display a page in the app.
class GenericPage extends StatefulWidget {
/// Creates a generic page.
const GenericPage({
this.child,
Key? key,
this.showPushButton = false,
this.showBackButton = false,
this.path,
}) : super(key: key ?? const ValueKey<String>('ShellWidget'));

/// The child widget to be displayed in the page.
final Widget? child;

/// Whether to show the push button.
final bool showPushButton;

/// Whether to show the back button.
final bool showBackButton;

/// The path of the page.
final String? path;

@override
State<GenericPage> createState() => _GenericPageState();
}

class _GenericPageState extends State<GenericPage> {
late StreamSubscription<int> _stateSubscription;
int _currentState = 0;
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
@override
void initState() {
super.initState();
_stateSubscription = counterStream.stateStream.listen((int state) {
setState(() {
_currentState = state;
});
});
}

@override
void dispose() {
_stateSubscription.cancel();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: widget.child != null
? AppBar(
title: Text('Count: $_currentState'),
actions: <Widget>[
TextButton(
onPressed: () {
showDialog<void>(
context: context,
builder: (BuildContext context) {
return const DialogTest();
},
);
},
child: const Text('dialog1'),
),
TextButton(
onPressed: () {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
return const DialogTest();
},
);
},
child: const Text('dialog2'),
),
TextButton(
onPressed: () {
_scaffoldKey.currentState?.openEndDrawer();
},
child: const Text('EndDrawer'),
),
],
)
: null,
endDrawer: const Drawer(
width: 200,
child: DialogTest(),
),
body: _buildWidget(context),
);
}

Widget _buildWidget(BuildContext context) {
if (widget.child != null) {
return widget.child!;
}

if (widget.showBackButton) {
return TextButton(
onPressed: () {
// WHEN THE USER PRESSES THIS BUTTON, THE URL
// DOESN'T CHANGE, BUT THE SCREEN DOES
counterStream
.increment(); // <- when removing this line the issue is gone
GoRouter.of(context).pop();
},
child: const Text('<- Go Back'),
);
}

if (widget.showPushButton) {
return TextButton(
onPressed: () {
GoRouter.of(context).goNamed(widget.path!);
},
child: const Text('Push ->'),
);
}

return Text('Current state: $_currentState');
}
}
45 changes: 45 additions & 0 deletions packages/go_router/example/test/test_stream_listener.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// 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_test/flutter_test.dart';
import 'package:go_router_examples/stream_listener_router.dart';

void main() {
// For issue https://github.com/flutter/flutter/issues/150312.
testWidgets('GoRouter.of(context).pop() works', (WidgetTester tester) async {
await tester.pumpWidget(const MyApp());

// Navigate to the second page
expect(find.text('Push ->'), findsOneWidget);
await tester.tap(find.text('Push ->'));
await tester.pumpAndSettle();

// Navigate to the third page
expect(find.text('Push ->'), findsOneWidget);
await tester.tap(find.text('Push ->'));
await tester.pumpAndSettle();

// Verify we are on the second page
expect(find.text('<- Go Back'), findsOneWidget);
await tester.tap(find.text('<- Go Back'));
await tester.pumpAndSettle();

// Expect the Count is 1.
expect(find.text('Count: 1'), findsOneWidget);

// Check if we are pop back to the second page
// and push to the third page again.
expect(find.text('Push ->'), findsOneWidget);
await tester.tap(find.text('Push ->'));
await tester.pumpAndSettle();

// Now we try pop again.
expect(find.text('<- Go Back'), findsOneWidget);
await tester.tap(find.text('<- Go Back'));
await tester.pumpAndSettle();

// Check count increment.
expect(find.text('Count: 2'), findsOneWidget);
});
}
4 changes: 4 additions & 0 deletions packages/go_router/lib/src/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -496,12 +496,16 @@ class GoRouter implements RouterConfig<RouteMatchList> {
///
/// If the top-most route is a pop up or dialog, this method pops it instead
/// of any GoRoute under it.
///
/// Ensure that the `value` of `routeInformationProvider` is synced
/// with `routerDelegate.currentConfiguration`.
void pop<T extends Object?>([T? result]) {
assert(() {
log('popping ${routerDelegate.currentConfiguration.uri}');
return true;
}());
routerDelegate.pop<T>(result);
restore(routerDelegate.currentConfiguration);
}

/// Refresh the route.
Expand Down
Loading