From bf802cd7869a514bb16805a08ed0c0815feebb43 Mon Sep 17 00:00:00 2001 From: Luke Hutchison Date: Wed, 13 Dec 2023 00:58:06 -0700 Subject: [PATCH] Fix memory leak (#5) --- CHANGELOG.md | 4 +++ README.md | 14 ++++----- example/lib/main.dart | 4 +-- lib/src/reactive_value_notifier.dart | 43 ++++++++++++++++++++++------ pubspec.yaml | 2 +- 5 files changed, 47 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea548b8..e29d424 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### 2.0.0 + +Fix memory leak (listeners were being added with every build). This required converting `ReactiveValueNotifier` from an extension to a subclass, because it needed an extra private field. (#5) + ### 1.0.4 Specify correct minimum Dart and Flutter versions (fixes #2). diff --git a/README.md b/README.md index 9fecec5..aa12eeb 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This library provides a mechanism for causing a UI to reactively update in respo This is the simplest possible state management / reactive UI update solution for Flutter, by far, reducing boilerplate compared to all the other insanely complex [state management approaches](https://docs.flutter.dev/development/data-and-backend/state-mgmt/options) currently available. -The closest thing to `flutter_reactive_value` is [`ValueListenableBuilder`](https://api.flutter.dev/flutter/widgets/ValueListenableBuilder-class.html). `flutter_reactive_value` provides the same basic capabilities as `ValueListenableBuilder`, but with much less syntactic overhead (i.e. you could think of `flutter_reactive_value` as syntactic sugar). `ValueListenableBuilder` may work better if your reactive widget has a child element that is complex and non-reactive. +The closest thing to `flutter_reactive_value` is [`ValueListenableBuilder`](https://api.flutter.dev/flutter/widgets/ValueListenableBuilder-class.html). `flutter_reactive_value` provides the same basic capabilities as `ValueListenableBuilder`, but with much less syntactic overhead (i.e. you could think of `flutter_reactive_value` as syntactic sugar). `ValueListenableBuilder` may work better if your reactive widget has a child element that is complex and non-reactive, because it takes a `child` parameter for any child widget that is not affected by changes to the `ValueNotifier`'s value. ## Usage @@ -23,13 +23,13 @@ dependencies: import 'package:flutter_reactive_value/flutter_reactive_value.dart' ``` -(3) Use the standard Flutter [`ValueNotifier`](https://api.flutter.dev/flutter/foundation/ValueNotifier-class.html) mechanism to declare any values you want your UI to listen for changes to: +(3) Use `ReactiveValueNotifier` rather than the standard Flutter [`ValueNotifier`](https://api.flutter.dev/flutter/foundation/ValueNotifier-class.html) to declare any values you want your UI to listen for changes to: ```dart -final counter = ValueNotifier(0); +final counter = ReactiveValueNotifier(0); ``` -(4) Build your UI the standard way, using a `Widget` hierarchy, and anywhere you want to use the value and respond to future changes in the value by updating the UI, instead of using the `ValueNotifier.value` getter method, use `ValueNotifier.reactiveValue(BuildContext)`: +(4) Build your UI the standard way, using a `Widget` hierarchy, and anywhere you want to use the value and respond to future changes in the value by updating the UI, instead of using the usual `ValueNotifier.value` getter method, use `ReactiveValueNotifier.reactiveValue(BuildContext)`: ```dart class HomeView extends StatelessWidget { @@ -97,7 +97,7 @@ Container( If you try to wrap a collection or object in a `ValueNotifier`, e.g. to track a set of values using `ValueNotifier>`, then modifying fields, or adding or removing values from the collection or object will not notify the listeners of the `ValueNotifier` that the value has changed (because the value itself has not changed). In this case you can call the extension method `notifyChanged()` to manually call the listeners. For example: ```dart -final tags = ValueNotifier({}); +final tags = ReactiveValueNotifier({}); void addOrRemoveTag(String tag, bool add) { if ((add && tags.value.add(tag)) || (!add && tags.value.remove(tag))) { @@ -110,10 +110,6 @@ void addOrRemoveTag(String tag, bool add) { Use `flutter_reactive_value` together with my other library, [`flutter_persistent_value_notifier`](https://github.com/lukehutch/flutter_persistent_value_notifier), to enable persistent reactive state in your app! -## The code - -The `flutter_reactive_value` mechanism is extremely simple. [See the code here.](https://github.com/lukehutch/flutter_reactive_value/blob/main/lib/src/reactive_value_notifier.dart) - ## Author `flutter_reactive_value` was written by Luke Hutchison, and is released under the MIT license. \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart index 12494e1..ecc135b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,5 @@ -import 'package:flutter_reactive_value/flutter_reactive_value.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_reactive_value/flutter_reactive_value.dart'; void main() async { runApp( @@ -10,7 +10,7 @@ void main() async { ); } -final counter = ValueNotifier(0); +final counter = ReactiveValueNotifier(0); class HomeView extends StatelessWidget { const HomeView({Key? key}) : super(key: key); diff --git a/lib/src/reactive_value_notifier.dart b/lib/src/reactive_value_notifier.dart index 7846f03..11d7e25 100644 --- a/lib/src/reactive_value_notifier.dart +++ b/lib/src/reactive_value_notifier.dart @@ -9,33 +9,60 @@ class _ListenerWrapper { /// Extend [ValueNotifier] so that [Element] objects in the build tree can /// respond to changes in the value. -extension ReactiveValueNotifier on ValueNotifier { +class ReactiveValueNotifier extends ValueNotifier { + ReactiveValueNotifier(super.value); + + /// An Expando mapping from `Element` objects to `true`, if the `Element` + /// is subscribed to this `ValueNotifier`. + final _subscribedElements = Expando(); + /// Fetch the [value] of this [ValueNotifier], and subscribe the element /// that is currently being built (the [context]) to any changes in the /// value. T reactiveValue(BuildContext context) { - final elementRef = WeakReference(context as Element); + final element = context as Element; + // If element is already subscribed, return the current value without + // subscribing again + if (_subscribedElements[element] == true) { + return value; + } + // Add a weak reference to the element to the Expando of subscribed + // elements + _subscribedElements[element] = true; + + // Create a weak reference to the element + final elementRef = WeakReference(element); + final listenerWrapper = _ListenerWrapper(); listenerWrapper.listener = () { + // State is not supposed to be mutated during `build` assert( SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks, 'Do not mutate state (by setting the value of the ValueNotifier ' 'that you are subscribed to) during a `build` method. If you need ' 'to schedule a value update after `build` has completed, use ' - '`SchedulerBinding.instance.scheduleTask(updateTask, Priority.idle)`, ' - '`SchedulerBinding.addPostFrameCallback(updateTask)`, ' + 'SchedulerBinding.instance.scheduleTask(updateTask, Priority.idle), ' + 'SchedulerBinding.addPostFrameCallback(updateTask), ' 'or similar.'); // If the element has not been garbage collected (causing // `elementRef.target` to be null), or unmounted - if (elementRef.target?.mounted ?? false) { - // Mark the element as needing to be rebuilt - elementRef.target!.markNeedsBuild(); + final elementRefTarget = elementRef.target; + if (elementRefTarget != null) { + if (elementRefTarget.mounted) { + // Then mark the element as needing to be rebuilt + elementRefTarget.markNeedsBuild(); + } + // Remove the element from the Expando of subscribed elements + _subscribedElements[elementRefTarget] = null; } - // Remove the listener -- only listen to one change per `build` + // Remove the listener -- only listen to one change per build + // (each subsequent build will resubscribe) removeListener(listenerWrapper.listener!); }; + // Listen to changes to the ReactiveValue addListener(listenerWrapper.listener!); + // Return the current value return value; } diff --git a/pubspec.yaml b/pubspec.yaml index 1b9c202..d919ca6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_reactive_value description: Simple reactive state management for Flutter. -version: 1.0.4 +version: 2.0.0 homepage: https://github.com/lukehutch/flutter_reactive_value environment: