fix: memory leaks (#615)
This commit is contained in:
parent
be92cfa209
commit
ffab5a1ca2
12 changed files with 137 additions and 16 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ class _DesktopSelectionServiceWidgetState
|
|||
clearSelection();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
editorState.selectionNotifier.removeListener(_updateSelection);
|
||||
currentSelection.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -245,6 +245,8 @@ class EditorState {
|
|||
_debouncedSealHistoryItemTimer?.cancel();
|
||||
onDispose.value += 1;
|
||||
onDispose.dispose();
|
||||
document.dispose();
|
||||
selectionNotifier.dispose();
|
||||
}
|
||||
|
||||
/// Apply the transaction to the state.
|
||||
|
|
|
|||
|
|
@ -151,6 +151,8 @@ class _PositionedListState extends State<PositionedList> {
|
|||
@override
|
||||
void dispose() {
|
||||
scrollController.removeListener(_schedulePositionNotificationUpdate);
|
||||
scrollController.dispose();
|
||||
registeredElements.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
76
test/memory_leak/memory_leak_test.dart
Normal file
76
test/memory_leak/memory_leak_test.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue