// Copyright 2014 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/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class StateMarker extends StatefulWidget { const StateMarker({super.key, this.child}); final Widget? child; @override StateMarkerState createState() => StateMarkerState(); } class StateMarkerState extends State { late String marker; @override Widget build(BuildContext context) { return widget.child ?? Container(); } } void main() { testWidgets('Can nest apps', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp(home: MaterialApp(home: Text('Home sweet home')))); expect(find.text('Home sweet home'), findsOneWidget); }); testWidgets('Focus handling', (WidgetTester tester) async { final focusNode = FocusNode(); addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( home: Material( child: Center(child: TextField(focusNode: focusNode, autofocus: true)), ), ), ); expect(focusNode.hasFocus, isTrue); }); testWidgets('Can place app inside FocusScope', (WidgetTester tester) async { final focusScopeNode = FocusScopeNode(); addTearDown(focusScopeNode.dispose); await tester.pumpWidget( FocusScope( autofocus: true, node: focusScopeNode, child: const MaterialApp(home: Text('Home')), ), ); expect(find.text('Home'), findsOneWidget); }); testWidgets('Can show grid without losing sync', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp(home: StateMarker())); final StateMarkerState state1 = tester.state(find.byType(StateMarker)); state1.marker = 'original'; await tester.pumpWidget(const MaterialApp(debugShowMaterialGrid: true, home: StateMarker())); final StateMarkerState state2 = tester.state(find.byType(StateMarker)); expect(state1, equals(state2)); expect(state2.marker, equals('original')); }); testWidgets('Do not rebuild page during a route transition', (WidgetTester tester) async { var buildCounter = 0; await tester.pumpWidget( MaterialApp( home: Builder( builder: (BuildContext context) { return Material( child: ElevatedButton( child: const Text('X'), onPressed: () { Navigator.of(context).pushNamed('/next'); }, ), ); }, ), routes: { '/next': (BuildContext context) { return Builder( builder: (BuildContext context) { ++buildCounter; return const Text('Y'); }, ); }, }, ), ); expect(buildCounter, 0); await tester.tap(find.text('X')); expect(buildCounter, 0); await tester.pump(); expect(buildCounter, 1); await tester.pump(const Duration(milliseconds: 10)); expect(buildCounter, 1); await tester.pump(const Duration(milliseconds: 10)); expect(buildCounter, 1); await tester.pump(const Duration(milliseconds: 10)); expect(buildCounter, 1); await tester.pump(const Duration(milliseconds: 10)); expect(buildCounter, 1); await tester.pump(const Duration(seconds: 1)); expect(buildCounter, 1); expect(find.text('Y'), findsOneWidget); }); testWidgets('Do rebuild the home page if it changes', (WidgetTester tester) async { var buildCounter = 0; await tester.pumpWidget( MaterialApp( home: Builder( builder: (BuildContext context) { ++buildCounter; return const Text('A'); }, ), ), ); expect(buildCounter, 1); expect(find.text('A'), findsOneWidget); await tester.pumpWidget( MaterialApp( home: Builder( builder: (BuildContext context) { ++buildCounter; return const Text('B'); }, ), ), ); expect(buildCounter, 2); expect(find.text('B'), findsOneWidget); }); testWidgets('Do not rebuild the home page if it does not actually change', ( WidgetTester tester, ) async { var buildCounter = 0; final Widget home = Builder( builder: (BuildContext context) { ++buildCounter; return const Placeholder(); }, ); await tester.pumpWidget(MaterialApp(home: home)); expect(buildCounter, 1); await tester.pumpWidget(MaterialApp(home: home)); expect(buildCounter, 1); }); testWidgets('Do rebuild pages that come from the routes table if the MaterialApp changes', ( WidgetTester tester, ) async { var buildCounter = 0; final routes = { '/': (BuildContext context) { ++buildCounter; return const Placeholder(); }, }; await tester.pumpWidget(MaterialApp(routes: routes)); expect(buildCounter, 1); await tester.pumpWidget(MaterialApp(routes: routes)); expect(buildCounter, 2); }); testWidgets('Cannot pop the initial route', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp(home: Text('Home'))); expect(find.text('Home'), findsOneWidget); final NavigatorState navigator = tester.state(find.byType(Navigator)); final bool result = await navigator.maybePop(); expect(result, isFalse); expect(find.text('Home'), findsOneWidget); }); testWidgets('Default initialRoute', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( routes: {'/': (BuildContext context) => const Text('route "/"')}, ), ); expect(find.text('route "/"'), findsOneWidget); }); testWidgets('One-step initial route', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( initialRoute: '/a', routes: { '/': (BuildContext context) => const Text('route "/"'), '/a': (BuildContext context) => const Text('route "/a"'), '/a/b': (BuildContext context) => const Text('route "/a/b"'), '/b': (BuildContext context) => const Text('route "/b"'), }, ), ); expect(find.text('route "/"', skipOffstage: false), findsOneWidget); expect(find.text('route "/a"'), findsOneWidget); expect(find.text('route "/a/b"', skipOffstage: false), findsNothing); expect(find.text('route "/b"', skipOffstage: false), findsNothing); }); testWidgets('Return value from pop is correct', (WidgetTester tester) async { late Future result; await tester.pumpWidget( MaterialApp( home: Builder( builder: (BuildContext context) { return Material( child: ElevatedButton( child: const Text('X'), onPressed: () async { result = Navigator.of(context).pushNamed('/a'); }, ), ); }, ), routes: { '/a': (BuildContext context) { return Material( child: ElevatedButton( child: const Text('Y'), onPressed: () { Navigator.of(context).pop('all done'); }, ), ); }, }, ), ); await tester.tap(find.text('X')); await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(find.text('Y'), findsOneWidget); await tester.tap(find.text('Y')); await tester.pump(); expect(await result, equals('all done')); }); testWidgets('Two-step initial route', (WidgetTester tester) async { final routes = { '/': (BuildContext context) => const Text('route "/"'), '/a': (BuildContext context) => const Text('route "/a"'), '/a/b': (BuildContext context) => const Text('route "/a/b"'), '/b': (BuildContext context) => const Text('route "/b"'), }; await tester.pumpWidget(MaterialApp(initialRoute: '/a/b', routes: routes)); expect(find.text('route "/"', skipOffstage: false), findsOneWidget); expect(find.text('route "/a"', skipOffstage: false), findsOneWidget); expect(find.text('route "/a/b"'), findsOneWidget); expect(find.text('route "/b"', skipOffstage: false), findsNothing); }); testWidgets('Initial route with missing step', (WidgetTester tester) async { final routes = { '/': (BuildContext context) => const Text('route "/"'), '/a': (BuildContext context) => const Text('route "/a"'), '/a/b': (BuildContext context) => const Text('route "/a/b"'), '/b': (BuildContext context) => const Text('route "/b"'), }; await tester.pumpWidget(MaterialApp(initialRoute: '/a/b/c', routes: routes)); final dynamic exception = tester.takeException(); expect(exception, isA()); if (exception is String) { expect(exception.startsWith('Could not navigate to initial route.'), isTrue); expect(find.text('route "/"'), findsOneWidget); expect(find.text('route "/a"'), findsNothing); expect(find.text('route "/a/b"'), findsNothing); expect(find.text('route "/b"'), findsNothing); } }); testWidgets('Make sure initialRoute is only used the first time', (WidgetTester tester) async { final routes = { '/': (BuildContext context) => const Text('route "/"'), '/a': (BuildContext context) => const Text('route "/a"'), '/b': (BuildContext context) => const Text('route "/b"'), }; await tester.pumpWidget(MaterialApp(initialRoute: '/a', routes: routes)); expect(find.text('route "/"', skipOffstage: false), findsOneWidget); expect(find.text('route "/a"'), findsOneWidget); expect(find.text('route "/b"', skipOffstage: false), findsNothing); // changing initialRoute has no effect await tester.pumpWidget(MaterialApp(initialRoute: '/b', routes: routes)); expect(find.text('route "/"', skipOffstage: false), findsOneWidget); expect(find.text('route "/a"'), findsOneWidget); expect(find.text('route "/b"', skipOffstage: false), findsNothing); // removing it has no effect await tester.pumpWidget(MaterialApp(routes: routes)); expect(find.text('route "/"', skipOffstage: false), findsOneWidget); expect(find.text('route "/a"'), findsOneWidget); expect(find.text('route "/b"', skipOffstage: false), findsNothing); }); testWidgets( 'onGenerateRoute / onUnknownRoute', experimentalLeakTesting: LeakTesting.settings .withIgnoredAll(), // leaking by design because of exception (WidgetTester tester) async { final log = []; await tester.pumpWidget( MaterialApp( onGenerateRoute: (RouteSettings settings) { log.add('onGenerateRoute ${settings.name}'); return null; }, onUnknownRoute: (RouteSettings settings) { log.add('onUnknownRoute ${settings.name}'); return null; }, ), ); expect(tester.takeException(), isFlutterError); expect(log, ['onGenerateRoute /', 'onUnknownRoute /']); }, ); testWidgets('MaterialApp with builder and no route information works.', ( WidgetTester tester, ) async { // Regression test for https://github.com/flutter/flutter/issues/18904 await tester.pumpWidget( MaterialApp( builder: (BuildContext context, Widget? child) { return const SizedBox(); }, ), ); }); testWidgets("WidgetsApp doesn't rebuild routes when MediaQuery updates", ( WidgetTester tester, ) async { // Regression test for https://github.com/flutter/flutter/issues/37878 addTearDown(tester.platformDispatcher.clearAllTestValues); addTearDown(tester.view.reset); var routeBuildCount = 0; var dependentBuildCount = 0; await tester.pumpWidget( WidgetsApp( color: const Color.fromARGB(255, 255, 255, 255), onGenerateRoute: (_) { return PageRouteBuilder( pageBuilder: (_, _, _) { routeBuildCount++; return Builder( builder: (BuildContext context) { dependentBuildCount++; MediaQuery.of(context); return Container(); }, ); }, ); }, ), ); expect(routeBuildCount, equals(1)); expect(dependentBuildCount, equals(1)); // didChangeMetrics tester.view.physicalSize = const Size(42, 42); await tester.pump(); expect(routeBuildCount, equals(1)); expect(dependentBuildCount, equals(2)); // didChangeTextScaleFactor tester.platformDispatcher.textScaleFactorTestValue = 42; await tester.pump(); expect(routeBuildCount, equals(1)); expect(dependentBuildCount, equals(3)); // didChangePlatformBrightness tester.platformDispatcher.platformBrightnessTestValue = Brightness.dark; await tester.pump(); expect(routeBuildCount, equals(1)); expect(dependentBuildCount, equals(4)); // didChangeAccessibilityFeatures tester.platformDispatcher.accessibilityFeaturesTestValue = FakeAccessibilityFeatures.allOn; await tester.pumpAndSettle(); expect(routeBuildCount, equals(1)); expect(dependentBuildCount, equals(5)); }); testWidgets('Can get text scale from media query', (WidgetTester tester) async { TextScaler? textScaler; await tester.pumpWidget( MaterialApp( home: Builder( builder: (BuildContext context) { textScaler = MediaQuery.textScalerOf(context); return Container(); }, ), ), ); expect(textScaler, isSystemTextScaler(withScaleFactor: 1.0)); }); testWidgets('MaterialApp.navigatorKey', (WidgetTester tester) async { final key = GlobalKey(); await tester.pumpWidget( MaterialApp(navigatorKey: key, color: const Color(0xFF112233), home: const Placeholder()), ); expect(key.currentState, isA()); await tester.pumpWidget(const MaterialApp(color: Color(0xFF112233), home: Placeholder())); expect(key.currentState, isNull); await tester.pumpWidget( MaterialApp(navigatorKey: key, color: const Color(0xFF112233), home: const Placeholder()), ); expect(key.currentState, isA()); }); testWidgets('Has default material and cupertino localizations', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Builder( builder: (BuildContext context) { return Column( children: [ Text(MaterialLocalizations.of(context).selectAllButtonLabel), Text(CupertinoLocalizations.of(context).selectAllButtonLabel), ], ); }, ), ), ); // Default US "select all" text. expect(find.text('Select all'), findsOneWidget); // Default Cupertino US "select all" text. expect(find.text('Select All'), findsOneWidget); }); testWidgets('MaterialApp uses regular theme when themeMode is light', ( WidgetTester tester, ) async { addTearDown(tester.platformDispatcher.clearAllTestValues); // Mock the test to explicitly report a light platformBrightness. tester.platformDispatcher.platformBrightnessTestValue = Brightness.light; late ThemeData appliedTheme; await tester.pumpWidget( MaterialApp( theme: ThemeData(brightness: Brightness.light), darkTheme: ThemeData(brightness: Brightness.dark), themeMode: ThemeMode.light, home: Builder( builder: (BuildContext context) { appliedTheme = Theme.of(context); return const SizedBox(); }, ), ), ); expect(appliedTheme.brightness, Brightness.light); // Mock the test to explicitly report a dark platformBrightness. tester.platformDispatcher.platformBrightnessTestValue = Brightness.dark; await tester.pumpWidget( MaterialApp( theme: ThemeData(brightness: Brightness.light), darkTheme: ThemeData(brightness: Brightness.dark), themeMode: ThemeMode.light, home: Builder( builder: (BuildContext context) { appliedTheme = Theme.of(context); return const SizedBox(); }, ), ), ); expect(appliedTheme.brightness, Brightness.light); }); testWidgets('MaterialApp uses darkTheme when themeMode is dark', (WidgetTester tester) async { addTearDown(tester.platformDispatcher.clearAllTestValues); // Mock the test to explicitly report a light platformBrightness. tester.platformDispatcher.platformBrightnessTestValue = Brightness.light; late ThemeData appliedTheme; await tester.pumpWidget( MaterialApp( theme: ThemeData(brightness: Brightness.light), darkTheme: ThemeData(brightness: Brightness.dark), themeMode: ThemeMode.dark, home: Builder( builder: (BuildContext context) { appliedTheme = Theme.of(context); return const SizedBox(); }, ), ), ); expect(appliedTheme.brightness, Brightness.dark); // Mock the test to explicitly report a dark platformBrightness. tester.platformDispatcher.platformBrightnessTestValue = Brightness.dark; await tester.pumpWidget( MaterialApp( theme: ThemeData(brightness: Brightness.light), darkTheme: ThemeData(brightness: Brightness.dark), themeMode: ThemeMode.dark, home: Builder( builder: (BuildContext context) { appliedTheme = Theme.of(context); return const SizedBox(); }, ), ), ); expect(appliedTheme.brightness, Brightness.dark); }); testWidgets( 'MaterialApp uses regular theme when themeMode is system and platformBrightness is light', (WidgetTester tester) async { addTearDown(tester.platformDispatcher.clearAllTestValues); // Mock the test to explicitly report a light platformBrightness. tester.platformDispatcher.platformBrightnessTestValue = Brightness.light; late ThemeData appliedTheme; await tester.pumpWidget( MaterialApp( theme: ThemeData(brightness: Brightness.light), darkTheme: ThemeData(brightness: Brightness.dark), home: Builder( builder: (BuildContext context) { appliedTheme = Theme.of(context); return const SizedBox(); }, ), ), ); expect(appliedTheme.brightness, Brightness.light); }, ); testWidgets( 'MaterialApp uses darkTheme when themeMode is system and platformBrightness is dark', (WidgetTester tester) async { addTearDown(tester.platformDispatcher.clearAllTestValues); // Mock the test to explicitly report a dark platformBrightness. tester.platformDispatcher.platformBrightnessTestValue = Brightness.dark; late ThemeData appliedTheme; await tester.pumpWidget( MaterialApp( theme: ThemeData(brightness: Brightness.light), darkTheme: ThemeData(brightness: Brightness.dark), home: Builder( builder: (BuildContext context) { appliedTheme = Theme.of(context); return const SizedBox(); }, ), ), ); expect(appliedTheme.brightness, Brightness.dark); }, ); testWidgets( 'MaterialApp uses light theme when platformBrightness is dark but no dark theme is provided', (WidgetTester tester) async { addTearDown(tester.platformDispatcher.clearAllTestValues); // Mock the test to explicitly report a dark platformBrightness. tester.platformDispatcher.platformBrightnessTestValue = Brightness.dark; late ThemeData appliedTheme; await tester.pumpWidget( MaterialApp( theme: ThemeData(brightness: Brightness.light), home: Builder( builder: (BuildContext context) { appliedTheme = Theme.of(context); return const SizedBox(); }, ), ), ); expect(appliedTheme.brightness, Brightness.light); }, ); testWidgets( 'MaterialApp uses fallback light theme when platformBrightness is dark but no theme is provided at all', (WidgetTester tester) async { addTearDown(tester.platformDispatcher.clearAllTestValues); // Mock the test to explicitly report a dark platformBrightness. tester.platformDispatcher.platformBrightnessTestValue = Brightness.dark; late ThemeData appliedTheme; await tester.pumpWidget( MaterialApp( home: Builder( builder: (BuildContext context) { appliedTheme = Theme.of(context); return const SizedBox(); }, ), ), ); expect(appliedTheme.brightness, Brightness.light); }, ); testWidgets( 'MaterialApp uses fallback light theme when platformBrightness is light and a dark theme is provided', (WidgetTester tester) async { addTearDown(tester.platformDispatcher.clearAllTestValues); // Mock the test to explicitly report a dark platformBrightness. tester.platformDispatcher.platformBrightnessTestValue = Brightness.light; late ThemeData appliedTheme; await tester.pumpWidget( MaterialApp( darkTheme: ThemeData(brightness: Brightness.dark), home: Builder( builder: (BuildContext context) { appliedTheme = Theme.of(context); return const SizedBox(); }, ), ), ); expect(appliedTheme.brightness, Brightness.light); }, ); testWidgets('MaterialApp uses dark theme when platformBrightness is dark', ( WidgetTester tester, ) async { addTearDown(tester.platformDispatcher.clearAllTestValues); // Mock the test to explicitly report a dark platformBrightness. tester.platformDispatcher.platformBrightnessTestValue = Brightness.dark; late ThemeData appliedTheme; await tester.pumpWidget( MaterialApp( theme: ThemeData(brightness: Brightness.light), darkTheme: ThemeData(brightness: Brightness.dark), home: Builder( builder: (BuildContext context) { appliedTheme = Theme.of(context); return const SizedBox(); }, ), ), ); expect(appliedTheme.brightness, Brightness.dark); }); testWidgets('MaterialApp uses high contrast theme when appropriate', (WidgetTester tester) async { addTearDown(tester.platformDispatcher.clearAllTestValues); tester.platformDispatcher.platformBrightnessTestValue = Brightness.light; tester.platformDispatcher.accessibilityFeaturesTestValue = FakeAccessibilityFeatures.allOn; late ThemeData appliedTheme; await tester.pumpWidget( MaterialApp( theme: ThemeData(primaryColor: Colors.lightBlue), highContrastTheme: ThemeData(primaryColor: Colors.blue), home: Builder( builder: (BuildContext context) { appliedTheme = Theme.of(context); return const SizedBox(); }, ), ), ); expect(appliedTheme.primaryColor, Colors.blue); }); testWidgets('MaterialApp uses high contrast dark theme when appropriate', ( WidgetTester tester, ) async { addTearDown(tester.platformDispatcher.clearAllTestValues); tester.platformDispatcher.platformBrightnessTestValue = Brightness.dark; tester.platformDispatcher.accessibilityFeaturesTestValue = FakeAccessibilityFeatures.allOn; late ThemeData appliedTheme; await tester.pumpWidget( MaterialApp( theme: ThemeData(primaryColor: Colors.lightBlue), darkTheme: ThemeData(primaryColor: Colors.lightGreen), highContrastTheme: ThemeData(primaryColor: Colors.blue), highContrastDarkTheme: ThemeData(primaryColor: Colors.green), home: Builder( builder: (BuildContext context) { appliedTheme = Theme.of(context); return const SizedBox(); }, ), ), ); expect(appliedTheme.primaryColor, Colors.green); }); testWidgets('MaterialApp uses dark theme when no high contrast dark theme is provided', ( WidgetTester tester, ) async { addTearDown(tester.platformDispatcher.clearAllTestValues); tester.platformDispatcher.platformBrightnessTestValue = Brightness.dark; tester.platformDispatcher.accessibilityFeaturesTestValue = FakeAccessibilityFeatures.allOn; late ThemeData appliedTheme; await tester.pumpWidget( MaterialApp( theme: ThemeData(primaryColor: Colors.lightBlue), darkTheme: ThemeData(primaryColor: Colors.lightGreen), home: Builder( builder: (BuildContext context) { appliedTheme = Theme.of(context); return const SizedBox(); }, ), ), ); expect(appliedTheme.primaryColor, Colors.lightGreen); }); testWidgets('MaterialApp animates theme changes', (WidgetTester tester) async { final lightTheme = ThemeData(); final darkTheme = ThemeData.dark(); await tester.pumpWidget( MaterialApp( theme: lightTheme, darkTheme: darkTheme, themeMode: ThemeMode.light, home: Builder( builder: (BuildContext context) { return const Scaffold(); }, ), ), ); expect( tester.widget(find.byType(Material)).color, lightTheme.scaffoldBackgroundColor, ); // Change to dark theme await tester.pumpWidget( MaterialApp( darkTheme: ThemeData.dark(), themeMode: ThemeMode.dark, home: Builder( builder: (BuildContext context) { return const Scaffold(); }, ), ), ); // Wait half kThemeAnimationDuration = 200ms. await tester.pump(const Duration(milliseconds: 100)); // Default curve is linear so background should be half way between // the two colors. final Color halfBGColor = Color.lerp( lightTheme.scaffoldBackgroundColor, darkTheme.scaffoldBackgroundColor, 0.5, )!; expect(tester.widget(find.byType(Material)).color, halfBGColor); }); testWidgets('MaterialApp theme animation can be turned off', (WidgetTester tester) async { final lightTheme = ThemeData(); final darkTheme = ThemeData.dark(); var scaffoldRebuilds = 0; final Widget scaffold = Builder( builder: (BuildContext context) { scaffoldRebuilds++; // Use Theme.of() to ensure we are building when the theme changes. return Scaffold(backgroundColor: Theme.of(context).scaffoldBackgroundColor); }, ); await tester.pumpWidget( MaterialApp( theme: lightTheme, darkTheme: darkTheme, themeMode: ThemeMode.light, themeAnimationDuration: Duration.zero, home: scaffold, ), ); expect( tester.widget(find.byType(Material)).color, lightTheme.scaffoldBackgroundColor, ); expect(scaffoldRebuilds, 1); // Change to dark theme await tester.pumpWidget( MaterialApp( darkTheme: ThemeData.dark(), themeMode: ThemeMode.dark, themeAnimationDuration: Duration.zero, home: scaffold, ), ); // Wait for any animation to finish. await tester.pumpAndSettle(); expect(tester.widget(find.byType(Material)).color, darkTheme.scaffoldBackgroundColor); expect(scaffoldRebuilds, 2); }); testWidgets('MaterialApp switches themes when the platformBrightness changes.', ( WidgetTester tester, ) async { addTearDown(tester.platformDispatcher.clearAllTestValues); // Mock the test to explicitly report a light platformBrightness. tester.platformDispatcher.platformBrightnessTestValue = Brightness.light; ThemeData? themeBeforeBrightnessChange; ThemeData? themeAfterBrightnessChange; await tester.pumpWidget( MaterialApp( theme: ThemeData(brightness: Brightness.light), darkTheme: ThemeData(brightness: Brightness.dark), home: Builder( builder: (BuildContext context) { if (themeBeforeBrightnessChange == null) { themeBeforeBrightnessChange = Theme.of(context); } else { themeAfterBrightnessChange = Theme.of(context); } return const SizedBox(); }, ), ), ); // Switch the platformBrightness from light to dark and pump the widget tree // to process changes. tester.platformDispatcher.platformBrightnessTestValue = Brightness.dark; await tester.pumpAndSettle(); expect(themeBeforeBrightnessChange!.brightness, Brightness.light); expect(themeAfterBrightnessChange!.brightness, Brightness.dark); }); testWidgets('Material2 - MaterialApp provides default overscroll color', ( WidgetTester tester, ) async { Future slowDrag(WidgetTester tester, Offset start, Offset offset) async { final TestGesture gesture = await tester.startGesture(start); for (var index = 0; index < 10; index += 1) { await gesture.moveBy(offset); await tester.pump(const Duration(milliseconds: 20)); } await gesture.up(); } // The overscroll color should be a transparent version of the colorScheme's // secondary color. const secondaryColor = Color(0xff008800); final Color glowSecondaryColor = secondaryColor.withOpacity(0.05); final theme = ThemeData.from( useMaterial3: false, colorScheme: const ColorScheme.light().copyWith(secondary: secondaryColor), ); await tester.pumpWidget( MaterialApp( theme: theme, home: const SingleChildScrollView(child: SizedBox(height: 2000.0)), ), ); final RenderObject painter = tester.renderObject(find.byType(CustomPaint).first); await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 5.0)); expect(painter, paints..circle(color: glowSecondaryColor)); }); testWidgets('MaterialApp can customize initial routes', (WidgetTester tester) async { final navigatorKey = GlobalKey(); await tester.pumpWidget( MaterialApp( navigatorKey: navigatorKey, onGenerateInitialRoutes: (String initialRoute) { expect(initialRoute, '/abc'); return >[ PageRouteBuilder( pageBuilder: ( BuildContext context, Animation animation, Animation secondaryAnimation, ) { return const Text('non-regular page one'); }, ), PageRouteBuilder( pageBuilder: ( BuildContext context, Animation animation, Animation secondaryAnimation, ) { return const Text('non-regular page two'); }, ), ]; }, initialRoute: '/abc', routes: { '/': (BuildContext context) => const Text('regular page one'), '/abc': (BuildContext context) => const Text('regular page two'), }, ), ); expect(find.text('non-regular page two'), findsOneWidget); expect(find.text('non-regular page one'), findsNothing); expect(find.text('regular page one'), findsNothing); expect(find.text('regular page two'), findsNothing); navigatorKey.currentState!.pop(); await tester.pumpAndSettle(); expect(find.text('non-regular page two'), findsNothing); expect(find.text('non-regular page one'), findsOneWidget); expect(find.text('regular page one'), findsNothing); expect(find.text('regular page two'), findsNothing); }); testWidgets('MaterialApp does create HeroController with the MaterialRectArcTween', ( WidgetTester tester, ) async { final HeroController controller = MaterialApp.createMaterialHeroController(); addTearDown(controller.dispose); final Tween tween = controller.createRectTween!( const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0), const Rect.fromLTRB(0.0, 0.0, 20.0, 20.0), ); expect(tween, isA()); }); testWidgets('MaterialApp.navigatorKey can be updated', (WidgetTester tester) async { final key1 = GlobalKey(); await tester.pumpWidget(MaterialApp(navigatorKey: key1, home: const Placeholder())); expect(key1.currentState, isA()); final key2 = GlobalKey(); await tester.pumpWidget(MaterialApp(navigatorKey: key2, home: const Placeholder())); expect(key2.currentState, isA()); expect(key1.currentState, isNull); }); testWidgets('MaterialApp.router works', (WidgetTester tester) async { final provider = PlatformRouteInformationProvider( initialRouteInformation: RouteInformation(uri: Uri.parse('initial')), ); addTearDown(provider.dispose); final delegate = SimpleNavigatorRouterDelegate( builder: (BuildContext context, RouteInformation information) { return Text(information.uri.toString()); }, onPopPage: (Route route, void result, SimpleNavigatorRouterDelegate delegate) { delegate.routeInformation = RouteInformation(uri: Uri.parse('popped')); return route.didPop(result); }, ); addTearDown(delegate.dispose); await tester.pumpWidget( MaterialApp.router( routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), routerDelegate: delegate, ), ); expect(find.text('initial'), findsOneWidget); // Simulate android back button intent. final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('popRoute')); await tester.binding.defaultBinaryMessenger.handlePlatformMessage( 'flutter/navigation', message, (_) {}, ); await tester.pumpAndSettle(); expect(find.text('popped'), findsOneWidget); }); testWidgets('MaterialApp.router works with onNavigationNotification', ( WidgetTester tester, ) async { // This is a regression test for https://github.com/flutter/flutter/issues/139903. final provider = PlatformRouteInformationProvider( initialRouteInformation: RouteInformation(uri: Uri.parse('initial')), ); addTearDown(provider.dispose); final delegate = SimpleNavigatorRouterDelegate( builder: (BuildContext context, RouteInformation information) { return Text(information.uri.toString()); }, onPopPage: (Route route, void result, SimpleNavigatorRouterDelegate delegate) { delegate.routeInformation = RouteInformation(uri: Uri.parse('popped')); return route.didPop(result); }, ); addTearDown(delegate.dispose); var navigationCount = 0; await tester.pumpWidget( MaterialApp.router( routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), routerDelegate: delegate, onNavigationNotification: (NavigationNotification? notification) { navigationCount += 1; return true; }, ), ); expect(find.text('initial'), findsOneWidget); expect(navigationCount, greaterThan(0)); final navigationCountAfterBuild = navigationCount; // Simulate android back button intent. final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('popRoute')); await tester.binding.defaultBinaryMessenger.handlePlatformMessage( 'flutter/navigation', message, (_) {}, ); await tester.pumpAndSettle(); expect(find.text('popped'), findsOneWidget); expect(navigationCount, greaterThan(navigationCountAfterBuild)); }); testWidgets('MaterialApp.router route information parser is optional', ( WidgetTester tester, ) async { final delegate = SimpleNavigatorRouterDelegate( builder: (BuildContext context, RouteInformation information) { return Text(information.uri.toString()); }, onPopPage: (Route route, void result, SimpleNavigatorRouterDelegate delegate) { delegate.routeInformation = RouteInformation(uri: Uri.parse('popped')); return route.didPop(result); }, ); addTearDown(delegate.dispose); delegate.routeInformation = RouteInformation(uri: Uri.parse('initial')); await tester.pumpWidget(MaterialApp.router(routerDelegate: delegate)); expect(find.text('initial'), findsOneWidget); // Simulate android back button intent. final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('popRoute')); await tester.binding.defaultBinaryMessenger.handlePlatformMessage( 'flutter/navigation', message, (_) {}, ); await tester.pumpAndSettle(); expect(find.text('popped'), findsOneWidget); }); testWidgets( 'MaterialApp.router throw if route information provider is provided but no route information parser', (WidgetTester tester) async { final delegate = SimpleNavigatorRouterDelegate( builder: (BuildContext context, RouteInformation information) { return Text(information.uri.toString()); }, onPopPage: (Route route, void result, SimpleNavigatorRouterDelegate delegate) { delegate.routeInformation = RouteInformation(uri: Uri.parse('popped')); return route.didPop(result); }, ); addTearDown(delegate.dispose); delegate.routeInformation = RouteInformation(uri: Uri.parse('initial')); final provider = PlatformRouteInformationProvider( initialRouteInformation: RouteInformation(uri: Uri.parse('initial')), ); await tester.pumpWidget( MaterialApp.router(routeInformationProvider: provider, routerDelegate: delegate), ); expect(tester.takeException(), isAssertionError); provider.dispose(); }, ); testWidgets( 'MaterialApp.router throw if route configuration is provided along with other delegate', (WidgetTester tester) async { final delegate = SimpleNavigatorRouterDelegate( builder: (BuildContext context, RouteInformation information) { return Text(information.uri.toString()); }, onPopPage: (Route route, void result, SimpleNavigatorRouterDelegate delegate) { delegate.routeInformation = RouteInformation(uri: Uri.parse('popped')); return route.didPop(result); }, ); addTearDown(delegate.dispose); delegate.routeInformation = RouteInformation(uri: Uri.parse('initial')); final routerConfig = RouterConfig(routerDelegate: delegate); await tester.pumpWidget( MaterialApp.router(routerDelegate: delegate, routerConfig: routerConfig), ); expect(tester.takeException(), isAssertionError); }, ); testWidgets('MaterialApp.router router config works', (WidgetTester tester) async { late SimpleNavigatorRouterDelegate routerDelegate; addTearDown(() => routerDelegate.dispose()); late PlatformRouteInformationProvider provider; addTearDown(() => provider.dispose()); final routerConfig = RouterConfig( routeInformationProvider: provider = PlatformRouteInformationProvider( initialRouteInformation: RouteInformation(uri: Uri.parse('initial')), ), routeInformationParser: SimpleRouteInformationParser(), routerDelegate: routerDelegate = SimpleNavigatorRouterDelegate( builder: (BuildContext context, RouteInformation information) { return Text(information.uri.toString()); }, onPopPage: (Route route, void result, SimpleNavigatorRouterDelegate delegate) { delegate.routeInformation = RouteInformation(uri: Uri.parse('popped')); return route.didPop(result); }, ), backButtonDispatcher: RootBackButtonDispatcher(), ); await tester.pumpWidget(MaterialApp.router(routerConfig: routerConfig)); expect(find.text('initial'), findsOneWidget); // Simulate android back button intent. final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('popRoute')); await tester.binding.defaultBinaryMessenger.handlePlatformMessage( 'flutter/navigation', message, (_) {}, ); await tester.pumpAndSettle(); expect(find.text('popped'), findsOneWidget); }); testWidgets('MaterialApp.builder can build app without a Navigator', (WidgetTester tester) async { Widget? builderChild; await tester.pumpWidget( MaterialApp( builder: (BuildContext context, Widget? child) { builderChild = child; return Container(); }, ), ); expect(builderChild, isNull); }); testWidgets('MaterialApp has correct default ScrollBehavior', (WidgetTester tester) async { late BuildContext capturedContext; await tester.pumpWidget( MaterialApp( home: Builder( builder: (BuildContext context) { capturedContext = context; return const Placeholder(); }, ), ), ); expect(ScrollConfiguration.of(capturedContext).runtimeType, MaterialScrollBehavior); }); testWidgets('MaterialApp has correct default KeyboardDismissBehavior', ( WidgetTester tester, ) async { late BuildContext capturedContext; await tester.pumpWidget( MaterialApp( home: Builder( builder: (BuildContext context) { capturedContext = context; return const Placeholder(); }, ), ), ); expect( ScrollConfiguration.of(capturedContext).getKeyboardDismissBehavior(capturedContext), ScrollViewKeyboardDismissBehavior.manual, ); }); testWidgets('MaterialApp can override default KeyboardDismissBehavior', ( WidgetTester tester, ) async { late BuildContext capturedContext; await tester.pumpWidget( MaterialApp( scrollBehavior: const MaterialScrollBehavior().copyWith( keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, ), home: Builder( builder: (BuildContext context) { capturedContext = context; return const Placeholder(); }, ), ), ); expect( ScrollConfiguration.of(capturedContext).getKeyboardDismissBehavior(capturedContext), ScrollViewKeyboardDismissBehavior.onDrag, ); }); testWidgets('A ScrollBehavior can be set for MaterialApp', (WidgetTester tester) async { late BuildContext capturedContext; await tester.pumpWidget( MaterialApp( scrollBehavior: const MockScrollBehavior(), home: Builder( builder: (BuildContext context) { capturedContext = context; return const Placeholder(); }, ), ), ); final ScrollBehavior scrollBehavior = ScrollConfiguration.of(capturedContext); expect(scrollBehavior.runtimeType, MockScrollBehavior); expect( scrollBehavior.getScrollPhysics(capturedContext).runtimeType, NeverScrollableScrollPhysics, ); }); testWidgets( 'Material2 - ScrollBehavior default android overscroll indicator', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), scrollBehavior: const MaterialScrollBehavior(), home: ListView( children: const [SizedBox(height: 1000.0, width: 1000.0, child: Text('Test'))], ), ), ); expect(find.byType(StretchingOverscrollIndicator), findsNothing); expect(find.byType(GlowingOverscrollIndicator), findsOneWidget); }, variant: TargetPlatformVariant.only(TargetPlatform.android), ); testWidgets( 'Material3 - ScrollBehavior default android overscroll indicator', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( scrollBehavior: const MaterialScrollBehavior(), home: ListView( children: const [SizedBox(height: 1000.0, width: 1000.0, child: Text('Test'))], ), ), ); expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); expect(find.byType(GlowingOverscrollIndicator), findsNothing); }, variant: TargetPlatformVariant.only(TargetPlatform.android), ); testWidgets( 'MaterialScrollBehavior default stretch android overscroll indicator', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: ListView( children: const [SizedBox(height: 1000.0, width: 1000.0, child: Text('Test'))], ), ), ); expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); expect(find.byType(GlowingOverscrollIndicator), findsNothing); }, variant: TargetPlatformVariant.only(TargetPlatform.android), ); testWidgets( 'Overscroll indicator can be set by theme', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( // The current default is M3 and stretch overscroll, setting via the theme should override. theme: ThemeData().copyWith(useMaterial3: false), home: ListView( children: const [SizedBox(height: 1000.0, width: 1000.0, child: Text('Test'))], ), ), ); expect(find.byType(GlowingOverscrollIndicator), findsOneWidget); expect(find.byType(StretchingOverscrollIndicator), findsNothing); }, variant: TargetPlatformVariant.only(TargetPlatform.android), ); testWidgets( 'Material3 - ListView clip behavior updates overscroll indicator clip behavior', (WidgetTester tester) async { Widget buildFrame(Clip clipBehavior) { return MaterialApp( home: Column( children: [ SizedBox( height: 300, child: ListView.builder( itemCount: 20, clipBehavior: clipBehavior, itemBuilder: (BuildContext context, int index) { return Padding( padding: const EdgeInsets.all(10.0), child: Text('Index $index'), ); }, ), ), Opacity(opacity: 0.5, child: Container(color: const Color(0xD0FF0000), height: 100)), ], ), ); } // Test default clip behavior. await tester.pumpWidget(buildFrame(Clip.hardEdge)); expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); expect(find.byType(GlowingOverscrollIndicator), findsNothing); expect(find.text('Index 1'), findsOneWidget); RenderClipRect renderClip = tester.allRenderObjects.whereType().first; // Currently not clipping expect(renderClip.clipBehavior, equals(Clip.none)); TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Index 1'))); // Overscroll the start. await gesture.moveBy(const Offset(0.0, 200.0)); await tester.pumpAndSettle(); expect(find.text('Index 1'), findsOneWidget); expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(0)); renderClip = tester.allRenderObjects.whereType().first; // Now clipping expect(renderClip.clipBehavior, equals(Clip.hardEdge)); await gesture.up(); await tester.pumpAndSettle(); // Test custom clip behavior. await tester.pumpWidget(buildFrame(Clip.none)); renderClip = tester.allRenderObjects.whereType().first; // Currently not clipping expect(renderClip.clipBehavior, equals(Clip.none)); gesture = await tester.startGesture(tester.getCenter(find.text('Index 1'))); // Overscroll the start. await gesture.moveBy(const Offset(0.0, 200.0)); await tester.pumpAndSettle(); expect(find.text('Index 1'), findsOneWidget); expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(0)); renderClip = tester.allRenderObjects.whereType().first; // Now clipping expect(renderClip.clipBehavior, equals(Clip.none)); await gesture.up(); await tester.pumpAndSettle(); }, variant: TargetPlatformVariant.only(TargetPlatform.android), ); testWidgets( 'When `useInheritedMediaQuery` is true an existing MediaQuery is used if one is available', (WidgetTester tester) async { late BuildContext capturedContext; final uniqueKey = UniqueKey(); await tester.pumpWidget( MediaQuery( key: uniqueKey, data: const MediaQueryData(), child: MaterialApp( useInheritedMediaQuery: true, builder: (BuildContext context, Widget? child) { capturedContext = context; return const Placeholder(); }, color: const Color(0xFF123456), ), ), ); expect(capturedContext.dependOnInheritedWidgetOfExactType()?.key, uniqueKey); }, ); testWidgets( 'Assert in buildScrollbar that controller != null when using it (vertical)', (WidgetTester tester) async { const ScrollBehavior defaultBehavior = MaterialScrollBehavior(); late BuildContext capturedContext; await tester.pumpWidget( MaterialApp( home: ScrollConfiguration( // Avoid the default ones here. behavior: const MaterialScrollBehavior().copyWith(scrollbars: false), child: SingleChildScrollView( child: Builder( builder: (BuildContext context) { capturedContext = context; return Container(height: 1000.0); }, ), ), ), ), ); const details = ScrollableDetails(direction: AxisDirection.down); final Widget child = Container(); switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.iOS: // Does not throw if we aren't using it. defaultBehavior.buildScrollbar(capturedContext, child, details); case TargetPlatform.linux: case TargetPlatform.macOS: case TargetPlatform.windows: expect( () { defaultBehavior.buildScrollbar(capturedContext, child, details); }, throwsA( isA().having( (AssertionError error) => error.toString(), 'description', contains('details.controller != null'), ), ), ); } }, variant: TargetPlatformVariant.all(), ); testWidgets( 'Assert in buildScrollbar that controller != null when using it (horizontal)', (WidgetTester tester) async { const ScrollBehavior defaultBehavior = MaterialScrollBehavior(); late BuildContext capturedContext; await tester.pumpWidget( MaterialApp( home: ScrollConfiguration( // Avoid the default ones here. behavior: const MaterialScrollBehavior().copyWith(scrollbars: false), child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Builder( builder: (BuildContext context) { capturedContext = context; return Container(height: 1000.0); }, ), ), ), ), ); const details = ScrollableDetails(direction: AxisDirection.left); final Widget child = Container(); switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.iOS: case TargetPlatform.linux: case TargetPlatform.macOS: case TargetPlatform.windows: // Does not throw if we aren't using it. // Horizontal axis gets no scrollbars for all platforms. defaultBehavior.buildScrollbar(capturedContext, child, details); } }, variant: TargetPlatformVariant.all(), ); testWidgets('Override theme animation using AnimationStyle', (WidgetTester tester) async { final lightTheme = ThemeData(); final darkTheme = ThemeData.dark(); Widget buildWidget({ThemeMode themeMode = ThemeMode.light, AnimationStyle? animationStyle}) { return MaterialApp( theme: lightTheme, darkTheme: darkTheme, themeMode: themeMode, themeAnimationStyle: animationStyle, home: const Scaffold(body: Text('body')), ); } // Test the initial Scaffold background color. await tester.pumpWidget(buildWidget()); expect( tester.widget(find.byType(Material)).color, isSameColorAs(lightTheme.colorScheme.surface), ); // Test the Scaffold background color animation from light to dark theme. await tester.pumpWidget(buildWidget(themeMode: ThemeMode.dark)); await tester.pump(); await tester.pump(const Duration(milliseconds: 50)); // Advance animation by 50 milliseconds. // Scaffold background color is slightly updated. expect( tester.widget(find.byType(Material)).color, isSameColorAs(const Color(0xffc3bdc5)), ); // Let the animation finish. await tester.pumpAndSettle(); // Scaffold background color is fully updated to dark theme. expect( tester.widget(find.byType(Material)).color, isSameColorAs(darkTheme.colorScheme.surface), ); // Reset to light theme to compare the Scaffold background color animation // with the default animation curve. await tester.pumpWidget(buildWidget()); await tester.pumpAndSettle(); // Switch to dark theme with overridden animation curve. await tester.pumpWidget( buildWidget( themeMode: ThemeMode.dark, animationStyle: const AnimationStyle(curve: Curves.easeIn), ), ); await tester.pump(); await tester.pump(const Duration(milliseconds: 50)); // Scaffold background color is slightly updated but with a different // color than the default animation curve. expect( tester.widget(find.byType(Material)).color, isSameColorAs(const Color(0xffe7e1e9)), ); // Let the animation finish. await tester.pumpAndSettle(); // Scaffold background color is fully updated to dark theme. expect( tester.widget(find.byType(Material)).color, isSameColorAs(darkTheme.colorScheme.surface), ); // Switch from dark to light theme with overridden animation duration. await tester.pumpWidget(buildWidget(animationStyle: AnimationStyle.noAnimation)); await tester.pump(); await tester.pump(const Duration(milliseconds: 1)); expect( tester.widget(find.byType(Material)).color, isNot(darkTheme.colorScheme.surface), ); expect( tester.widget(find.byType(Material)).color, isSameColorAs(lightTheme.colorScheme.surface), ); }); testWidgets('AnimationStyle.noAnimation removes AnimatedTheme from the tree', ( WidgetTester tester, ) async { await tester.pumpWidget(const MaterialApp(themeAnimationStyle: AnimationStyle())); expect(find.byType(AnimatedTheme), findsOneWidget); expect(find.byType(Theme), findsOneWidget); await tester.pumpWidget(const MaterialApp(themeAnimationStyle: AnimationStyle.noAnimation)); expect(find.byType(AnimatedTheme), findsNothing); expect(find.byType(Theme), findsOneWidget); }); // Regression test for https://github.com/flutter/flutter/issues/137875. testWidgets('MaterialApp works in an unconstrained environment', (WidgetTester tester) async { await tester.pumpWidget( const UnconstrainedBox(child: MaterialApp(home: SizedBox(width: 123, height: 456))), ); expect(tester.getSize(find.byType(MaterialApp)), const Size(123, 456)); }); // Regression test for https://github.com/flutter/flutter/issues/156959. testWidgets( 'MaterialApp with builder works when themeAnimationStyle is AnimationStyle.noAnimation', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( themeAnimationStyle: AnimationStyle.noAnimation, builder: (BuildContext context, Widget? child) { return const Text('Works'); }, ), ); expect(find.text('Works'), findsOne); }, ); testWidgets('MaterialApp does not crash at zero area', (WidgetTester tester) async { await tester.pumpWidget( const Center( child: SizedBox.shrink(child: MaterialApp(home: Text('X'))), ), ); expect(tester.getSize(find.byType(MaterialApp)), Size.zero); }); } class MockScrollBehavior extends ScrollBehavior { const MockScrollBehavior(); @override ScrollPhysics getScrollPhysics(BuildContext context) => const NeverScrollableScrollPhysics(); } typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext context, RouteInformation information); typedef SimpleNavigatorRouterDelegatePopPage = bool Function(Route route, T result, SimpleNavigatorRouterDelegate delegate); class SimpleRouteInformationParser extends RouteInformationParser { SimpleRouteInformationParser(); @override Future parseRouteInformation(RouteInformation information) { return SynchronousFuture(information); } @override RouteInformation restoreRouteInformation(RouteInformation configuration) { return configuration; } } class SimpleNavigatorRouterDelegate extends RouterDelegate with PopNavigatorRouterDelegateMixin, ChangeNotifier { SimpleNavigatorRouterDelegate({required this.builder, required this.onPopPage}) { ChangeNotifier.maybeDispatchObjectCreation(this); } @override GlobalKey navigatorKey = GlobalKey(); RouteInformation get routeInformation => _routeInformation; late RouteInformation _routeInformation; set routeInformation(RouteInformation newValue) { _routeInformation = newValue; notifyListeners(); } SimpleRouterDelegateBuilder builder; SimpleNavigatorRouterDelegatePopPage onPopPage; @override Future setNewRoutePath(RouteInformation configuration) { _routeInformation = configuration; return SynchronousFuture(null); } bool _handlePopPage(Route route, void data) { return onPopPage(route, data, this); } @override Widget build(BuildContext context) { return Navigator( key: navigatorKey, onPopPage: _handlePopPage, pages: >[ // We need at least two pages for the pop to propagate through. // Otherwise, the navigator will bubble the pop to the system navigator. const MaterialPage(child: Text('base')), MaterialPage( key: ValueKey(routeInformation.uri.toString()), child: builder(context, routeInformation), ), ], ); } }