Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
18 changes: 15 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,13 @@ 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 signal is always initialized, but the value is lazy.
// Accessing hasValue triggers computation if not yet initialized.
@override
bool get hasValue => true;
bool get hasValue {
if (!_disposed && !_initialized) value;
return true;
}

final _deps = <alien.ReactiveNode>{};

Expand All @@ -149,7 +156,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 +184,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