fix: memory leaks (#615)

This commit is contained in:
Lucas.Xu 2023-12-08 14:39:18 +07:00 • committed by GitHub
commit ffab5a1ca2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 137 additions and 16 deletions

View file

@ -5,7 +5,7 @@ import 'package:example/pages/desktop_editor.dart';
import 'package:example/pages/mobile_editor.dart'; import 'package:example/pages/mobile_editor.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class Editor extends StatelessWidget { class Editor extends StatefulWidget {
const Editor({ const Editor({
super.key, super.key,
required this.jsonString, required this.jsonString,
@ -20,16 +20,30 @@ class Editor extends StatelessWidget {
final TextDirection textDirection; final TextDirection textDirection;
@override
State<Editor> createState() => _EditorState();
}
class _EditorState extends State<Editor> {
EditorState? editorState;
@override
void dispose() {
editorState?.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
color: Colors.white, color: Colors.white,
child: FutureBuilder<String>( child: FutureBuilder<String>(
future: jsonString, future: widget.jsonString,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData && if (snapshot.hasData &&
snapshot.connectionState == ConnectionState.done) { snapshot.connectionState == ConnectionState.done) {
final editorState = EditorState( EditorState editorState = EditorState(
document: Document.fromJson( document: Document.fromJson(
Map<String, Object>.from( Map<String, Object>.from(
json.decode(snapshot.data!), json.decode(snapshot.data!),
@ -42,14 +56,16 @@ class Editor extends StatelessWidget {
editorState.transactionStream.listen((event) { editorState.transactionStream.listen((event) {
if (event.$1 == TransactionTime.after) { if (event.$1 == TransactionTime.after) {
onEditorStateChange(editorState); widget.onEditorStateChange(editorState);
} }
}); });
this.editorState = editorState;
if (PlatformExtension.isDesktopOrWeb) { if (PlatformExtension.isDesktopOrWeb) {
return DesktopEditor( return DesktopEditor(
editorState: editorState, editorState: editorState,
textDirection: textDirection, textDirection: widget.textDirection,
); );
} else if (PlatformExtension.isMobile) { } else if (PlatformExtension.isMobile) {
return MobileEditor( return MobileEditor(

View file

@ -86,6 +86,14 @@ class Document {
return last; return last;
} }
/// Must call this method when the [Document] is no longer needed.
void dispose() {
final nodes = NodeIterator(document: this, startNode: root).toList();
for (final node in nodes) {
node.dispose();
}
}
/// Returns the node at the given [path]. /// Returns the node at the given [path].
Node? nodeAtPath(Path path) { Node? nodeAtPath(Path path) {
return root.childAtPath(path); return root.childAtPath(path);

View file

@ -83,7 +83,6 @@ class PageBlockComponent extends BlockComponentStatelessWidget {
return ScrollablePositionedList.builder( return ScrollablePositionedList.builder(
shrinkWrap: scrollController.shrinkWrap, shrinkWrap: scrollController.shrinkWrap,
// padding: editorState.editorStyle.padding,
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
itemCount: items.length + extentCount, itemCount: items.length + extentCount,
itemBuilder: (context, index) { itemBuilder: (context, index) {

View file

@ -25,6 +25,12 @@ class _AutoScrollableWidgetState extends State<AutoScrollableWidget> {
late AutoScroller _autoScroller; late AutoScroller _autoScroller;
late ScrollableState _scrollableState; late ScrollableState _scrollableState;
@override
void dispose() {
_scrollableState.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget builder(context) { Widget builder(context) {

View file

@ -3,6 +3,7 @@ import 'dart:math';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:appflowy_editor/src/flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:appflowy_editor/src/flutter/scrollable_positioned_list/src/item_positions_notifier.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// This class controls the scroll behavior of the editor. /// This class controls the scroll behavior of the editor.
@ -145,7 +146,13 @@ class EditorScrollController {
if (!shrinkWrap) { if (!shrinkWrap) {
_scrollOffsetSubscription.cancel(); _scrollOffsetSubscription.cancel();
_itemPositionsListener.itemPositions.removeListener(_listenItemPositions); _itemPositionsListener.itemPositions.removeListener(_listenItemPositions);
(_itemPositionsListener as ItemPositionsNotifier?)
?.itemPositions
.dispose();
} }
offsetNotifier.dispose();
visibleRangeNotifier.dispose();
} }
Future<void> animateTo({ Future<void> animateTo({

View file

@ -1,6 +1,4 @@
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/editor/editor_component/service/scroll/auto_scrollable_widget.dart';
import 'package:appflowy_editor/src/editor/editor_component/service/scroll/auto_scroller.dart';
import 'package:appflowy_editor/src/editor/editor_component/service/scroll/desktop_scroll_service.dart'; import 'package:appflowy_editor/src/editor/editor_component/service/scroll/desktop_scroll_service.dart';
import 'package:appflowy_editor/src/editor/editor_component/service/scroll/mobile_scroll_service.dart'; import 'package:appflowy_editor/src/editor/editor_component/service/scroll/mobile_scroll_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -51,15 +49,13 @@ class _ScrollServiceWidgetState extends State<ScrollServiceWidget>
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Provider.value( return Provider.value(
value: widget.editorScrollController, value: widget.editorScrollController,
child: AutoScrollableWidget( child: Builder(
scrollController: scrollController, builder: (context) {
builder: (context, autoScroller) {
if (PlatformExtension.isDesktopOrWeb) { if (PlatformExtension.isDesktopOrWeb) {
return _buildDesktopScrollService(context, autoScroller); return _buildDesktopScrollService(context);
} else if (PlatformExtension.isMobile) { } else if (PlatformExtension.isMobile) {
return _buildMobileScrollService(context, autoScroller); return _buildMobileScrollService(context);
} }
throw UnimplementedError(); throw UnimplementedError();
}, },
), ),
@ -68,7 +64,6 @@ class _ScrollServiceWidgetState extends State<ScrollServiceWidget>
Widget _buildDesktopScrollService( Widget _buildDesktopScrollService(
BuildContext context, BuildContext context,
AutoScroller autoScroller,
) { ) {
return DesktopScrollService( return DesktopScrollService(
key: _forwardKey, key: _forwardKey,
@ -78,7 +73,6 @@ class _ScrollServiceWidgetState extends State<ScrollServiceWidget>
Widget _buildMobileScrollService( Widget _buildMobileScrollService(
BuildContext context, BuildContext context,
AutoScroller autoScroller,
) { ) {
return MobileScrollService( return MobileScrollService(
key: _forwardKey, key: _forwardKey,

View file

@ -82,6 +82,7 @@ class _DesktopSelectionServiceWidgetState
clearSelection(); clearSelection();
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
editorState.selectionNotifier.removeListener(_updateSelection); editorState.selectionNotifier.removeListener(_updateSelection);
currentSelection.dispose();
super.dispose(); super.dispose();
} }

View file

@ -245,6 +245,8 @@ class EditorState {
_debouncedSealHistoryItemTimer?.cancel(); _debouncedSealHistoryItemTimer?.cancel();
onDispose.value += 1; onDispose.value += 1;
onDispose.dispose(); onDispose.dispose();
document.dispose();
selectionNotifier.dispose();
} }
/// Apply the transaction to the state. /// Apply the transaction to the state.

View file

@ -151,6 +151,8 @@ class _PositionedListState extends State<PositionedList> {
@override @override
void dispose() { void dispose() {
scrollController.removeListener(_schedulePositionNotificationUpdate); scrollController.removeListener(_schedulePositionNotificationUpdate);
scrollController.dispose();
registeredElements.dispose();
super.dispose(); super.dispose();
} }

View file

@ -380,6 +380,9 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
secondary.itemPositionsNotifier.itemPositions secondary.itemPositionsNotifier.itemPositions
.removeListener(_updatePositions); .removeListener(_updatePositions);
_animationController?.dispose(); _animationController?.dispose();
primary.itemPositionsNotifier.itemPositions.dispose();
secondary.itemPositionsNotifier.itemPositions.dispose();
secondary.scrollController.dispose();
super.dispose(); super.dispose();
} }
@ -695,4 +698,8 @@ class _ListDisplayDetails {
double alignment = 0; double alignment = 0;
final Key key; final Key key;
void dispose() {
itemPositionsNotifier.itemPositions.dispose();
}
} }

View file

@ -49,12 +49,15 @@ dependencies:
keyboard_height_plugin: ^0.0.4 keyboard_height_plugin: ^0.0.4
numerus: ^2.1.2 numerus: ^2.1.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^3.0.1 flutter_lints: ^3.0.1
network_image_mock: ^2.1.1 network_image_mock: ^2.1.1
mockito: ^5.4.1 mockito: ^5.4.1
leak_tracker_flutter_testing: ^1.0.10
leak_tracker: '>=9.0.0 <11.0.0'
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec

View file

@ -0,0 +1,76 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:leak_tracker/leak_tracker.dart';
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
void main() async {
testWidgetsWithLeakTracking(
'memory leak',
(tester) async {
await tester.pumpWidget(const _NoMemoryLeaks());
await tester.pumpAndSettle();
},
leakTesting: LeakTesting.settings.copyWith(
ignore: false,
leakDiagnosticConfig: const LeakDiagnosticConfig(
collectStackTraceOnStart: true,
collectRetainingPathForNotGCed: true,
),
onLeaks: (leaks) {
// dump the leaks
for (final leak in leaks.all) {
final stack =
leak.context![ContextKeys.startCallstack]! as StackTrace;
final stackInEditor = stack
.toString()
.split('\n')
.where((stack) => stack.contains('package:appflowy_editor'))
.join('\n');
debugPrint('''
${leak.type} => ${leak.trackedClass}
$stackInEditor
-------------------------------------------------------------------------------
''');
}
expect(leaks.all, isEmpty);
},
),
);
}
class _NoMemoryLeaks extends StatefulWidget {
const _NoMemoryLeaks();
@override
State<_NoMemoryLeaks> createState() => __NoMemoryLeaksState();
}
class __NoMemoryLeaksState extends State<_NoMemoryLeaks> {
final editorState = EditorState.blank();
@override
void dispose() {
editorState.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: SafeArea(
child: Container(
width: 500,
decoration: BoxDecoration(
border: Border.all(color: Colors.blue),
),
child: AppFlowyEditor(
editorState: editorState,
),
),
),
),
);
}
}