scribble
scribble copied to clipboard
fix: guard repaint boundary keys via per-instance attach/detach
Description
problem
- Share of a single
repaintBoundaryKeyacross multipleScribblewidgets causedMultiple widgets used the same GlobalKeywhen go_router kept two/notes/:noteId/editpages alive viaMaterialPage(maintainState: true, key: state.pageKey)(see it-contest repo, branch design/clean-dev-xodnd @f13a67f / dev @38f056b).
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY
╞═══════════════════════════════════════════════════════════
The following assertion was thrown while finalizing the widget tree:
Multiple widgets used the same GlobalKey.
The key [GlobalKey#d6fd3] was used by multiple widgets. The parents of those widgets
were:
- CustomPaint(renderObject: RenderCustomPaint#f1798 NEEDS-PAINT)
- CustomPaint(renderObject: RenderCustomPaint#b3622)
A GlobalKey can only be specified on one widget at a time in the widget tree.
When the exception was thrown, this was the stack:
#0 BuildOwner._debugVerifyGlobalKeyReservation.<anonymous closure>.<anonymous
closure>.<anonymous closure> (package:flutter/src/widgets/framework.dart:3220:13)
#1 _LinkedHashMapMixin.forEach (dart:_compact_hash:764:13)
#2 BuildOwner._debugVerifyGlobalKeyReservation.<anonymous closure>.<anonymous
closure> (package:flutter/src/widgets/framework.dart:3164:20)
#3 _LinkedHashMapMixin.forEach (dart:_compact_hash:764:13)
#4 BuildOwner._debugVerifyGlobalKeyReservation.<anonymous closure>
(package:flutter/src/widgets/framework.dart:3158:36)
#5 BuildOwner._debugVerifyGlobalKeyReservation
(package:flutter/src/widgets/framework.dart:3228:6)
#6 BuildOwner.finalizeTree.<anonymous closure>
(package:flutter/src/widgets/framework.dart:3291:11)
#7 BuildOwner.finalizeTree (package:flutter/src/widgets/framework.dart:3378:8)
#8 WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:1247:19)
#9 RendererBinding._handlePersistentFrameCallback
(package:flutter/src/rendering/binding.dart:495:5)
#10 SchedulerBinding._invokeFrameCallback
(package:flutter/src/scheduler/binding.dart:1438:15)
#11 SchedulerBinding.handleDrawFrame
(package:flutter/src/scheduler/binding.dart:1351:9)
#12 SchedulerBinding._handleDrawFrame
(package:flutter/src/scheduler/binding.dart:1204:5)
#13 _invoke (dart:ui/hooks.dart:331:13)
#14 PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:444:5)
#15 _drawFrame (dart:ui/hooks.dart:303:31)
═══════════════════════════════════════════════════════════════════════════════════════
═════════════
Another exception was thrown: Multiple widgets used the same GlobalKey.
Another exception was thrown: Multiple widgets used the same GlobalKey.
Another exception was thrown: Multiple widgets used the same GlobalKey.
how i solved
- Converted
Scribbleto aStatefulWidget, giving each instance its own_localRepaintBoundaryKeyand hooking the notifier through newattachRepaintBoundaryKey/detachRepaintBoundaryKeylifecycle methods. - Added a debug-only warning in
ScribbleNotifier.renderImagewhenever the legacy fallback key is used so integrators are nudged to adopt the new lifecycle before the fallback disappears.
how it works
-
ScribbleNotifiertracks the attached keys and falls back to the legacy key so existing code keeps working;renderImagenow resolves to the most recently attached surface, which matches the single-canvas-per-notifier contract.
verification
- Manual verification: dependency override from
https://github.com/timcreatedit/scribbleref: maintohttps://github.com/ehdnd/scribbleref: fix/globalkey-collisioninsidehttps://github.com/tryCatchPing/it-contest(branches design/clean-dev-xodnd @f13a67f / dev @38f056b). RestoredMaterialPage(maintainState: true)—which previously forced us to fallback tofalse—and confirmed linked-note navigation (link tap, backlinks panel, note list) no longer emits the GlobalKey assertion. - Automated verification:
flutter testpasses on Flutter 3.32.5 (FVM). The new widget test (Scribble does not throw when sharing a notifier across multiple instances) fails against pre-fix sources, confirming regression coverage. - ,, Attempts to build a minimal reproduction outside of the app were unsuccessful; the failure only appeared once the go_router + Riverpod keepAlive + maintainState stack was in place. If anyone manages to isolate a smaller example, happy to add it.
note for integrators
- Custom
ScribbleNotifierBaseimplementations must overrideattachRepaintBoundaryKey/detachRepaintBoundaryKeyto keeprenderImageworking; consider this a behavior change for external notifiers.- In debug builds, continuing to rely on the legacy fallback GlobalKey now prints a warning so integrators notice the upcoming change.
Checklist
- [x] My PR title is in the style of conventional commits
- [x] All public facing APIs are documented with dartdoc
- [x] I have added tests to cover my changes