-
Notifications
You must be signed in to change notification settings - Fork 29.8k
Add CupertinoPageRouteWithDynamicTitle for dynamic title support #175129 #175303
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
|
An existing Git SHA, To re-trigger presubmits after closing or re-opeing a PR, or pushing a HEAD commit (i.e. with |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces CupertinoPageRouteWithDynamicTitle, a new route class that supports dynamic title updates via a ValueListenable. This is a valuable addition for creating more dynamic and responsive iOS-style navigation. The changes also include modifications to Route.changedInternalState to allow notifying adjacent routes of state changes, which is essential for the dynamic title feature to work correctly. The implementation is well-documented and includes a comprehensive set of tests. My review includes suggestions for improving code clarity in the new route class and enhancing the test suite to ensure full coverage of the new functionality.
| }) { | ||
| assert(opaque); | ||
| _titleListener = () { | ||
| // Notify neighboring routes when title changes | ||
| changedInternalState(notifyNeighbors: true); | ||
| }; | ||
| titleListenable.addListener(_titleListener!); | ||
| } | ||
|
|
||
| @override | ||
| DelegatedTransitionBuilder? get delegatedTransition => | ||
| CupertinoPageTransition.delegatedTransition; | ||
|
|
||
| /// Builds the primary contents of the route. | ||
| final WidgetBuilder builder; | ||
|
|
||
| @override | ||
| Widget buildContent(BuildContext context) => builder(context); | ||
|
|
||
| /// The dynamic title for this route. | ||
| /// | ||
| /// When the value of this [ValueListenable] changes, neighboring routes | ||
| /// will be notified to update their references to this route's title. | ||
| final ValueListenable<String?> titleListenable; | ||
|
|
||
| @override | ||
| String? get title => titleListenable.value; | ||
|
|
||
| @override | ||
| final bool maintainState; | ||
|
|
||
| @override | ||
| String get debugLabel => '${super.debugLabel}(${settings.name})'; | ||
|
|
||
| VoidCallback? _titleListener; | ||
|
|
||
| @override | ||
| void dispose() { | ||
| if (_titleListener != null) { | ||
| titleListenable.removeListener(_titleListener!); | ||
| } | ||
| super.dispose(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The listener management for titleListenable can be simplified for better readability and robustness. Instead of using a nullable VoidCallback? _titleListener field, you can define a private method to handle title changes. This method can then be directly passed to addListener and removeListener, avoiding the need for a nullable field, null checks, and force unwraps.
}) {
assert(opaque);
titleListenable.addListener(_handleTitleChanged);
}
@override
DelegatedTransitionBuilder? get delegatedTransition =>
CupertinoPageTransition.delegatedTransition;
/// Builds the primary contents of the route.
final WidgetBuilder builder;
@override
Widget buildContent(BuildContext context) => builder(context);
/// The dynamic title for this route.
///
/// When the value of this [ValueListenable] changes, neighboring routes
/// will be notified to update their references to this route's title.
final ValueListenable<String?> titleListenable;
@override
String? get title => titleListenable.value;
@override
final bool maintainState;
@override
String get debugLabel => '${super.debugLabel}(${settings.name})';
void _handleTitleChanged() {
// Notify neighboring routes when title changes
changedInternalState(notifyNeighbors: true);
}
@override
void dispose() {
titleListenable.removeListener(_handleTitleChanged);
super.dispose();
}| // Verify initial state | ||
| // Check that the navigation bar is present | ||
| expect(find.byType(CupertinoNavigationBar), findsOneWidget); | ||
| // Check that the second page content is displayed | ||
| expect(find.text('Second Page'), findsOneWidget); | ||
|
|
||
| // Update first route title | ||
| firstTitleNotifier.value = 'Updated First Page'; | ||
| await tester.pump(); | ||
|
|
||
| // The second route should now show the updated first page title in its back button | ||
| // Verify that the navigation bar is still present after title update | ||
| expect(find.byType(CupertinoNavigationBar), findsOneWidget); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test correctly sets up the scenario for dynamic title updates but doesn't verify that the back button's title on the second page actually updates. To make the test more robust, you should add assertions to check for the updated title text in the back button.
| // Verify initial state | |
| // Check that the navigation bar is present | |
| expect(find.byType(CupertinoNavigationBar), findsOneWidget); | |
| // Check that the second page content is displayed | |
| expect(find.text('Second Page'), findsOneWidget); | |
| // Update first route title | |
| firstTitleNotifier.value = 'Updated First Page'; | |
| await tester.pump(); | |
| // The second route should now show the updated first page title in its back button | |
| // Verify that the navigation bar is still present after title update | |
| expect(find.byType(CupertinoNavigationBar), findsOneWidget); | |
| // Verify initial state | |
| final Finder backButton = find.byType(CupertinoNavigationBarBackButton); | |
| expect(backButton, findsOneWidget); | |
| expect(find.descendant(of: backButton, matching: find.text('First Page')), findsOneWidget); | |
| expect(find.text('Second Page'), findsOneWidget); | |
| // Update first route title | |
| firstTitleNotifier.value = 'Updated First Page'; | |
| await tester.pump(); | |
| // The second route should now show the updated first page title in its back button. | |
| expect(find.descendant(of: backButton, matching: find.text('Updated First Page')), findsOneWidget); | |
| expect(find.descendant(of: backButton, matching: find.text('First Page')), findsNothing); |
| testWidgets('works with CupertinoNavigationBar', (WidgetTester tester) async { | ||
| final ValueNotifier<String?> titleNotifier = ValueNotifier<String?>('Initial Title'); | ||
|
|
||
| await tester.pumpWidget( | ||
| CupertinoApp( | ||
| home: Builder( | ||
| builder: (BuildContext context) => CupertinoButton( | ||
| onPressed: () { | ||
| Navigator.push( | ||
| context, | ||
| CupertinoPageRouteWithDynamicTitle<void>( | ||
| builder: (BuildContext context) => const CupertinoPageScaffold( | ||
| navigationBar: CupertinoNavigationBar( | ||
| middle: Text('Navigation Bar'), | ||
| ), | ||
| child: Text('Page Content'), | ||
| ), | ||
| titleListenable: titleNotifier, | ||
| ), | ||
| ); | ||
| }, | ||
| child: const Text('Push Route'), | ||
| ), | ||
| ), | ||
| ), | ||
| ); | ||
|
|
||
| await tester.tap(find.text('Push Route')); | ||
| await tester.pumpAndSettle(); | ||
|
|
||
| // Check that the navigation bar is present | ||
| expect(find.byType(CupertinoNavigationBar), findsOneWidget); | ||
|
|
||
| // Update the title | ||
| titleNotifier.value = 'Updated Title'; | ||
| await tester.pump(); | ||
|
|
||
| // Check that the navigation bar is still present after title update | ||
| expect(find.byType(CupertinoNavigationBar), findsOneWidget); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| testWidgets('notifies neighboring routes when called with notifyNeighbors=true', (WidgetTester tester) async { | ||
| final ValueNotifier<String?> titleNotifier = ValueNotifier<String?>('Initial Title'); | ||
| bool didChangePreviousCalled = false; | ||
| bool didChangeNextCalled = false; | ||
|
|
||
| // Create a custom route that tracks when didChangePrevious/didChangeNext are called | ||
| final _TestRoute customRoute = _TestRoute( | ||
| titleListenable: titleNotifier, | ||
| onDidChangePrevious: () => didChangePreviousCalled = true, | ||
| onDidChangeNext: () => didChangeNextCalled = true, | ||
| ); | ||
|
|
||
| await tester.pumpWidget( | ||
| CupertinoApp( | ||
| home: Builder( | ||
| builder: (BuildContext context) => CupertinoButton( | ||
| onPressed: () { | ||
| Navigator.push(context, customRoute); | ||
| }, | ||
| child: const Text('Push Route'), | ||
| ), | ||
| ), | ||
| ), | ||
| ); | ||
|
|
||
| await tester.tap(find.text('Push Route')); | ||
| await tester.pumpAndSettle(); | ||
|
|
||
| // Reset flags | ||
| didChangePreviousCalled = false; | ||
| didChangeNextCalled = false; | ||
|
|
||
| // Call changedInternalState with notifyNeighbors=true | ||
| customRoute.changedInternalState(notifyNeighbors: true); | ||
| await tester.pump(); | ||
|
|
||
| // Since this is the only route, no neighbors should be notified | ||
| expect(didChangePreviousCalled, isFalse); | ||
| expect(didChangeNextCalled, isFalse); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test 'notifies neighboring routes when called with notifyNeighbors=true' only verifies the scenario where the route has no neighbors. To ensure the feature is fully tested, consider adding another test case where the route has both a previous and a next route, and assert that didChangePrevious and didChangeNext are called on the respective neighbors.
|
An existing Git SHA, To re-trigger presubmits after closing or re-opeing a PR, or pushing a HEAD commit (i.e. with |
3cd8950 to
f750c90
Compare
This commit introduces the `CupertinoPageRouteWithDynamicTitle` class, which extends `CupertinoPageRoute` to allow dynamic updates to the route title using a `ValueListenable<String?>`. When the title changes, neighboring routes are notified to update their references, enhancing navigation bar title management in iOS-designed apps. Additionally, the `changedInternalState` method in the `Route` class is updated to support notifying neighboring routes about state changes. Tests are added to ensure the correct behavior of dynamic title updates and neighbor notifications. - Added `CupertinoPageRouteWithDynamicTitle` for dynamic title management. - Updated `changedInternalState` to notify neighboring routes. - Comprehensive tests for dynamic title updates and neighbor notifications included.
f750c90 to
2eb6066
Compare
MitchellGoodwin
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure that this is something we want, or at least that this is the approach we want. This is a pretty engineered change for a specific case. The title of a route is generally not something that is considered to change while open very often, similar to how the url of the web page you're on isn't expected to change without you redirecting. Having the previousPageTitle change depending on state from the previous page can make some amount of sense, but I don't think we want that to be done this way. It might be better done through state management set up by the app developer. I don't know if we want to open that can of worms. There's a lot of edge cases that I'm not sure we want to take on, like the page title updating mid page transition, it updating while off screen, etc.
| @protected | ||
| @mustCallSuper | ||
| void changedInternalState() {} | ||
| void changedInternalState({bool notifyNeighbors = false}) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a pretty big change to the core navigator behavior that I'm not sure we want to change for this specific case.
thanks, it make sense . i am leaving from it |
@MitchellGoodwin That's the problem. I've tried to do it in go_router and came to a conclusion that you simply cant. Not without a proper support from the framework. Once page object goes to router it's no longer changes and you cant use state management because pages/routes are not widgets. I ask you to reconsider, since this is not possible to to from userland currently. See #175129 for a use case. (Update title when data arrives). Another use case is localization change, so your back button will update for a new language while you're still in a settings page. As a side note
That is not universally true for search forms. It is expected that url changes while you type, so hard reload wont reset your query, same with tags selection and chips that have query parameter equivalent. You're staying on same page, changing your filters and URL updates accordingly without redirecting to any new destination. |
Introduces a new route class, CupertinoPageRouteWithDynamicTitle, which extends CupertinoPageRoute to allow dynamic updates of the route title using a ValueListenable<String?>. This change enables neighboring routes to be notified of title changes, enhancing navigation bar functionality in iOS-designed apps.
Additionally, updates to the Route class's changedInternalState method allow for notifying adjacent routes about state changes. Comprehensive tests have been added to ensure the correct behavior of dynamic title updates and neighbor notifications.
Fix: #175129
Demo Code
Pre-launch Checklist
///).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. Comments from the
gemini-code-assistbot 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.