Merge pull request #1225 from AppFlowy-IO/fix/dispose-shrinkwrap-scroll-listeners

fix: detach shrink-wrapped editor listeners on dispose
This commit is contained in:
Nathan.fooo 2026-07-19 15:03:59 +08:00 • committed by GitHub
commit b43e53469e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 82 additions and 17 deletions

View file

@ -24,22 +24,13 @@ class EditorScrollController {
// if shrinkWrap is true, we will render the document with Column layout. // if shrinkWrap is true, we will render the document with Column layout.
// otherwise, we will render the document with ScrollablePositionedList. // otherwise, we will render the document with ScrollablePositionedList.
if (shrinkWrap) { if (shrinkWrap) {
void updateVisibleRange() { _updateVisibleRange();
visibleRangeNotifier.value = ( editorState.document.root.addListener(_updateVisibleRange);
0,
editorState.document.root.children.length - 1,
);
}
updateVisibleRange();
editorState.document.root.addListener(updateVisibleRange);
shouldDisposeScrollController = scrollController == null; shouldDisposeScrollController = scrollController == null;
this.scrollController = scrollController ?? ScrollController(); this.scrollController = scrollController ?? ScrollController();
// listen to the scroll offset // listen to the scroll offset
this.scrollController.addListener( this.scrollController.addListener(_updateScrollOffset);
() => offsetNotifier.value = this.scrollController.offset,
);
} else { } else {
// listen to the scroll offset // listen to the scroll offset
_scrollOffsetSubscription = _scrollOffsetListener.changes.listen((value) { _scrollOffsetSubscription = _scrollOffsetListener.changes.listen((value) {
@ -141,13 +132,23 @@ class EditorScrollController {
late final StreamSubscription<double> _scrollOffsetSubscription; late final StreamSubscription<double> _scrollOffsetSubscription;
void _updateVisibleRange() {
visibleRangeNotifier.value = (
0,
editorState.document.root.children.length - 1,
);
}
void _updateScrollOffset() {
offsetNotifier.value = scrollController.offset;
}
// dispose the subscription // dispose the subscription
void dispose() { void dispose() {
if (shouldDisposeScrollController) { if (shrinkWrap) {
scrollController.dispose(); editorState.document.root.removeListener(_updateVisibleRange);
} scrollController.removeListener(_updateScrollOffset);
} else {
if (!shrinkWrap) {
_scrollOffsetSubscription.cancel(); _scrollOffsetSubscription.cancel();
_itemPositionsListener.itemPositions.removeListener(_listenItemPositions); _itemPositionsListener.itemPositions.removeListener(_listenItemPositions);
(_itemPositionsListener as ItemPositionsNotifier?) (_itemPositionsListener as ItemPositionsNotifier?)
@ -155,6 +156,10 @@ class EditorScrollController {
.dispose(); .dispose();
} }
if (shouldDisposeScrollController) {
scrollController.dispose();
}
offsetNotifier.dispose(); offsetNotifier.dispose();
visibleRangeNotifier.dispose(); visibleRangeNotifier.dispose();
} }

View file

@ -0,0 +1,60 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets(
'disposing a shrink-wrapped controller detaches document listeners',
(tester) async {
final editorState = EditorState.blank(withInitialText: false);
addTearDown(editorState.dispose);
final scrollController = EditorScrollController(
editorState: editorState,
shrinkWrap: true,
);
scrollController.dispose();
editorState.document.insert([0], [paragraphNode()]);
expect(tester.takeException(), isNull);
},
);
testWidgets(
'disposing a shrink-wrapped controller detaches external scroll listeners',
(tester) async {
final editorState = EditorState.blank(withInitialText: false);
final externalScrollController = _TestScrollController();
addTearDown(editorState.dispose);
addTearDown(externalScrollController.dispose);
final scrollController = EditorScrollController(
editorState: editorState,
shrinkWrap: true,
scrollController: externalScrollController,
);
expect(externalScrollController.listenerCount, 1);
scrollController.dispose();
expect(externalScrollController.listenerCount, 0);
},
);
}
class _TestScrollController extends ScrollController {
final Set<VoidCallback> _listeners = {};
int get listenerCount => _listeners.length;
@override
void addListener(VoidCallback listener) {
_listeners.add(listener);
super.addListener(listener);
}
@override
void removeListener(VoidCallback listener) {
_listeners.remove(listener);
super.removeListener(listener);
}
}