Kate Lovett 9d96df2364
Modernize framework lints (#179089)
WIP

Commits separated as follows:
- Update lints in analysis_options files
- Run `dart fix --apply`
- Clean up leftover analysis issues 
- Run `dart format .` in the right places.

Local analysis and testing passes. Checking CI now.

Part of https://github.com/flutter/flutter/issues/178827
- Adoption of flutter_lints in examples/api coming in a separate change
(cc @loic-sharma)

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-11-26 01:10:39 +00:00

455 lines
14 KiB
Dart

// 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/material.dart';
import 'package:flutter_test/flutter_test.dart';
bool willPopValue = false;
class SamplePage extends StatefulWidget {
const SamplePage({super.key});
@override
SamplePageState createState() => SamplePageState();
}
class SamplePageState extends State<SamplePage> {
ModalRoute<void>? _route;
Future<bool> _callback() async => willPopValue;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_route?.removeScopedWillPopCallback(_callback);
_route = ModalRoute.of(context);
_route?.addScopedWillPopCallback(_callback);
}
@override
void dispose() {
super.dispose();
_route?.removeScopedWillPopCallback(_callback);
}
@override
Widget build(BuildContext context) {
return Scaffold(appBar: AppBar(title: const Text('Sample Page')));
}
}
int willPopCount = 0;
class SampleForm extends StatelessWidget {
const SampleForm({super.key, required this.callback});
final WillPopCallback callback;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample Form')),
body: SizedBox.expand(
child: Form(
onWillPop: () {
willPopCount += 1;
return callback();
},
child: const TextField(),
),
),
);
}
}
// Expose the protected hasScopedWillPopCallback getter
class _TestPageRoute<T> extends MaterialPageRoute<T> {
_TestPageRoute({super.settings, required super.builder}) : super(maintainState: true);
bool get hasCallback => super.hasScopedWillPopCallback;
}
class _TestPage extends Page<dynamic> {
_TestPage({required this.builder, required LocalKey key}) : _key = GlobalKey(), super(key: key);
final WidgetBuilder builder;
final GlobalKey<dynamic> _key;
@override
Route<dynamic> createRoute(BuildContext context) {
return _TestPageRoute<dynamic>(
settings: this,
builder: (BuildContext context) {
// keep state during move to another location in tree
return KeyedSubtree(key: _key, child: builder.call(context));
},
);
}
}
void main() {
testWidgets('ModalRoute scopedWillPopupCallback can inhibit back button', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Builder(
builder: (BuildContext context) {
return Center(
child: TextButton(
child: const Text('X'),
onPressed: () {
showDialog<void>(
context: context,
builder: (BuildContext context) => const SamplePage(),
);
},
),
);
},
),
),
),
);
expect(find.byTooltip('Back'), findsNothing);
expect(find.text('Sample Page'), findsNothing);
await tester.tap(find.text('X'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(find.text('Sample Page'), findsOneWidget);
willPopValue = false;
await tester.tap(find.byTooltip('Back'));
await tester.pump();
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(find.text('Sample Page'), findsOneWidget);
// Use didPopRoute() to simulate the system back button. Check that
// didPopRoute() indicates that the notification was handled.
final dynamic widgetsAppState = tester.state(find.byType(WidgetsApp));
// ignore: avoid_dynamic_calls
expect(await widgetsAppState.didPopRoute(), isTrue);
expect(find.text('Sample Page'), findsOneWidget);
willPopValue = true;
await tester.tap(find.byTooltip('Back'));
await tester.pump();
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(find.text('Sample Page'), findsNothing);
});
testWidgets('willPop will only pop if the callback returns true', (WidgetTester tester) async {
Widget buildFrame() {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Builder(
builder: (BuildContext context) {
return Center(
child: TextButton(
child: const Text('X'),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) {
return SampleForm(callback: () => Future<bool>.value(willPopValue));
},
),
);
},
),
);
},
),
),
);
}
await tester.pumpWidget(buildFrame());
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
expect(find.text('Sample Form'), findsOneWidget);
// Should pop if callback returns true
willPopValue = true;
await tester.tap(find.byTooltip('Back'));
await tester.pumpAndSettle();
expect(find.text('Sample Form'), findsNothing);
});
testWidgets('Form.willPop can inhibit back button', (WidgetTester tester) async {
Widget buildFrame() {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Builder(
builder: (BuildContext context) {
return Center(
child: TextButton(
child: const Text('X'),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) {
return SampleForm(callback: () => Future<bool>.value(willPopValue));
},
),
);
},
),
);
},
),
),
);
}
await tester.pumpWidget(buildFrame());
await tester.tap(find.text('X'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(find.text('Sample Form'), findsOneWidget);
willPopValue = false;
willPopCount = 0;
await tester.tap(find.byTooltip('Back'));
await tester.pump(); // Start the pop "back" operation.
await tester.pump(); // Complete the willPop() Future.
await tester.pump(const Duration(seconds: 1)); // Wait until it has finished.
expect(find.text('Sample Form'), findsOneWidget);
expect(willPopCount, 1);
willPopValue = true;
willPopCount = 0;
await tester.tap(find.byTooltip('Back'));
await tester.pump(); // Start the pop "back" operation.
await tester.pump(); // Complete the willPop() Future.
await tester.pump(const Duration(seconds: 1)); // Wait until it has finished.
expect(find.text('Sample Form'), findsNothing);
expect(willPopCount, 1);
});
testWidgets('Form.willPop callbacks do not accumulate', (WidgetTester tester) async {
Future<bool> showYesNoAlert(BuildContext context) async {
return (await showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
actions: <Widget>[
TextButton(
child: const Text('YES'),
onPressed: () {
Navigator.of(context).pop(true);
},
),
TextButton(
child: const Text('NO'),
onPressed: () {
Navigator.of(context).pop(false);
},
),
],
);
},
))!;
}
Widget buildFrame() {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Builder(
builder: (BuildContext context) {
return Center(
child: TextButton(
child: const Text('X'),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) {
return SampleForm(callback: () => showYesNoAlert(context));
},
),
);
},
),
);
},
),
),
);
}
await tester.pumpWidget(buildFrame());
await tester.tap(find.text('X'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(find.text('Sample Form'), findsOneWidget);
// Press the Scaffold's back button. This causes the willPop callback
// to run, which shows the YES/NO Alert Dialog. Veto the back operation
// by pressing the Alert's NO button.
await tester.tap(find.byTooltip('Back'));
await tester.pump(); // Start the pop "back" operation.
await tester.pump(); // Call willPop which will show an Alert.
await tester.tap(find.text('NO'));
await tester.pump(); // Start the dismiss animation.
await tester.pump(); // Resolve the willPop callback.
await tester.pump(const Duration(seconds: 1)); // Wait until it has finished.
expect(find.text('Sample Form'), findsOneWidget);
// Do it again.
// Each time the Alert is shown and dismissed the FormState's
// didChangeDependencies() method runs. We're making sure that the
// didChangeDependencies() method doesn't add an extra willPop callback.
await tester.tap(find.byTooltip('Back'));
await tester.pump(); // Start the pop "back" operation.
await tester.pump(); // Call willPop which will show an Alert.
await tester.tap(find.text('NO'));
await tester.pump(); // Start the dismiss animation.
await tester.pump(); // Resolve the willPop callback.
await tester.pump(const Duration(seconds: 1)); // Wait until it has finished.
expect(find.text('Sample Form'), findsOneWidget);
// This time really dismiss the SampleForm by pressing the Alert's
// YES button.
await tester.tap(find.byTooltip('Back'));
await tester.pump(); // Start the pop "back" operation.
await tester.pump(); // Call willPop which will show an Alert.
await tester.tap(find.text('YES'));
await tester.pump(); // Start the dismiss animation.
await tester.pump(); // Resolve the willPop callback.
await tester.pump(const Duration(seconds: 1)); // Wait until it has finished.
expect(find.text('Sample Form'), findsNothing);
});
testWidgets('Route.scopedWillPop callbacks do not accumulate', (WidgetTester tester) async {
late StateSetter contentsSetState; // call this to rebuild the route's SampleForm contents
var contentsEmpty = false; // when true, don't include the SampleForm in the route
final route = _TestPageRoute<void>(
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
contentsSetState = setState;
return contentsEmpty
? Container()
: SampleForm(key: UniqueKey(), callback: () async => false);
},
);
},
);
Widget buildFrame() {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Builder(
builder: (BuildContext context) {
return Center(
child: TextButton(
child: const Text('X'),
onPressed: () {
Navigator.of(context).push(route);
},
),
);
},
),
),
);
}
await tester.pumpWidget(buildFrame());
await tester.tap(find.text('X'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(find.text('Sample Form'), findsOneWidget);
expect(route.hasCallback, isTrue);
// Rebuild the route's SampleForm child an additional 3x for good measure.
contentsSetState(() {});
await tester.pump();
contentsSetState(() {});
await tester.pump();
contentsSetState(() {});
await tester.pump();
// Now build the route's contents without the sample form.
contentsEmpty = true;
contentsSetState(() {});
await tester.pump();
expect(route.hasCallback, isFalse);
});
testWidgets('should handle new route if page moved from one navigator to another', (
WidgetTester tester,
) async {
// Regression test for https://github.com/flutter/flutter/issues/89133
late StateSetter contentsSetState;
var moveToAnotherNavigator = false;
final pages = <Page<dynamic>>[
_TestPage(
key: UniqueKey(),
builder: (BuildContext context) {
return WillPopScope(onWillPop: () async => true, child: const Text('anchor'));
},
),
];
Widget buildNavigator(Key? key, List<Page<dynamic>> pages) {
return Navigator(
key: key,
pages: pages,
onPopPage: (Route<dynamic> route, dynamic result) {
return route.didPop(result);
},
);
}
Widget buildFrame() {
return MaterialApp(
home: Scaffold(
body: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
contentsSetState = setState;
if (moveToAnotherNavigator) {
return buildNavigator(const ValueKey<int>(1), pages);
}
return buildNavigator(const ValueKey<int>(2), pages);
},
),
),
);
}
await tester.pumpWidget(buildFrame());
await tester.pump();
final route1 = ModalRoute.of(tester.element(find.text('anchor')))! as _TestPageRoute<dynamic>;
expect(route1.hasCallback, isTrue);
moveToAnotherNavigator = true;
contentsSetState(() {});
await tester.pump();
final route2 = ModalRoute.of(tester.element(find.text('anchor')))! as _TestPageRoute<dynamic>;
expect(route1.hasCallback, isFalse);
expect(route2.hasCallback, isTrue);
});
}