Repository: flutter
Issue: MaterialPage and Page Equality Broken in Navigator Comparison
Labels: framework,f: material design,f: routes,P3,team-framework,triaged-framework

### Steps to reproduce

The Navigator widget's `pages` property accepts a list a pages. The issue is that the internal logic for comparing page quality doesn't compare Page equality correctly.

This results in all the pages in the navigation stack rebuilding, which is bad for performance.

**Example**

![image](https://github.com/user-attachments/assets/6e4d9fff-1bcd-482d-9375-768ee6695d36)

In this example, you can toggle between recreating the page list versus maintaining the instances of the page models. When the pages are recreated, every route is rebuilt when navigating. Watch the console for duplicate build print statements.

**Console Log**

> Add New Page 3
>
> Rebuild Page 3
> Rebuild Page 2
> Rebuild Page 1
> Rebuild Page 0

**Page Equality Tests**

Tests demonstrating Page equality is only preserved by using the same instance. Even Pages with the exact same properties are not equal.

<details><summary>Code</summary>
<p>

```dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  group('MaterialPage Equality Tests', () {
    test('MaterialPages with same properties should be equal', () {
      final page1 = MaterialPage(
        key: const ValueKey('test'),
        name: 'TestPage',
        arguments: {'id': 1},
        child: const Text('Test'),
      );

      final page2 = MaterialPage(
        key: const ValueKey('test'),
        name: 'TestPage',
        arguments: {'id': 1},
        child: const Text('Test'),
      );

      expect(page1 == page2, isTrue,
          reason:
              'MaterialPages with identical properties should be considered equal');
    });

    test('List of MaterialPages considers new instances as different', () {
      final List<Page> originalPages = [
        MaterialPage(
          key: const ValueKey('page1'),
          name: 'Page1',
          child: const Text('Page 1'),
        ),
        MaterialPage(
          key: const ValueKey('page2'),
          name: 'Page2',
          child: const Text('Page 2'),
        ),
      ];

      final List<Page> newPages = [
        MaterialPage(
          key: const ValueKey('page1'),
          name: 'Page1',
          child: const Text('Page 1'),
        ),
        MaterialPage(
          key: const ValueKey('page2'),
          name: 'Page2',
          child: const Text('Page 2'),
        ),
      ];

      expect(listEquals(originalPages, newPages), isTrue,
          reason:
              'Lists with new instances of MaterialPages are incorrectly considered different');
    });
  });
}
```

</p>
</details> 

### Expected results

The key expectation is that MaterialPage instances should be considered equal if they have the same key and essential properties, even if they are separate instances. This expectation aligns with how Flutter handles widget rebuilding in other contexts. For example, when a StatelessWidget or StatefulWidget is rebuilt with the same key and type, Flutter optimizes the rebuild process, understanding that the underlying structure remains the same.

In the context of navigation, this behavior is particularly important. When updating the navigation stack, developers often recreate the list of pages, perhaps adding or removing pages based on the application's state. The expectation is that if a MaterialPage in the new list has the same key and essential properties (like name, arguments, and the type of the child widget) as a page in the existing list, it should be treated as the same page. This would allow the Navigator to optimize its update process, avoiding unnecessary rebuilds of routes that haven't fundamentally changed.

The use of keys in Flutter is generally a signal for maintaining identity across rebuilds. When a developer assigns a key to a MaterialPage, they are essentially saying, "This page represents the same logical screen or route, even if I'm creating a new instance of it." The navigation system should respect this intent, using the key as the primary means of identifying and comparing pages.

I expect the Navigator to treat these two MaterialPages as the same.

```dart
          MaterialPage(
            key: ValueKey('page_1'),
            child: TestPage(title: 'Page 1'),
          ),
```

should be treated as the same page by the Navigator as 

```dart
          MaterialPage(
            key: ValueKey('page_1'),
            child: TestPage(title: 'Page 1'),
          ),
```

The keys are the same!

### Actual results

The internal logic of the Navigator does not respect the Page's Key for equality. The reason this happens is when a new MaterialPage is created, even with the same properties, it gets a new identity in memory. If it doesn't have a specific LocalKey set in the Navigator, this new identity causes it to be treated as a different page.
This means that even though two MaterialPages are equal, the Navigator might still treat them as different due to their memory identity and an existing LocalKey not being assigned.

This doesn't match the documented or expected behavior that says pages are compared via Keys.

`navigator.dart`
https://github.com/flutter/flutter/blob/82ebb74c6411afc55a8b0937535d6baeb03ea826/packages/flutter/lib/src/widgets/navigator.dart#L4173

```dart
   if (
     nextPage.key == null ||
     !pageKeyToOldEntry.containsKey(nextPage.key) ||
     !pageKeyToOldEntry[nextPage.key]!.canUpdateFrom(nextPage)
   ) {
     // Create a new entry
   } else {
     // Update existing entry
     final _RouteEntry matchingEntry = pageKeyToOldEntry.remove(nextPage.key)!;
     matchingEntry.route._updateSettings(nextPage);
     newHistory.add(matchingEntry);
   }
```



### Code sample

<details open><summary>Code sample</summary>

```dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Navigation Rebuild Test',
      home: const NavigationRebuildTest(),
    );
  }
}

class NavigationRebuildTest extends StatefulWidget {
  const NavigationRebuildTest({super.key});

  @override
  State<NavigationRebuildTest> createState() => _NavigationRebuildTestState();
}

class _NavigationRebuildTestState extends State<NavigationRebuildTest> {
  List<Page<dynamic>> currentPages = [];
  bool recreateList = true;

  @override
  void initState() {
    super.initState();
    currentPages = [
      MaterialPage(
        key: const ValueKey('page1'),
        child: const TestPage(title: 'Page 1'),
      ),
    ];
  }

  void _addPage() {
    setState(() {
      if (recreateList) {
        currentPages = [
          ...currentPages.map((page) {
            if (page is MaterialPage) {
              return MaterialPage(
                key: page.key,
                name: page.name,
                arguments: page.arguments,
                fullscreenDialog: page.fullscreenDialog,
                child: page.child,
              );
            }
            return page;
          }),
          MaterialPage(
            key: ValueKey('page${currentPages.length + 1}'),
            child: TestPage(title: 'Page ${currentPages.length + 1}'),
          ),
        ];
      } else {
        currentPages = [
          ...currentPages,
          MaterialPage(
            key: ValueKey('page${currentPages.length + 1}'),
            child: TestPage(title: 'Page ${currentPages.length + 1}'),
          ),
        ];
      }
    });
  }

  void _toggleRecreateList(bool value) {
    setState(() {
      recreateList = value;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Navigation Rebuild Test'),
        actions: [
          Row(
            children: [
              const Text('Recreate List'),
              Switch(
                value: recreateList,
                onChanged: _toggleRecreateList,
              ),
            ],
          ),
        ],
      ),
      body: Navigator(
        pages: currentPages,
        onPopPage: (route, result) {
          if (!route.didPop(result)) {
            return false;
          }
          setState(() {
            currentPages.removeLast();
          });
          return true;
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _addPage,
        child: const Icon(Icons.add),
      ),
    );
  }
}

class TestPage extends StatelessWidget {
  final String title;

  const TestPage({super.key, required this.title});

  @override
  Widget build(BuildContext context) {
    debugPrint('Building $title'); // This print statement helps track rebuilds

    return Scaffold(
      appBar: AppBar(title: Text(title)),
      body: Center(child: Text(title)),
    );
  }
}
```

</details>


### Screenshots or Video

<details open>
<summary>Screenshots / Video demonstration</summary>

[Upload media here]

</details>


### Logs

<details open><summary>Logs</summary>

```console
[Paste your logs here]
```

</details>


### Flutter Doctor output

<details open><summary>Doctor output</summary>

```console
[√] Flutter (Channel stable, 3.24.3, on Microsoft Windows [Version 10.0.22631.4317], locale en-US)
    • Flutter version 3.24.3 on channel stable at D:\flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 2663184aa7 (4 weeks ago), 2024-09-11 16:27:48 -0500
    • Engine revision 36335019a8
    • Dart version 3.5.3
    • DevTools version 2.37.3

[√] Windows Version (Installed version of Windows is version 10 or higher)

[!] Android toolchain - develop for Android devices (Android SDK version 35.0.0)
    • Android SDK at C:\Users\searc\AppData\Local\Android\sdk
    • Platform android-35, build-tools 35.0.0
    • Java binary at: C:\Program Files\Android\Android Studio\jbr\bin\java
    • Java version OpenJDK Runtime Environment (build 21.0.3+-12282718-b509.11)
    ! Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses

[X] Chrome - develop for the web (Cannot find Chrome executable at .\Google\Chrome\Application\chrome.exe)
    ! Cannot find Chrome. Try setting CHROME_EXECUTABLE to a Chrome executable.

[√] Visual Studio - develop Windows apps (Visual Studio Community 2022 17.9.1)
    • Visual Studio at C:\Program Files\Microsoft Visual Studio\2022\Community
    • Visual Studio Community 2022 version 17.9.34616.47
    • Windows 10 SDK version 10.0.22621.0

[√] Android Studio (version 2024.2)
    • Android Studio at C:\Program Files\Android\Android Studio
    • Flutter plugin can be installed from:
       https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
       https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 21.0.3+-12282718-b509.11)

[√] VS Code (version 1.91.0)
    • VS Code at C:\Users\searc\AppData\Local\Programs\Microsoft VS Code
    • Flutter extension version 3.98.0

[√] Connected device (3 available)
    • ONEPLUS A6013 (mobile) • 26bd994b • android-arm64  • Android 11 (API 30)
    • Windows (desktop)      • windows  • windows-x64    • Microsoft Windows [Version 10.0.22631.4317]
    • Edge (web)             • edge     • web-javascript • Microsoft Edge 129.0.2792.79

[√] Network resources
    • All expected network resources are available.
```

</details>