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: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(
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
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