JaspervanRiet / duck_router

A Flutter router with intents
MIT License
4 stars 3 forks source link

Moving the Navigator in the widget tree crashes #40

Closed passsy closed 3 days ago

passsy commented 3 days ago

I'm the author of Wiredash and a user raised an issue having troubles integrating Wiredash within their app which uses duck_router.

I prepared this minimal example demonstrating the bug without the wiredash sdk: Using Flutter 3.24.3, duck_router 5.1.2

import 'package:duck_router/duck_router.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class HomeLocation extends Location {
  @override
  String get path => 'home';

  @override
  LocationBuilder? get builder => (context) => const Home();
}

class Home extends StatefulWidget {
  const Home({super.key});

  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            context.findAncestorStateOfType<_MyAppState>()!.withBorder = true;
          },
          child: const Text('Wrap app with border'),
        ),
      ),
    );
  }
}

final router = DuckRouter(initialLocation: HomeLocation());

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  bool _withBorder = false;

  bool get withBorder => _withBorder;

  set withBorder(bool value) {
    setState(() {
      _withBorder = value;
    });
  }

  final appKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    Widget child = KeyedSubtree(
      key: appKey,
      child: MaterialApp.router(
        title: 'Flutter Demo',
        routerConfig: router,
      ),
    );

    if (withBorder) {
      child = ColoredBox(
        color: Colors.blue,
        child: Padding(
          padding: const EdgeInsets.all(20),
          child: child,
        ),
      );
    }

    return child;
  }
}

Clicking the "Wrap app with border" button causes this crash:

======== Exception caught by widgets library =======================================================
The following assertion was thrown building HeroControllerScope:
'package:flutter/src/widgets/navigator.dart': Failed assertion: line 3931 pos 18: '!keyReservation.contains(key)': is not true.

Either the assertion indicates an error in the framework itself, or we should provide substantially more information in this error message to help you determine and fix the underlying cause.
In either case, please report this assertion by filing a bug on GitHub:
  https://github.com/flutter/flutter/issues/new?template=2_bug.yml

The relevant error-causing widget was: 
  MaterialApp MaterialApp:file:///Users/pascalwelsch/Downloads/test_chat/lib/mini_main.dart:65:26
When the exception was thrown, this was the stack: 
#2      NavigatorState._debugCheckDuplicatedPageKeys.<anonymous closure> (package:flutter/src/widgets/navigator.dart:3931:18)
#3      NavigatorState._debugCheckDuplicatedPageKeys (package:flutter/src/widgets/navigator.dart:3936:6)
#4      NavigatorState._updatePages.<anonymous closure> (package:flutter/src/widgets/navigator.dart:3999:7)
#5      NavigatorState._updatePages (package:flutter/src/widgets/navigator.dart:4002:6)
#6      NavigatorState.didUpdateWidget (package:flutter/src/widgets/navigator.dart:3911:7)
#7      StatefulElement.update (package:flutter/src/widgets/framework.dart:5789:55)
#8      Element.updateChild (package:flutter/src/widgets/framework.dart:3941:15)
#9      ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5642:16)
#10     Element.rebuild (package:flutter/src/widgets/framework.dart:5333:7)
#11     ProxyElement.update (package:flutter/src/widgets/framework.dart:5946:5)
#12     Element.updateChild (package:flutter/src/widgets/framework.dart:3941:15)
#13     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5642:16)
#14     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5780:11)
#15     Element.rebuild (package:flutter/src/widgets/framework.dart:5333:7)
#16     StatefulElement.update (package:flutter/src/widgets/framework.dart:5803:5)
#17     Element.updateChild (package:flutter/src/widgets/framework.dart:3941:15)
#18     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5642:16)
#19     Element.rebuild (package:flutter/src/widgets/framework.dart:5333:7)
#20     ProxyElement.update (package:flutter/src/widgets/framework.dart:5946:5)
#21     Element.updateChild (package:flutter/src/widgets/framework.dart:3941:15)
#22     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5642:16)
#23     Element.rebuild (package:flutter/src/widgets/framework.dart:5333:7)
#24     StatelessElement.update (package:flutter/src/widgets/framework.dart:5693:5)
#25     Element.updateChild (package:flutter/src/widgets/framework.dart:3941:15)
#26     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5642:16)
#27     Element.rebuild (package:flutter/src/widgets/framework.dart:5333:7)
#28     ProxyElement.update (package:flutter/src/widgets/framework.dart:5946:5)
#29     Element.updateChild (package:flutter/src/widgets/framework.dart:3941:15)
#30     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5642:16)
#31     Element.rebuild (package:flutter/src/widgets/framework.dart:5333:7)
#32     ProxyElement.update (package:flutter/src/widgets/framework.dart:5946:5)
#33     Element.updateChild (package:flutter/src/widgets/framework.dart:3941:15)
#34     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5642:16)
#35     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5780:11)
#36     Element.rebuild (package:flutter/src/widgets/framework.dart:5333:7)
#37     BuildScope._tryRebuild (package:flutter/src/widgets/framework.dart:2693:15)
#38     BuildScope._flushDirtyElements (package:flutter/src/widgets/framework.dart:2752:11)
#39     BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:3048:18)
#40     WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:1162:21)
#41     RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:468:5)
#42     SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1397:15)
#43     SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1318:9)
#44     SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:1176:5)
#45     _invoke (dart:ui/hooks.dart:312:13)
#46     PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:419:5)
#47     _drawFrame (dart:ui/hooks.dart:283:31)
(elided 2 frames from class _AssertionError)
====================================================================================================

Further debugging shows that the key [<'home'>] is duplicated in the Navigator.pages list.

What's happening is that the MaterialApp hosting the Navigator is wrapped with another widget. To keep the state, is uses a GlobalKey. This is the same mechanism Wiredash uses to wrap the users app.

JaspervanRiet commented 3 days ago

Thank you for the clear bug report @passsy, made my debugging a lot easier. This has now been fixed.