[go_router] Reduces excessive rebuilds due to inherited look up. (#4227)

fixes https://github.com/flutter/flutter/issues/123570
This commit is contained in:
chunhtai
2023-06-23 16:56:09 -07:00
committed by GitHub
parent f55d455f0d
commit 6b70804799
10 changed files with 146 additions and 101 deletions

View File

@ -1,3 +1,12 @@
## 9.0.0
- **BREAKING CHANGE**:
- Removes GoRouter.location and GoRouter.canPop. Use GoRouterState.of().location and
Navigator.of().canPop instead.
- GoRouter does not `extends` ChangeNotifier.
- [Migration guide](https://flutter.dev/go/go-router-v9-breaking-changes)
- Reduces excessive rebuilds due to inherited look up.
## 8.2.0 ## 8.2.0
- Adds onException to GoRouter constructor. - Adds onException to GoRouter constructor.

View File

@ -37,6 +37,7 @@ See the API documentation for details on the following topics:
- [Error handling](https://pub.dev/documentation/go_router/latest/topics/Error%20handling-topic.html) - [Error handling](https://pub.dev/documentation/go_router/latest/topics/Error%20handling-topic.html)
## Migration guides ## Migration guides
- [Migrating to 9.0.0](https://flutter.dev/go/go-router-v9-breaking-changes).
- [Migrating to 8.0.0](https://flutter.dev/go/go-router-v8-breaking-changes). - [Migrating to 8.0.0](https://flutter.dev/go/go-router-v8-breaking-changes).
- [Migrating to 7.0.0](https://flutter.dev/go/go-router-v7-breaking-changes). - [Migrating to 7.0.0](https://flutter.dev/go/go-router-v7-breaking-changes).
- [Migrating to 6.0.0](https://flutter.dev/go/go-router-v6-breaking-changes) - [Migrating to 6.0.0](https://flutter.dev/go/go-router-v6-breaking-changes)

View File

@ -133,6 +133,14 @@ class GoRouteInformationProvider extends RouteInformationProvider
RouteInformation get value => _value; RouteInformation get value => _value;
RouteInformation _value; RouteInformation _value;
@override
// TODO(chunhtai): remove this ignore once package minimum dart version is
// above 3.
// ignore: unnecessary_overrides
void notifyListeners() {
super.notifyListeners();
}
void _setValue(String location, Object state) { void _setValue(String location, Object state) {
final bool shouldNotify = final bool shouldNotify =
_value.location != location || _value.state != state; _value.location != location || _value.state != state;

View File

@ -11,13 +11,13 @@ import '../router.dart';
/// ///
/// Used for to find the current GoRouter in the widget tree. This is useful /// Used for to find the current GoRouter in the widget tree. This is useful
/// when routing from anywhere in your app. /// when routing from anywhere in your app.
class InheritedGoRouter extends InheritedNotifier<GoRouter> { class InheritedGoRouter extends InheritedWidget {
/// Default constructor for the inherited go router. /// Default constructor for the inherited go router.
const InheritedGoRouter({ const InheritedGoRouter({
required super.child, required super.child,
required this.goRouter, required this.goRouter,
super.key, super.key,
}) : super(notifier: goRouter); });
/// The [GoRouter] that is made available to the widget tree. /// The [GoRouter] that is made available to the widget tree.
final GoRouter goRouter; final GoRouter goRouter;
@ -27,4 +27,7 @@ class InheritedGoRouter extends InheritedNotifier<GoRouter> {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<GoRouter>('goRouter', goRouter)); properties.add(DiagnosticsProperty<GoRouter>('goRouter', goRouter));
} }
@override
bool updateShouldNotify(covariant InheritedWidget oldWidget) => false;
} }

View File

@ -62,7 +62,7 @@ typedef GoExceptionHandler = void Function(
/// {@category Deep linking} /// {@category Deep linking}
/// {@category Error handling} /// {@category Error handling}
/// {@category Named routes} /// {@category Named routes}
class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> { class GoRouter implements RouterConfig<RouteMatchList> {
/// Default constructor to configure a GoRouter with a routes builder /// Default constructor to configure a GoRouter with a routes builder
/// and an error page builder. /// and an error page builder.
/// ///
@ -152,7 +152,6 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
builderWithNav: (BuildContext context, Widget child) => builderWithNav: (BuildContext context, Widget child) =>
InheritedGoRouter(goRouter: this, child: child), InheritedGoRouter(goRouter: this, child: child),
); );
routerDelegate.addListener(_handleStateMayChange);
assert(() { assert(() {
log.info('setting initial location $initialLocation'); log.info('setting initial location $initialLocation');
@ -296,34 +295,9 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
@override @override
late final GoRouteInformationParser routeInformationParser; late final GoRouteInformationParser routeInformationParser;
/// Gets the current location.
// TODO(chunhtai): deprecates this once go_router_builder is migrated to
// GoRouterState.of.
String get location => _location;
String _location = '/';
/// Returns `true` if there is at least two or more route can be pop. /// Returns `true` if there is at least two or more route can be pop.
bool canPop() => routerDelegate.canPop(); bool canPop() => routerDelegate.canPop();
void _handleStateMayChange() {
final String newLocation;
if (routerDelegate.currentConfiguration.isNotEmpty &&
routerDelegate.currentConfiguration.matches.last
is ImperativeRouteMatch) {
newLocation = (routerDelegate.currentConfiguration.matches.last
as ImperativeRouteMatch)
.matches
.uri
.toString();
} else {
newLocation = routerDelegate.currentConfiguration.uri.toString();
}
if (_location != newLocation) {
_location = newLocation;
notifyListeners();
}
}
/// Get a location from route name and parameters. /// Get a location from route name and parameters.
/// This is useful for redirecting to a named location. /// This is useful for redirecting to a named location.
String namedLocation( String namedLocation(
@ -488,7 +462,7 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
/// of any GoRoute under it. /// of any GoRoute under it.
void pop<T extends Object?>([T? result]) { void pop<T extends Object?>([T? result]) {
assert(() { assert(() {
log.info('popping $location'); log.info('popping ${routerDelegate.currentConfiguration.uri}');
return true; return true;
}()); }());
routerDelegate.pop<T>(result); routerDelegate.pop<T>(result);
@ -497,7 +471,7 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
/// Refresh the route. /// Refresh the route.
void refresh() { void refresh() {
assert(() { assert(() {
log.info('refreshing $location'); log.info('refreshing ${routerDelegate.currentConfiguration.uri}');
return true; return true;
}()); }());
routeInformationProvider.notifyListeners(); routeInformationProvider.notifyListeners();
@ -507,27 +481,25 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
/// ///
/// This method throws when it is called during redirects. /// This method throws when it is called during redirects.
static GoRouter of(BuildContext context) { static GoRouter of(BuildContext context) {
final InheritedGoRouter? inherited = final GoRouter? inherited = maybeOf(context);
context.dependOnInheritedWidgetOfExactType<InheritedGoRouter>();
assert(inherited != null, 'No GoRouter found in context'); assert(inherited != null, 'No GoRouter found in context');
return inherited!.goRouter; return inherited!;
} }
/// The current GoRouter in the widget tree, if any. /// The current GoRouter in the widget tree, if any.
/// ///
/// This method returns null when it is called during redirects. /// This method returns null when it is called during redirects.
static GoRouter? maybeOf(BuildContext context) { static GoRouter? maybeOf(BuildContext context) {
final InheritedGoRouter? inherited = final InheritedGoRouter? inherited = context
context.dependOnInheritedWidgetOfExactType<InheritedGoRouter>(); .getElementForInheritedWidgetOfExactType<InheritedGoRouter>()
?.widget as InheritedGoRouter?;
return inherited?.goRouter; return inherited?.goRouter;
} }
@override /// Disposes resource created by this object.
void dispose() { void dispose() {
routeInformationProvider.dispose(); routeInformationProvider.dispose();
routerDelegate.removeListener(_handleStateMayChange);
routerDelegate.dispose(); routerDelegate.dispose();
super.dispose();
} }
String _effectiveInitialLocation(String? initialLocation) { String _effectiveInitialLocation(String? initialLocation) {

View File

@ -1,7 +1,7 @@
name: go_router name: go_router
description: A declarative router for Flutter based on Navigation 2 supporting description: A declarative router for Flutter based on Navigation 2 supporting
deep linking, data-driven routes and more deep linking, data-driven routes and more
version: 8.2.0 version: 9.0.0
repository: https://github.com/flutter/packages/tree/main/packages/go_router repository: https://github.com/flutter/packages/tree/main/packages/go_router
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22

View File

@ -5,6 +5,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:go_router/src/match.dart';
void main() { void main() {
group('replaceNamed', () { group('replaceNamed', () {
@ -37,7 +38,10 @@ void main() {
final GoRouter router = await createGoRouter(tester); final GoRouter router = await createGoRouter(tester);
await tester.tap(find.text('Settings')); await tester.tap(find.text('Settings'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(router.location, '/page-0/settings?search=notification'); final ImperativeRouteMatch routeMatch = router
.routerDelegate.currentConfiguration.last as ImperativeRouteMatch;
expect(routeMatch.matches.uri.toString(),
'/page-0/settings?search=notification');
}); });
}); });
} }

View File

@ -258,42 +258,6 @@ void main() {
expect(find.byType(DummyScreen), findsOneWidget); expect(find.byType(DummyScreen), findsOneWidget);
}); });
testWidgets('can access GoRouter parameters from builder',
(WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(path: '/', redirect: (_, __) => '/1'),
GoRoute(
path: '/:id',
builder: (BuildContext context, GoRouterState state) {
return Text(GoRouter.of(context).location);
}),
];
final GoRouter router = await createRouter(routes, tester);
expect(find.text('/1'), findsOneWidget);
router.go('/123?id=456');
await tester.pumpAndSettle();
expect(find.text('/123?id=456'), findsOneWidget);
});
testWidgets('can access GoRouter parameters from error builder',
(WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(path: '/', builder: dummy),
];
final GoRouter router = await createRouter(routes, tester,
errorBuilder: (BuildContext context, GoRouterState state) {
return Text(GoRouter.of(context).location);
});
router.go('/123?id=456');
await tester.pumpAndSettle();
expect(find.text('/123?id=456'), findsOneWidget);
router.go('/1234?id=456');
await tester.pumpAndSettle();
expect(find.text('/1234?id=456'), findsOneWidget);
});
testWidgets('repeatedly pops imperative route does not crash', testWidgets('repeatedly pops imperative route does not crash',
(WidgetTester tester) async { (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/123369. // Regression test for https://github.com/flutter/flutter/issues/123369.
@ -616,7 +580,11 @@ void main() {
// NOTE: match the lower case, since location is canonicalized to match the // NOTE: match the lower case, since location is canonicalized to match the
// path case whereas the location can be any case; so long as the path // path case whereas the location can be any case; so long as the path
// produces a match regardless of the location case, we win! // produces a match regardless of the location case, we win!
expect(router.location.toLowerCase(), loc.toLowerCase()); expect(
router.routerDelegate.currentConfiguration.uri
.toString()
.toLowerCase(),
loc.toLowerCase());
expect(matches, hasLength(1)); expect(matches, hasLength(1));
expect(find.byType(FamilyScreen), findsOneWidget); expect(find.byType(FamilyScreen), findsOneWidget);
@ -1680,7 +1648,8 @@ void main() {
return state.matchedLocation == '/login' ? null : '/login'; return state.matchedLocation == '/login' ? null : '/login';
}); });
expect(router.location, '/login'); expect(
router.routerDelegate.currentConfiguration.uri.toString(), '/login');
expect(redirected, isTrue); expect(redirected, isTrue);
redirected = false; redirected = false;
@ -1688,7 +1657,8 @@ void main() {
await sendPlatformUrl('/dummy', tester); await sendPlatformUrl('/dummy', tester);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(router.location, '/login'); expect(
router.routerDelegate.currentConfiguration.uri.toString(), '/login');
expect(redirected, isTrue); expect(redirected, isTrue);
}); });
@ -1716,11 +1686,12 @@ void main() {
return state.location; return state.location;
}); });
expect(router.location, '/'); expect(router.routerDelegate.currentConfiguration.uri.toString(), '/');
// Directly set the url through platform message. // Directly set the url through platform message.
await sendPlatformUrl('/dummy', tester); await sendPlatformUrl('/dummy', tester);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(router.location, '/dummy'); expect(
router.routerDelegate.currentConfiguration.uri.toString(), '/dummy');
}); });
testWidgets('top-level redirect w/ named routes', testWidgets('top-level redirect w/ named routes',
@ -1756,7 +1727,8 @@ void main() {
? null ? null
: state.namedLocation('login'), : state.namedLocation('login'),
); );
expect(router.location, '/login'); expect(
router.routerDelegate.currentConfiguration.uri.toString(), '/login');
}); });
testWidgets('route-level redirect', (WidgetTester tester) async { testWidgets('route-level redirect', (WidgetTester tester) async {
@ -1784,7 +1756,8 @@ void main() {
final GoRouter router = await createRouter(routes, tester); final GoRouter router = await createRouter(routes, tester);
router.go('/dummy'); router.go('/dummy');
await tester.pump(); await tester.pump();
expect(router.location, '/login'); expect(
router.routerDelegate.currentConfiguration.uri.toString(), '/login');
}); });
testWidgets('top-level redirect take priority over route level', testWidgets('top-level redirect take priority over route level',
@ -1826,7 +1799,8 @@ void main() {
await sendPlatformUrl('/dummy', tester); await sendPlatformUrl('/dummy', tester);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(router.location, '/login'); expect(
router.routerDelegate.currentConfiguration.uri.toString(), '/login');
expect(redirected, isTrue); expect(redirected, isTrue);
}); });
@ -1860,7 +1834,8 @@ void main() {
final GoRouter router = await createRouter(routes, tester); final GoRouter router = await createRouter(routes, tester);
router.go('/dummy'); router.go('/dummy');
await tester.pump(); await tester.pump();
expect(router.location, '/login'); expect(
router.routerDelegate.currentConfiguration.uri.toString(), '/login');
}); });
testWidgets('multiple mixed redirect', (WidgetTester tester) async { testWidgets('multiple mixed redirect', (WidgetTester tester) async {
@ -1890,7 +1865,7 @@ void main() {
state.matchedLocation == '/dummy1' ? '/dummy2' : null); state.matchedLocation == '/dummy1' ? '/dummy2' : null);
router.go('/dummy1'); router.go('/dummy1');
await tester.pump(); await tester.pump();
expect(router.location, '/'); expect(router.routerDelegate.currentConfiguration.uri.toString(), '/');
}); });
testWidgets('top-level redirect loop', (WidgetTester tester) async { testWidgets('top-level redirect loop', (WidgetTester tester) async {
@ -2012,7 +1987,7 @@ void main() {
tester, tester,
initialLocation: '/dummy', initialLocation: '/dummy',
); );
expect(router.location, '/'); expect(router.routerDelegate.currentConfiguration.uri.toString(), '/');
}); });
testWidgets('top-level redirect state', (WidgetTester tester) async { testWidgets('top-level redirect state', (WidgetTester tester) async {
@ -2314,7 +2289,8 @@ void main() {
await sendPlatformUrl('/dummy/dummy2', tester); await sendPlatformUrl('/dummy/dummy2', tester);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(router.location, '/other'); expect(
router.routerDelegate.currentConfiguration.uri.toString(), '/other');
}); });
}); });
@ -2340,7 +2316,8 @@ void main() {
tester, tester,
initialLocation: '/dummy', initialLocation: '/dummy',
); );
expect(router.location, '/dummy'); expect(
router.routerDelegate.currentConfiguration.uri.toString(), '/dummy');
}); });
testWidgets('initial location with extra', (WidgetTester tester) async { testWidgets('initial location with extra', (WidgetTester tester) async {
@ -2366,7 +2343,8 @@ void main() {
initialLocation: '/dummy', initialLocation: '/dummy',
initialExtra: 'extra', initialExtra: 'extra',
); );
expect(router.location, '/dummy'); expect(
router.routerDelegate.currentConfiguration.uri.toString(), '/dummy');
expect(find.byKey(const ValueKey<Object?>('extra')), findsOneWidget); expect(find.byKey(const ValueKey<Object?>('extra')), findsOneWidget);
}); });
@ -2389,7 +2367,7 @@ void main() {
tester, tester,
initialLocation: '/dummy', initialLocation: '/dummy',
); );
expect(router.location, '/'); expect(router.routerDelegate.currentConfiguration.uri.toString(), '/');
}); });
testWidgets( testWidgets(
@ -2465,7 +2443,7 @@ void main() {
final RouteMatchList matches = final RouteMatchList matches =
router.routerDelegate.currentConfiguration; router.routerDelegate.currentConfiguration;
expect(router.location, loc); expect(router.routerDelegate.currentConfiguration.uri.toString(), loc);
expect(matches.matches, hasLength(1)); expect(matches.matches, hasLength(1));
expect(find.byType(FamilyScreen), findsOneWidget); expect(find.byType(FamilyScreen), findsOneWidget);
expect(matches.pathParameters['fid'], fid); expect(matches.pathParameters['fid'], fid);
@ -2495,7 +2473,7 @@ void main() {
final RouteMatchList matches = final RouteMatchList matches =
router.routerDelegate.currentConfiguration; router.routerDelegate.currentConfiguration;
expect(router.location, loc); expect(router.routerDelegate.currentConfiguration.uri.toString(), loc);
expect(matches.matches, hasLength(1)); expect(matches.matches, hasLength(1));
expect(find.byType(FamilyScreen), findsOneWidget); expect(find.byType(FamilyScreen), findsOneWidget);
expect(matches.uri.queryParameters['fid'], fid); expect(matches.uri.queryParameters['fid'], fid);
@ -2729,11 +2707,11 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final RouteMatchList matches = router.routerDelegate.currentConfiguration; final RouteMatchList matches = router.routerDelegate.currentConfiguration;
expect(router.location, loc);
expect(matches.matches, hasLength(2)); expect(matches.matches, hasLength(2));
expect(find.byType(PersonScreen), findsOneWidget); expect(find.byType(PersonScreen), findsOneWidget);
final ImperativeRouteMatch imperativeRouteMatch = final ImperativeRouteMatch imperativeRouteMatch =
matches.matches.last as ImperativeRouteMatch; matches.matches.last as ImperativeRouteMatch;
expect(imperativeRouteMatch.matches.uri.toString(), loc);
expect(imperativeRouteMatch.matches.pathParameters['fid'], fid); expect(imperativeRouteMatch.matches.pathParameters['fid'], fid);
expect(imperativeRouteMatch.matches.pathParameters['pid'], pid); expect(imperativeRouteMatch.matches.pathParameters['pid'], pid);
}); });
@ -2799,7 +2777,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
RouteMatchList matches = router.routerDelegate.currentConfiguration; RouteMatchList matches = router.routerDelegate.currentConfiguration;
expect(router.location, loc); expect(router.routerDelegate.currentConfiguration.uri.toString(), loc);
expect(matches.matches, hasLength(4)); expect(matches.matches, hasLength(4));
expect(find.byType(PersonScreen), findsOneWidget); expect(find.byType(PersonScreen), findsOneWidget);
expect(matches.pathParameters['fid'], fid); expect(matches.pathParameters['fid'], fid);
@ -2861,7 +2839,8 @@ void main() {
router.routerDelegate.currentConfiguration.matches; router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(1)); expect(matches, hasLength(1));
expectLocationWithQueryParams(router.location); expectLocationWithQueryParams(
router.routerDelegate.currentConfiguration.uri.toString());
expect( expect(
tester.widget<DummyScreen>(find.byType(DummyScreen)), tester.widget<DummyScreen>(find.byType(DummyScreen)),
isA<DummyScreen>().having( isA<DummyScreen>().having(
@ -2912,7 +2891,8 @@ void main() {
router.routerDelegate.currentConfiguration.matches; router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(1)); expect(matches, hasLength(1));
expectLocationWithQueryParams(router.location); expectLocationWithQueryParams(
router.routerDelegate.currentConfiguration.uri.toString());
expect( expect(
tester.widget<DummyScreen>(find.byType(DummyScreen)), tester.widget<DummyScreen>(find.byType(DummyScreen)),
isA<DummyScreen>().having( isA<DummyScreen>().having(
@ -2962,7 +2942,8 @@ void main() {
router.routerDelegate.currentConfiguration.matches; router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(1)); expect(matches, hasLength(1));
expectLocationWithQueryParams(router.location); expectLocationWithQueryParams(
router.routerDelegate.currentConfiguration.uri.toString());
expect( expect(
tester.widget<DummyScreen>(find.byType(DummyScreen)), tester.widget<DummyScreen>(find.byType(DummyScreen)),
isA<DummyScreen>().having( isA<DummyScreen>().having(

View File

@ -25,7 +25,7 @@ void main() {
expect(shouldNotify, false); expect(shouldNotify, false);
}); });
test('updates when goRouter changes', () { test('does not update even when goRouter changes', () {
final GoRouter oldGoRouter = GoRouter( final GoRouter oldGoRouter = GoRouter(
routes: <GoRoute>[ routes: <GoRoute>[
GoRoute( GoRoute(
@ -46,7 +46,7 @@ void main() {
oldGoRouter: oldGoRouter, oldGoRouter: oldGoRouter,
newGoRouter: newGoRouter, newGoRouter: newGoRouter,
); );
expect(shouldNotify, true); expect(shouldNotify, false);
}); });
}); });

View File

@ -0,0 +1,67 @@
// 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:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
import 'test_helpers.dart';
void main() {
testWidgets('GoRouter.push does not trigger unnecessary rebuilds',
(WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/', builder: (BuildContext context, __) => const HomePage()),
GoRoute(
path: '/1',
builder: (BuildContext context, __) {
return ElevatedButton(
onPressed: () {
context.push('/1');
},
child: const Text('/1'),
);
}),
];
await createRouter(routes, tester);
expect(find.text('/'), findsOneWidget);
// first build
expect(HomePage.built, isTrue);
HomePage.built = false;
// Should not be built again afterward.
await tester.tap(find.text('/'));
await tester.pumpAndSettle();
expect(find.text('/1'), findsOneWidget);
expect(HomePage.built, isFalse);
await tester.tap(find.text('/1'));
await tester.pumpAndSettle();
expect(find.text('/1'), findsOneWidget);
expect(HomePage.built, isFalse);
await tester.tap(find.text('/1'));
await tester.pumpAndSettle();
expect(find.text('/1'), findsOneWidget);
expect(HomePage.built, isFalse);
});
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
static bool built = false;
@override
Widget build(BuildContext context) {
built = true;
return ElevatedButton(
onPressed: () {
context.push('/1');
},
child: const Text('/'),
);
}
}