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

View file

@ -86,6 +86,14 @@ class Document {
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].
Node? nodeAtPath(Path path) {
return root.childAtPath(path);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -49,12 +49,15 @@ dependencies:
keyboard_height_plugin: ^0.0.4
numerus: ^2.1.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.1
network_image_mock: ^2.1.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
# 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,
),
),
),
),
);
}
}