Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/todos/lib/widgets/todos_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class _TodoListState extends State<TodoList> {
final todos = mapFilterToTodosList(activeFilter).value;
return ListView.builder(
itemCount: todos.length,
itemBuilder: (BuildContext context, int index) {
itemBuilder: (context, index) {
final todo = todos[index];
return TodoItem(
todo: todo,
Expand Down
8 changes: 4 additions & 4 deletions examples/todos/test/widget_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Widget wrapWithMockedTodosController({
}

void main() {
testWidgets('Todos with initial value', (WidgetTester tester) async {
testWidgets('Todos with initial value', (tester) async {
// create controller with an initial value
final initialTodos = List.generate(
3,
Expand All @@ -56,7 +56,7 @@ void main() {
expect(find.text('mock2'), findsOneWidget);
});

testWidgets('Add a todo', (WidgetTester tester) async {
testWidgets('Add a todo', (tester) async {
// Build our App and trigger a frame.
await tester.pumpWidget(
wrapWithMockedTodosController(
Expand All @@ -79,7 +79,7 @@ void main() {
expect(find.text('test todo'), findsOneWidget);
});

testWidgets('Remove a todo', (WidgetTester tester) async {
testWidgets('Remove a todo', (tester) async {
// create controller with an initial value
final initialTodos = List.generate(
3,
Expand Down Expand Up @@ -111,7 +111,7 @@ void main() {
expect(find.text('mock0'), findsNothing);
});

testWidgets('Toggle a todo', (WidgetTester tester) async {
testWidgets('Toggle a todo', (tester) async {
// create controller with an initial value
final initialTodos = List.generate(
2,
Expand Down
1 change: 1 addition & 0 deletions packages/flutter_solidart/test/flutter_solidart_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,7 @@ void main() {
});

testWidgets('(ArgProvider) Signal.updateValue method', (tester) async {
// ignore: avoid_types_on_closure_parameters
final counterProvider = Provider.withArgument((_, int n) => Signal(n));
await tester.pumpWidget(
MaterialApp(
Expand Down
4 changes: 4 additions & 0 deletions packages/solidart/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.8.4

- **FIX**: Prevent `LateInitializationError` when accessing `Computed.untrackedValue` before first `value` access. `hasValue` now triggers lazy computation, and `untrackedValue` asserts with a clear message if accessed before computation.

## 2.8.3

- **FIX**: Handle race conditions in Resource that caused multiple calls to `resolve`.
Expand Down
19 changes: 16 additions & 3 deletions packages/solidart/lib/src/core/computed.dart
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ class Computed<T> extends ReadSignal<T> {

try {
_untrackedValue = selector();
_initialized = true;

if (runnedOnce) {
_notifySignalUpdate();
Expand All @@ -107,6 +108,8 @@ class Computed<T> extends ReadSignal<T> {

bool _disposed = false;

bool _initialized = false;

late T _untrackedValue;

T? _previousValue;
Expand All @@ -120,9 +123,14 @@ class Computed<T> extends ReadSignal<T> {
// Used later to fire each callback when this signal is disposed.
final _onDisposeCallbacks = <VoidCallback>[];

// A computed signal is always initialized
// A computed always reports hasValue == true, but the underlying value is
// lazy: the selector runs on first access. Calling hasValue triggers that
// first computation.
@override
bool get hasValue => true;
bool get hasValue {
if (!_disposed && !_initialized) value;
return true;
}

final _deps = <alien.ReactiveNode>{};

Expand All @@ -149,7 +157,7 @@ class Computed<T> extends ReadSignal<T> {
@override
T get value {
if (_disposed) {
return _untrackedValue;
return untrackedValue;
}

final value = reactiveSystem.getComputedValue(_internalComputed);
Expand Down Expand Up @@ -177,6 +185,11 @@ class Computed<T> extends ReadSignal<T> {
/// Returns the untracked value of the computed.
@override
T get untrackedValue {
assert(
_initialized,
'Computed($name) has not been initialized yet. '
'Access "value" or "hasValue" first to trigger computation.',
);
return _untrackedValue;
Comment thread
nank1ro marked this conversation as resolved.
}

Expand Down
2 changes: 1 addition & 1 deletion packages/solidart/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: solidart
description: A simple State Management solution for Dart applications inspired by SolidJS
version: 2.8.3
version: 2.8.4
repository: https://github.com/nank1ro/solidart
documentation: https://solidart.mariuti.com
topics:
Expand Down
17 changes: 17 additions & 0 deletions packages/solidart/test/solidart_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,23 @@ void main() {
},
);

test('hasValue triggers computation, enabling untrackedValue', () {
final counter = Signal(5);
final doubled = Computed(() => counter.value * 2);

// Before any .value access, hasValue should trigger computation
expect(doubled.hasValue, true);

// untrackedValue should now work without LateInitializationError
expect(doubled.untrackedValue, 10);
});

test('untrackedValue asserts if accessed before computation', () {
final counter = Signal(5);
final doubled = Computed(() => counter.value * 2);
expect(() => doubled.untrackedValue, throwsA(isA<AssertionError>()));
});

test('Computed contains previous value', () async {
final signal = Signal(0);
final derived = Computed(() => signal.value * 2);
Expand Down
Loading