Skip to content

Commit d620efc

Browse files
authored
feat 134 (#136)
1 parent de3b725 commit d620efc

11 files changed

Lines changed: 203 additions & 62 deletions

File tree

packages/flutter_solidart/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
## 2.4.0
2+
3+
- **CHORE**: Detect if `SignalBuilder` didn't track any reactive value and throw a `SignalBuilderWithoutDependenciesError`.
4+
5+
### Changes from solidart
6+
7+
- **FEAT**: Add `run` method to `Computed` to manually trigger an update of its value.
8+
- **FEAT**: Add `run` method to `Effect` to manually re-run the effect.
9+
- **CHORE**: Detect if `Effect` didn't track any reactive value and throw an `EffectWithoutDependenciesException` exception.
10+
111
## 2.3.3
212

313
- **FIX**: Bump the `solidart` dependency to `^2.4.1`.

packages/flutter_solidart/example/lib/main.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import 'dart:developer' as dev;
22

33
import 'package:example/pages/counter.dart';
4-
import 'package:example/pages/derived_signal.dart';
4+
import 'package:example/pages/computed.dart';
55
import 'package:example/pages/effects.dart';
66
import 'package:example/pages/lazy_counter.dart';
77
import 'package:example/pages/map_signal.dart';
@@ -62,7 +62,7 @@ final routes = <String, WidgetBuilder>{
6262
'/counter': (_) => const CounterPage(),
6363
'/lazy-counter': (_) => const LazyCounterPage(),
6464
'/show': (_) => const ShowPage(),
65-
'/derived-signal': (_) => const DerivedSignalsPage(),
65+
'/computed': (_) => const ComputedPage(),
6666
'/effects': (_) => const EffectsPage(),
6767
'/signal-builder': (_) => const SignalBuilderPage(),
6868
'/resource': (_) => const ResourcePage(),

packages/flutter_solidart/example/lib/pages/derived_signal.dart renamed to packages/flutter_solidart/example/lib/pages/computed.dart

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter_solidart/flutter_solidart.dart';
33

4-
class DerivedSignalsPage extends StatefulWidget {
5-
const DerivedSignalsPage({super.key});
4+
class ComputedPage extends StatefulWidget {
5+
const ComputedPage({super.key});
66

77
@override
8-
State<DerivedSignalsPage> createState() => _DerivedSignalsPageState();
8+
State<ComputedPage> createState() => _ComputedPageState();
99
}
1010

11-
class _DerivedSignalsPageState extends State<DerivedSignalsPage> {
11+
class _ComputedPageState extends State<ComputedPage> {
1212
late final count = Signal(0, name: 'count');
1313
late final doubleCount = Computed(() => count.value * 2, name: 'doubleCount');
1414

1515
@override
1616
Widget build(BuildContext context) {
1717
return Scaffold(
18-
appBar: AppBar(
19-
title: const Text('Derived Signals'),
20-
),
18+
appBar: AppBar(title: const Text('Computed')),
2119
body: Center(
2220
child: SignalBuilder(
2321
builder: (_, __) => Column(

packages/flutter_solidart/lib/src/widgets/signal_builder.dart

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@ import 'package:flutter/material.dart';
55
import 'package:flutter/scheduler.dart';
66
import 'package:solidart/solidart.dart';
77

8+
/// {@template SignalBuilderWithoutDependenciesError}
9+
/// This exception would be fired when an effect is created without tracking
10+
/// any dependencies.
11+
/// {@endtemplate}
12+
class SignalBuilderWithoutDependenciesError extends Error {
13+
@override
14+
String toString() => '''
15+
SignalBuilderWithoutDependenciesError: SignalBuilder was created without tracking any dependencies.
16+
Make sure to access at least one reactive value (Signal, Computed, etc.) inside the builder callback.
17+
This might happen if inside your `SignalBuilder.builder` method you are returning a `Builder` widget which won't track reactive values because it is considered a different function because it requires another `builder` function.
18+
''';
19+
}
20+
821
/// The [SignalBuilder] function used to build the widget tracking the signals.
922
typedef SignalBuilderFn = Widget Function(
1023
BuildContext context,
@@ -20,8 +33,6 @@ typedef SignalBuilderOnError = void Function(Object error);
2033
/// The [builder] argument must not be null.
2134
/// The [child] is optional but is good practice to use if part of the widget
2235
/// subtree does not depend on the values of the signals.
23-
/// The [onError] callback is optional and is called when an error occurs in the
24-
/// [builder] function.
2536
/// Example:
2637
///
2738
/// ```dart
@@ -47,7 +58,6 @@ class SignalBuilder extends Widget {
4758
const SignalBuilder({
4859
super.key,
4960
required this.builder,
50-
this.onError,
5161
this.child,
5262
});
5363

@@ -69,20 +79,13 @@ class SignalBuilder extends Widget {
6979
/// {@endtemplate}
7080
final Widget? child;
7181

72-
/// {@template signalbuilder.onerror}
73-
/// An optional callback that is called when an error occurs in the underlying
74-
/// effect when running [builder].
75-
/// {@endtemplate}
76-
final SignalBuilderOnError? onError;
77-
7882
/// The widget that the [builder] builds.
7983
@protected
8084
Widget build(BuildContext context) => builder(context, child);
8185

8286
/// Creates the [SignalBuilderElement] element.
8387
@override
84-
SignalBuilderElement createElement() =>
85-
SignalBuilderElement(this, onError: onError);
88+
SignalBuilderElement createElement() => SignalBuilderElement(this);
8689
}
8790

8891
/// {@template signal-builder-element}
@@ -96,13 +99,7 @@ class SignalBuilder extends Widget {
9699
/// {@endtemplate}
97100
class SignalBuilderElement extends ComponentElement {
98101
/// {@macro signal-builder-element}
99-
SignalBuilderElement(
100-
SignalBuilder super.widget, {
101-
this.onError,
102-
});
103-
104-
/// {@macro signalbuilder.onerror}
105-
final void Function(Object error)? onError;
102+
SignalBuilderElement(SignalBuilder super.widget);
106103

107104
Element? _parent;
108105
Effect? _effect;
@@ -117,7 +114,14 @@ class SignalBuilderElement extends ComponentElement {
117114
_effect = Effect(
118115
_invalidate,
119116
autoDispose: false,
120-
onError: onError,
117+
onError: (error) {
118+
final effectiveError = switch (error) {
119+
EffectWithoutDependenciesError() =>
120+
SignalBuilderWithoutDependenciesError(),
121+
_ => error,
122+
};
123+
_error = effectiveError;
124+
},
121125
detach: true,
122126
autorun: false,
123127
);

packages/flutter_solidart/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ dependencies:
1818
flutter:
1919
sdk: flutter
2020
meta: ^1.11.0
21-
solidart: ^2.4.1
21+
solidart: ^2.5.0
2222

2323
dev_dependencies:
2424
disco: ^1.0.0

packages/flutter_solidart/test/flutter_solidart_test.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -880,4 +880,24 @@ void main() {
880880
},
881881
timeout: const Timeout(Duration(seconds: 1)),
882882
);
883+
884+
testWidgets('SignalBuilder without dependencies throws an error',
885+
(tester) async {
886+
await tester.pumpWidget(
887+
MaterialApp(
888+
home: Scaffold(
889+
body: SignalBuilder(
890+
builder: (_, __) {
891+
return const Text('No dependencies here');
892+
},
893+
),
894+
),
895+
),
896+
);
897+
await tester.pumpAndSettle();
898+
expect(
899+
tester.takeException(),
900+
const TypeMatcher<SignalBuilderWithoutDependenciesError>(),
901+
);
902+
});
883903
}

packages/solidart/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 2.5.0
2+
3+
- **FEAT**: Add `run` method to `Computed` to manually trigger an update of its value.
4+
- **FEAT**: Add `run` method to `Effect` to manually re-run the effect.
5+
- **CHORE**: Detect if `Effect` didn't track any reactive value and throw an `EffectWithoutDependenciesError`.
6+
- **CHORE**: Detect if `SignalBuilder` didn't track any reactive value and throw a `SignalBuilderWithoutDependenciesError`.
7+
18
## 2.4.1
29

310
- **FIX**: `Signal.lazy` which caused an exception.

packages/solidart/lib/src/core/computed.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,15 @@ class Computed<T> extends ReadSignal<T> {
236236
}
237237
// coverage:ignore-end
238238

239+
/// Manually runs the computed to update its value.
240+
/// This is usually not necessary, as the computed will automatically
241+
/// update when its dependencies change.
242+
/// However, in some cases, you may want to force an update.
243+
void run() {
244+
if (_disposed) return;
245+
_internalComputed.update();
246+
}
247+
239248
@override
240249
String toString() {
241250
value;

packages/solidart/lib/src/core/effect.dart

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
part of 'core.dart';
22

3+
/// {@template EffectWithoutDependenciesError}
4+
/// This exception would be fired when an effect is created without tracking
5+
/// any dependencies.
6+
/// {@endtemplate}
7+
class EffectWithoutDependenciesError extends Error {
8+
/// {@macro EffectWithoutDependenciesException}
9+
EffectWithoutDependenciesError({required this.name});
10+
11+
/// The name of the effect
12+
final String name;
13+
14+
// coverage:ignore-start
15+
@override
16+
String toString() =>
17+
'''EffectWithoutDependenciesException: Effect ($name) was created without tracking any dependencies. Make sure to access at least one reactive value (Signal, Computed, etc.) inside the effect callback.''';
18+
// coverage:ignore-end
19+
}
20+
321
/// Dispose function
422
typedef DisposeEffect = void Function();
523

@@ -32,7 +50,7 @@ abstract class ReactionInterface {
3250
/// final counter = Signal(0);
3351
///
3452
/// // effect creation
35-
/// Effect((_) {
53+
/// Effect(() {
3654
/// print("The count is now ${counter.value}");
3755
/// });
3856
/// // The effect prints `The count is now 0`;
@@ -52,20 +70,13 @@ abstract class ReactionInterface {
5270
/// ```
5371
///
5472
/// Whenever you want to stop the effect from running, you just have to call
55-
/// the `dispose()` callback
56-
///
57-
/// You can also dispose an effect inside the callback
73+
/// the returned callback of the `Effect` method:
5874
/// ```dart
59-
/// Effect((dispose) {
60-
/// print("The count is now ${counter.value}");
61-
/// if (counter.value == 1) dispose();
62-
/// });
75+
/// final disposeEffect = Effect(() { /* your code */ });
76+
/// // later
77+
/// disposeEffect(); // this will stop the effect from running
6378
/// ```
6479
///
65-
/// In the example above the effect is disposed when the counter value is equal
66-
/// to 1
67-
///
68-
///
6980
/// Any effect runs at least once immediately when is created with the current
7081
/// signals values.
7182
///
@@ -173,6 +184,8 @@ class Effect implements ReactionInterface {
173184

174185
final _deps = <alien.ReactiveNode>{};
175186

187+
bool _firstRun = true;
188+
176189
/// The subscriber of the effect, do not use it directly.
177190
@protected
178191
alien.ReactiveNode get subscriber => _internalEffect;
@@ -237,11 +250,25 @@ class Effect implements ReactionInterface {
237250
if (SolidartConfig.autoDispose) {
238251
_deps.clear();
239252
var link = _internalEffect.deps;
253+
240254
for (; link != null; link = link.nextDep) {
241-
final dep = link.dep;
255+
_deps.add(link.dep);
256+
}
242257

243-
_deps.add(dep);
258+
if (_firstRun) {
259+
_firstRun = false;
260+
261+
if (_deps.isEmpty) {
262+
if (_onError != null) {
263+
_onError.call(EffectWithoutDependenciesError(name: name));
264+
} else {
265+
// coverage:ignore-start
266+
throw EffectWithoutDependenciesError(name: name);
267+
// coverage:ignore-end
268+
}
269+
}
244270
}
271+
245272
if (!autoDispose || _disposed) return;
246273
if (_internalEffect.deps?.dep == null) {
247274
dispose();

packages/solidart/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: solidart
22
description: A simple State Management solution for Dart applications inspired by SolidJS
3-
version: 2.4.1
3+
version: 2.5.0
44
repository: https://github.com/nank1ro/solidart
55
documentation: https://solidart.mariuti.com
66
topics:

0 commit comments

Comments
 (0)