From d85f5af05618146df1dd08185c9a6af9ff03e7c6 Mon Sep 17 00:00:00 2001 From: Pedro Paulo de Amorim Date: Wed, 7 Oct 2026 01:57:17 +0100 Subject: [PATCH 1/3] Fix memory leaks in the editor, selection overlay, and toolbar. - Dispose notifiers, controllers and overlay entries the editor creates. - Remove listeners/observers registered on external objects on dispose. - Fix controller swap removing a different listener than was added. - QuillEditor focusNode/scrollController are optional and owned by the state when omitted. - Add leak-tracking widget tests. --- .../lib/src/test/widget_tester_extension.dart | 9 +- lib/src/common/utils/quill_node_focus.dart | 4 +- lib/src/editor/editor.dart | 96 ++++++++++++++----- .../editor/raw_editor/raw_editor_state.dart | 16 ++-- lib/src/editor/widgets/cursor.dart | 1 + .../editor/widgets/text/text_selection.dart | 17 ++-- .../toolbar/buttons/color/color_dialog.dart | 6 ++ lib/src/toolbar/buttons/history_button.dart | 21 +++- pubspec.yaml | 3 + test/bug_fix_test.dart | 14 ++- test/editor/editor_leak_test.dart | 64 +++++++++++++ 11 files changed, 197 insertions(+), 54 deletions(-) create mode 100644 test/editor/editor_leak_test.dart diff --git a/flutter_quill_test/lib/src/test/widget_tester_extension.dart b/flutter_quill_test/lib/src/test/widget_tester_extension.dart index 46f8f0684..d2826294f 100644 --- a/flutter_quill_test/lib/src/test/widget_tester_extension.dart +++ b/flutter_quill_test/lib/src/test/widget_tester_extension.dart @@ -13,11 +13,11 @@ extension QuillWidgetTesterExt on WidgetTester { /// ``` Future quillGiveFocus(Finder finder) { return TestAsyncUtils.guard(() async { - final editor = findEditor(finder); - editor.widget.focusNode.requestFocus(); + final focusNode = findRawEditor(finder).widget.config.focusNode + ..requestFocus(); await pump(); expect( - editor.widget.focusNode.hasFocus, + focusNode.hasFocus, isTrue, ); }); @@ -31,8 +31,7 @@ extension QuillWidgetTesterExt on WidgetTester { /// ``` Future quillHasFocusEditor(Finder finder) { return TestAsyncUtils.guard(() async { - final editor = findEditor(finder); - return editor.widget.focusNode.hasFocus; + return findRawEditor(finder).widget.config.focusNode.hasFocus; }); } diff --git a/lib/src/common/utils/quill_node_focus.dart b/lib/src/common/utils/quill_node_focus.dart index 6d4c48c38..752ca846d 100644 --- a/lib/src/common/utils/quill_node_focus.dart +++ b/lib/src/common/utils/quill_node_focus.dart @@ -15,9 +15,9 @@ class QuillFocusNode extends FocusNode { super.descendantsAreFocusable, super.descendantsAreTraversable, bool keyboardEnabled = true, - }) : _keyboardEnabled = keyboardEnabled; + }) : _keyboardEnabled = keyboardEnabled; // ignore: prefer_initializing_formals - bool _keyboardEnabled = true; + bool _keyboardEnabled; bool get keyboardEnabled => _keyboardEnabled; diff --git a/lib/src/editor/editor.dart b/lib/src/editor/editor.dart index f1b23db34..e53d37e89 100644 --- a/lib/src/editor/editor.dart +++ b/lib/src/editor/editor.dart @@ -139,9 +139,9 @@ class QuillEditor extends StatefulWidget { /// ``` /// QuillEditor({ - required this.focusNode, - required this.scrollController, required this.controller, + this.focusNode, + this.scrollController, this.config = const QuillEditorConfig(), super.key, }) { @@ -157,15 +157,14 @@ class QuillEditor extends StatefulWidget { QuillEditorConfig config = const QuillEditorConfig(), FocusNode? focusNode, ScrollController? scrollController, - }) { - return QuillEditor( - key: key, - scrollController: scrollController ?? ScrollController(), - focusNode: focusNode ?? FocusNode(), - controller: controller, - config: config, - ); - } + }) => + QuillEditor( + key: key, + controller: controller, + focusNode: focusNode, + scrollController: scrollController, + config: config, + ); /// Controller object which establishes a link between a rich text document /// and this editor. @@ -175,10 +174,15 @@ class QuillEditor extends StatefulWidget { final QuillEditorConfig config; /// Controls whether this editor has keyboard focus. - final FocusNode focusNode; + /// + /// When null, the editor creates and disposes its own. Use + /// [QuillEditorState.focusNode] to access the effective node. + final FocusNode? focusNode; /// The [ScrollController] to use when vertically scrolling the contents. - final ScrollController scrollController; + /// + /// When null, the editor creates and disposes its own. + final ScrollController? scrollController; @override QuillEditorState createState() => QuillEditorState(); @@ -199,6 +203,48 @@ class QuillEditorState extends State /// {@macro drag_offset_notifier} final dragOffsetNotifier = isMobileApp ? ValueNotifier(null) : null; + final _webKeyboardFocusNode = kIsWeb + ? FocusNode( + onKeyEvent: (node, event) => KeyEventResult.skipRemainingHandlers, + ) + : null; + + FocusNode? _ownFocusNode; + ScrollController? _ownScrollController; + + FocusNode get focusNode => widget.focusNode ?? (_ownFocusNode ??= FocusNode()); + + ScrollController get scrollController => + widget.scrollController ?? + (_ownScrollController ??= ScrollController()); + + @override + void dispose() { + focusNode.removeListener(_onFocusChanged); + dragOffsetNotifier?.dispose(); + _webKeyboardFocusNode?.dispose(); + _ownFocusNode?.dispose(); + _ownScrollController?.dispose(); + super.dispose(); + } + + @override + void didUpdateWidget(QuillEditor oldWidget) { + super.didUpdateWidget(oldWidget); + final oldFocusNode = oldWidget.focusNode ?? _ownFocusNode; + if (oldFocusNode != focusNode) { + oldFocusNode?.removeListener(_onFocusChanged); + focusNode.addListener(_onFocusChanged); + } + } + + // Hide toolbar when the editor loses focus. + void _onFocusChanged() { + if (!focusNode.hasFocus) { + _editorKey.currentState?.hideToolbar(); + } + } + @override void initState() { super.initState(); @@ -209,18 +255,11 @@ class QuillEditorState extends State config.detectWordBoundary, ); - final focusNode = widget.focusNode; - if (config.autoFocus) { focusNode.requestFocus(); } - // Hide toolbar when the editor loses focus. - focusNode.addListener(() { - if (!focusNode.hasFocus) { - _editorKey.currentState?.hideToolbar(); - } - }); + focusNode.addListener(_onFocusChanged); } @override @@ -269,8 +308,8 @@ class QuillEditorState extends State spaceShortcutEvents: widget.config.spaceShortcutEvents, onKeyPressed: widget.config.onKeyPressed, customLeadingBuilder: widget.config.customLeadingBlockBuilder, - focusNode: widget.focusNode, - scrollController: widget.scrollController, + focusNode: focusNode, + scrollController: scrollController, scrollable: config.scrollable, enableAlwaysIndentOnTab: config.enableAlwaysIndentOnTab, scrollBottomInset: config.scrollBottomInset, @@ -347,9 +386,7 @@ class QuillEditorState extends State // See issue https://github.com/singerdmx/flutter-quill/issues/304 return KeyboardListener( onKeyEvent: (_) {}, - focusNode: FocusNode( - onKeyEvent: (node, event) => KeyEventResult.skipRemainingHandlers, - ), + focusNode: _webKeyboardFocusNode!, child: editor, ); } @@ -717,6 +754,13 @@ class RenderEditor extends RenderEditableContainerBox ValueListenable get selectionEndInViewport => _selectionEndInViewport; final ValueNotifier _selectionEndInViewport = ValueNotifier(true); + @override + void dispose() { + _selectionStartInViewport.dispose(); + _selectionEndInViewport.dispose(); + super.dispose(); + } + void _updateSelectionExtentsVisibility(Offset effectiveOffset) { final visibleRegion = Offset.zero & size; final startPosition = diff --git a/lib/src/editor/raw_editor/raw_editor_state.dart b/lib/src/editor/raw_editor/raw_editor_state.dart index cb87da241..b6a0b8480 100644 --- a/lib/src/editor/raw_editor/raw_editor_state.dart +++ b/lib/src/editor/raw_editor/raw_editor_state.dart @@ -377,7 +377,7 @@ class QuillRawEditorState extends EditorState // clipboard status to be checked w/o user interaction which fails. Default // to pasteable for web. if (kIsWeb) { - _clipboardStatus = ClipboardStatusNotifier( + _clipboardStatus ??= ClipboardStatusNotifier( value: ClipboardStatus.pasteable, ); } @@ -835,6 +835,7 @@ class QuillRawEditorState extends EditorState } else { // treat iOS Simulator like a keyboard OS isIOSSimulator().then((isIosSimulator) { + if (!mounted) return; if (isIosSimulator) { _keyboardVisible = true; } else { @@ -917,8 +918,8 @@ class QuillRawEditorState extends EditorState _cursorCont.style = widget.config.cursorStyle; if (controller != oldWidget.controller) { - oldWidget.controller.removeListener(_didChangeTextEditingValue); - controller.addListener(_didChangeTextEditingValue); + oldWidget.controller.removeListener(_didChangeTextEditingValueListener); + controller.addListener(_didChangeTextEditingValueListener); updateRemoteValueIfNeeded(); } @@ -967,10 +968,11 @@ class QuillRawEditorState extends EditorState _selectionOverlay?.dispose(); _selectionOverlay = null; controller.removeListener(_didChangeTextEditingValueListener); - if (!widget.config.readOnly) { - widget.config.focusNode.removeListener(_handleFocusChanged); - composingRange.removeListener(_onComposingRangeChanged); - } + _scrollController.removeListener(_updateSelectionOverlayForScroll); + widget.config.focusNode.removeListener(_handleFocusChanged); + WidgetsBinding.instance.removeObserver(this); + composingRange.dispose(); + _floatingCursorResetController.dispose(); _cursorCont.dispose(); if (_clipboardStatus != null) { _clipboardStatus! diff --git a/lib/src/editor/widgets/cursor.dart b/lib/src/editor/widgets/cursor.dart index de126e909..2e004dd7d 100644 --- a/lib/src/editor/widgets/cursor.dart +++ b/lib/src/editor/widgets/cursor.dart @@ -166,6 +166,7 @@ class CursorCont extends ChangeNotifier { show.dispose(); blink.dispose(); color.dispose(); + _floatingCursorTextPosition.dispose(); assert(_cursorTimer == null); super.dispose(); } diff --git a/lib/src/editor/widgets/text/text_selection.dart b/lib/src/editor/widgets/text/text_selection.dart index 75517e486..d209d66f0 100644 --- a/lib/src/editor/widgets/text/text_selection.dart +++ b/lib/src/editor/widgets/text/text_selection.dart @@ -209,8 +209,11 @@ class EditorTextSelectionOverlay { if (_handles == null) { return; } - _handles![0].remove(); - _handles![1].remove(); + for (final handle in _handles!) { + handle + ..remove() + ..dispose(); + } _handles = null; } @@ -220,7 +223,9 @@ class EditorTextSelectionOverlay { void hideToolbar() { assert(toolbar != null); dragOffsetNotifier?.removeListener(_dragOffsetListener); - toolbar!.remove(); + toolbar! + ..remove() + ..dispose(); toolbar = null; } @@ -347,11 +352,7 @@ class EditorTextSelectionOverlay { /// Hides the entire overlay including the toolbar and the handles. void hide() { - if (_handles != null) { - _handles![0].remove(); - _handles![1].remove(); - _handles = null; - } + hideHandles(); if (toolbar != null) { hideToolbar(); } diff --git a/lib/src/toolbar/buttons/color/color_dialog.dart b/lib/src/toolbar/buttons/color/color_dialog.dart index c67a0e9f0..161c57b40 100644 --- a/lib/src/toolbar/buttons/color/color_dialog.dart +++ b/lib/src/toolbar/buttons/color/color_dialog.dart @@ -48,6 +48,12 @@ class ColorPickerDialogState extends State { TextEditingController(text: color_picker.colorToHex(selectedColor)); } + @override + void dispose() { + hexController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return AlertDialog( diff --git a/lib/src/toolbar/buttons/history_button.dart b/lib/src/toolbar/buttons/history_button.dart index 32dd807e9..e2b97a11e 100644 --- a/lib/src/toolbar/buttons/history_button.dart +++ b/lib/src/toolbar/buttons/history_button.dart @@ -1,5 +1,8 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; +import '../../document/structs/doc_change.dart'; import '../../l10n/extensions/localizations_ext.dart'; import '../base_button/base_value_button.dart'; import '../config/buttons/history_options.dart'; @@ -36,6 +39,7 @@ class QuillToolbarHistoryButton extends QuillToolbarHistoryBaseButton { class QuillToolbarHistoryButtonState extends QuillToolbarHistoryBaseButtonState { var _canPressed = false; + StreamSubscription? _changesSubscription; @override String get defaultTooltip => @@ -55,11 +59,26 @@ class QuillToolbarHistoryButtonState _updateCanPressed(); // Set the init state // Listen for changes and change it - controller.changes.listen((event) async { + _changesSubscription?.cancel(); + _changesSubscription = controller.changes.listen((event) { _updateCanPressedWithSetState(); }); } + @override + void didUpdateWidget(QuillToolbarHistoryButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.controller != controller) { + _listenForChanges(); + } + } + + @override + void dispose() { + _changesSubscription?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { final childBuilder = this.childBuilder; diff --git a/pubspec.yaml b/pubspec.yaml index a32335375..d8b183a47 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,6 +64,7 @@ dev_dependencies: flutter_test: sdk: flutter flutter_quill_test: ^11.1.0 + leak_tracker_flutter_testing: any test: ^1.24.3 # For scripts only @@ -73,6 +74,8 @@ dev_dependencies: path: any dependency_overrides: + flutter_quill_test: + path: flutter_quill_test quill_native_bridge: git: url: https://github.com/T-Pro/quill-native-bridge.git diff --git a/test/bug_fix_test.dart b/test/bug_fix_test.dart index 0697d76da..1de5b4949 100644 --- a/test/bug_fix_test.dart +++ b/test/bug_fix_test.dart @@ -57,17 +57,21 @@ void main() { group('1189 - The provided text position is not in the current node', () { late QuillController controller; + late FocusNode focusNode; late QuillEditor editor; setUp(() { controller = QuillController.basic(); + focusNode = FocusNode(); editor = QuillEditor.basic( controller: controller, + focusNode: focusNode, ); }); tearDown(() { controller.dispose(); + focusNode.dispose(); }); testWidgets('Refocus editor after controller clears document', @@ -81,10 +85,10 @@ void main() { ); await tester.quillEnterText(find.byType(QuillEditor), 'test\n'); - editor.focusNode.unfocus(); + focusNode.unfocus(); await tester.pump(); controller.clear(); - editor.focusNode.requestFocus(); + focusNode.requestFocus(); await tester.pump(); expect(tester.takeException(), isNull); }); @@ -99,10 +103,10 @@ void main() { await tester.quillEnterText(find.byType(QuillEditor), 'test\n'); controller.formatSelection(Attribute.ul); - editor.focusNode.unfocus(); + focusNode.unfocus(); await tester.pump(); controller.formatSelection(const ListAttribute(null)); - editor.focusNode.requestFocus(); + focusNode.requestFocus(); await tester.pump(); expect(tester.takeException(), isNull); }); @@ -118,7 +122,7 @@ void main() { await tester.quillEnterText(find.byType(QuillEditor), 'test\n'); controller.formatSelection(Attribute.unchecked); - editor.focusNode.unfocus(); + focusNode.unfocus(); await tester.pump(); await tester.tap(find.byType(QuillCheckboxPoint)); expect(tester.takeException(), isNull); diff --git a/test/editor/editor_leak_test.dart b/test/editor/editor_leak_test.dart new file mode 100644 index 000000000..f6f0ac107 --- /dev/null +++ b/test/editor/editor_leak_test.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; + +void main() { + LeakTesting.enable(); + + testWidgets( + 'QuillEditor does not leak after unmount while focused with a selection', + experimentalLeakTesting: LeakTesting.settings.withTrackedAll(), + (tester) async { + final controller = QuillController.basic() + ..document.insert(0, 'hello world'); + final focusNode = FocusNode(); + final scrollController = ScrollController(); + addTearDown(controller.dispose); + addTearDown(focusNode.dispose); + addTearDown(scrollController.dispose); + + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: FlutterQuillLocalizations.localizationsDelegates, + home: Scaffold( + body: QuillEditor( + controller: controller, + focusNode: focusNode, + scrollController: scrollController, + ), + ), + ), + ); + + focusNode.requestFocus(); + await tester.pump(); + controller.updateSelection( + const TextSelection(baseOffset: 0, extentOffset: 5), + ChangeSource.local, + ); + await tester.pumpAndSettle(); + + await tester.pumpWidget(const SizedBox()); + }, + ); + + testWidgets( + 'QuillEditor.basic disposes its own focus node and scroll controller', + experimentalLeakTesting: LeakTesting.settings.withTrackedAll(), + (tester) async { + final controller = QuillController.basic(); + addTearDown(controller.dispose); + + Widget app() => MaterialApp( + localizationsDelegates: + FlutterQuillLocalizations.localizationsDelegates, + home: QuillEditor.basic(controller: controller), + ); + + await tester.pumpWidget(app()); + await tester.pumpWidget(app()); + await tester.pumpWidget(const SizedBox()); + }, + ); +} From 201f5b95bb941b1abe1cf4b115c6cd030a9e54b6 Mon Sep 17 00:00:00 2001 From: Pedro Paulo de Amorim Date: Wed, 7 Oct 2026 02:04:47 +0100 Subject: [PATCH 2/3] Silence experimental and unused-import analyzer warnings. Record the 11.5.0 changelog for the focus node, leak fixes, and native bridge fork. --- CHANGELOG.md | 20 +++++++++++++++- example/analysis_options.yaml | 5 ++++ example/pubspec.lock | 24 +++++++++---------- .../analysis_options.yaml | 6 +++++ .../src/editor/image/image_save_utils.dart | 1 - lib/flutter_quill.dart | 2 +- .../quill_single_child_scroll_view.dart | 2 ++ ..._editor_state_text_input_client_mixin.dart | 4 ---- lib/src/editor/widgets/link.dart | 1 - 9 files changed, 45 insertions(+), 20 deletions(-) create mode 100644 flutter_quill_extensions/analysis_options.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index c9ff048a3..028f986b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [11.5.0] - 2026-10-07 + +### Added + +- `QuillFocusNode`, which can disable the keyboard while the editor keeps focus. + +### Fixed + +- Memory leaks in the editor, selection overlay, and toolbar. Notifiers, listeners, and overlay entries the editor creates are now disposed. +- Crash when a focus retry runs after the editor is disposed. +- Implement `onFocusReceived()` so the editor builds on Flutter 3.44.0. +- Stop exporting the deprecated internal `linkPrefixes` constant. + +### Changed + +- Depend on the T-Pro `quill_native_bridge` fork with `xclip` removed. + ## [11.4.2] - 2025-07-22 ### Fixed @@ -173,7 +190,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Apple-specific font dependency for subscript and superscript functionality from the example. - **BREAKING**: The [`super_clipboard`](https://pub.dev/packages/super_clipboard) plugin, To restore legacy behavior for `super_clipboard`, use [`flutter_quill_extensions`](https://pub.dev/packages/flutter_quill_extensions) package and `FlutterQuillExtensions.useSuperClipboardPlugin()`. -[unreleased]: https://github.com/singerdmx/flutter-quill/compare/v11.4.2...HEAD +[unreleased]: https://github.com/singerdmx/flutter-quill/compare/v11.5.0...HEAD +[11.5.0]: https://github.com/singerdmx/flutter-quill/compare/v11.4.2...v11.5.0 [11.4.2]: https://github.com/singerdmx/flutter-quill/compare/v10.0.0...v11.4.2 [11.4.1]: https://github.com/singerdmx/flutter-quill/compare/v10.0.0...v11.4.1 [11.4.0]: https://github.com/singerdmx/flutter-quill/compare/v10.0.0...v11.4.0 diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index f9b303465..1d6837433 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -1 +1,6 @@ include: package:flutter_lints/flutter.yaml + +analyzer: + errors: + # Example intentionally demos experimental clipboard APIs. + experimental_member_use: ignore diff --git a/example/pubspec.lock b/example/pubspec.lock index 49f2574f0..b8f5b9e17 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -373,26 +373,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -429,10 +429,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -618,10 +618,10 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.7" typed_data: dependency: transitive description: @@ -698,10 +698,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" video_player: dependency: transitive description: diff --git a/flutter_quill_extensions/analysis_options.yaml b/flutter_quill_extensions/analysis_options.yaml new file mode 100644 index 000000000..9bd4a407a --- /dev/null +++ b/flutter_quill_extensions/analysis_options.yaml @@ -0,0 +1,6 @@ +include: ../analysis_options.yaml + +analyzer: + errors: + # First-party package. Uses flutter_quill experimental and internal APIs on purpose. + experimental_member_use: ignore diff --git a/flutter_quill_extensions/lib/src/editor/image/image_save_utils.dart b/flutter_quill_extensions/lib/src/editor/image/image_save_utils.dart index 5203c999c..7024d93d9 100644 --- a/flutter_quill_extensions/lib/src/editor/image/image_save_utils.dart +++ b/flutter_quill_extensions/lib/src/editor/image/image_save_utils.dart @@ -5,7 +5,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_quill/internal.dart'; -import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'image_load_utils.dart'; diff --git a/lib/flutter_quill.dart b/lib/flutter_quill.dart index a7c8f5ae2..1fecac2d7 100644 --- a/lib/flutter_quill.dart +++ b/lib/flutter_quill.dart @@ -27,7 +27,7 @@ export 'src/editor/raw_editor/raw_editor_state.dart'; export 'src/editor/style_widgets/style_widgets.dart'; export 'src/editor/widgets/cursor.dart'; export 'src/editor/widgets/default_styles.dart'; -export 'src/editor/widgets/link.dart'; +export 'src/editor/widgets/link.dart' hide linkPrefixes; export 'src/editor/widgets/text/magnifier.dart'; export 'src/editor/widgets/text/utils/text_block_utils.dart'; export 'src/editor_toolbar_controller_shared/copy_cut_service/copy_cut_service.dart'; diff --git a/lib/src/editor/raw_editor/quill_single_child_scroll_view.dart b/lib/src/editor/raw_editor/quill_single_child_scroll_view.dart index 69877c392..387970699 100644 --- a/lib/src/editor/raw_editor/quill_single_child_scroll_view.dart +++ b/lib/src/editor/raw_editor/quill_single_child_scroll_view.dart @@ -263,6 +263,8 @@ class _RenderSingleChildViewport extends RenderBox @override void applyPaintTransform(RenderBox child, Matrix4 transform) { final paintOffset = _paintOffset; + // ponytail: translateByDouble needs Flutter 3.32; SDK floor is 3.0 + // ignore: deprecated_member_use transform.translate(paintOffset.dx, paintOffset.dy); } diff --git a/lib/src/editor/raw_editor/raw_editor_state_text_input_client_mixin.dart b/lib/src/editor/raw_editor/raw_editor_state_text_input_client_mixin.dart index a3382aa66..84886e37a 100644 --- a/lib/src/editor/raw_editor/raw_editor_state_text_input_client_mixin.dart +++ b/lib/src/editor/raw_editor/raw_editor_state_text_input_client_mixin.dart @@ -5,13 +5,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; -import 'package:meta/meta.dart'; import '../../../flutter_quill.dart'; import '../../delta/delta_diff.dart'; -import '../../document/document.dart'; -import '../editor.dart'; -import 'raw_editor.dart'; mixin RawEditorStateTextInputClientMixin on EditorState implements TextInputClient { diff --git a/lib/src/editor/widgets/link.dart b/lib/src/editor/widgets/link.dart index 6338f4a8d..c15ee0a91 100644 --- a/lib/src/editor/widgets/link.dart +++ b/lib/src/editor/widgets/link.dart @@ -1,7 +1,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:meta/meta.dart'; import '../../controller/quill_controller.dart'; import '../../document/attribute.dart'; From b36728851c64642a9c0ebd76298079af0c576392 Mon Sep 17 00:00:00 2001 From: Pedro Paulo de Amorim Date: Wed, 7 Oct 2026 02:15:46 +0100 Subject: [PATCH 3/3] Raise the example Android build to Gradle 8.14. Flutter stable rejects Gradle 8.7, so the wrapper, Android Gradle plugin, Kotlin, and Java target move to the current template versions. --- example/android/app/build.gradle | 6 +++--- example/android/gradle/wrapper/gradle-wrapper.properties | 2 +- example/android/settings.gradle | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 9caa6dd24..1e62647a1 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -11,12 +11,12 @@ android { ndkVersion = flutter.ndkVersion compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = JavaVersion.VERSION_11 + jvmTarget = JavaVersion.VERSION_17.toString() } defaultConfig { diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index b82aa23a4..ca025c83a 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/example/android/settings.gradle b/example/android/settings.gradle index a00f9ea70..ff03f22be 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -18,8 +18,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.3.1" apply false - id "org.jetbrains.kotlin.android" version "1.8.22" apply false + id "com.android.application" version "8.11.1" apply false + id "org.jetbrains.kotlin.android" version "2.2.20" apply false } include ":app"