Skip to content

Commit 9c29f19

Browse files
feat solidart hooks v3 (#151)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 904c0c2 commit 9c29f19

14 files changed

Lines changed: 361 additions & 116 deletions

docs-v2/src/content/docs/flutter/solidart_hooks.mdx

Lines changed: 154 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ sidebar:
55
order: 1
66
---
77

8+
import { Aside } from '@astrojs/starlight/components';
9+
810
Helper library to make working with [solidart](https://pub.dev/packages/solidart) in [flutter_hooks](https://pub.dev/packages/flutter_hooks) easier.
911

1012
```dart
@@ -24,12 +26,16 @@ class Example extends HookWidget {
2426
});
2527
return Scaffold(
2628
body: Center(
27-
child: Column(
28-
mainAxisAlignment: MainAxisAlignment.center,
29-
children: [
30-
Text('Count: ${count.value}'),
31-
Text('Double: ${doubleCount.value}'),
32-
],
29+
child: SignalBuilder(
30+
builder: (context, child) {
31+
return Column(
32+
mainAxisAlignment: MainAxisAlignment.center,
33+
children: [
34+
Text('Count: ${count.value}'),
35+
Text('Double: ${doubleCount.value}'),
36+
],
37+
);
38+
}
3339
),
3440
),
3541
floatingActionButton: FloatingActionButton(
@@ -41,7 +47,66 @@ class Example extends HookWidget {
4147
}
4248
```
4349

44-
## useSignal
50+
<Aside type="caution">
51+
As you can see, a `SignalBuilder` is used to rebuild the widget when the signal changes.
52+
This is a good practice to avoid rebuilding the entire widget tree when a signal changes, and only rebuild the parts that depend on the signal.
53+
Alternatively, you can use `useListenable` from `flutter_hooks` to listen to the signal changes, but this will rebuild the entire widget tree; for example:
54+
55+
```dart
56+
class UseSignalExample extends HookWidget {
57+
const UseSignalExample({super.key});
58+
59+
@override
60+
Widget build(BuildContext context) {
61+
final count = useSignal(0);
62+
// this will rebuild the entire widget when the signal changes
63+
useListenable(count);
64+
65+
return Scaffold(
66+
appBar: AppBar(title: const Text('useSignal')),
67+
body: Center(child: Text('Count: ${count.value}')),
68+
floatingActionButton: FloatingActionButton(
69+
onPressed: () => count.value++,
70+
child: const Icon(Icons.add),
71+
),
72+
);
73+
}
74+
}
75+
```
76+
77+
There is another option which is to use `HookBuilder` + `useListenable` to only rebuild a part of the widget tree:
78+
79+
```dart {11-15}
80+
class UseSignalExample extends HookWidget {
81+
const UseSignalExample({super.key});
82+
83+
@override
84+
Widget build(BuildContext context) {
85+
final count = useSignal(0);
86+
87+
return Scaffold(
88+
appBar: AppBar(title: const Text('useSignal')),
89+
body: Center(
90+
child: HookBuilder(
91+
builder: (context) {
92+
useListenable(count);
93+
return Text('Count: ${count.value}');
94+
},
95+
),
96+
),
97+
floatingActionButton: FloatingActionButton(
98+
onPressed: () => count.value++,
99+
child: const Icon(Icons.add),
100+
),
101+
);
102+
}
103+
}
104+
```
105+
106+
But keep in mind that `SignalBuilder` is more optimized and will not trigger unnecessary rebuilds when multiple signals change at the same time.
107+
</Aside>
108+
109+
## **useSignal**
45110

46111
How to create a new signal inside of a hook widget:
47112

@@ -51,7 +116,13 @@ class Example extends HookWidget {
51116
Widget build(BuildContext context) {
52117
final count = useSignal(0);
53118
return Scaffold(
54-
body: Center(child: Text('Count: ${count.value}')),
119+
body: Center(
120+
child: SignalBuilder(
121+
builder: (context, child) {
122+
return Text('Count: ${count.value}');
123+
},
124+
),
125+
),
55126
floatingActionButton: FloatingActionButton(
56127
onPressed: () => count.value++,
57128
child: const Icon(Icons.add),
@@ -64,7 +135,7 @@ class Example extends HookWidget {
64135
The widget will automatically rebuild when the value changes.
65136
The signal will get disposed when the widget gets unmounted.
66137

67-
## useComputed
138+
## **useComputed**
68139

69140
How to create a new computed signal inside of a hook widget:
70141

@@ -76,12 +147,16 @@ class Example extends HookWidget {
76147
final doubled = useComputed(() => count.value * 2);
77148
return Scaffold(
78149
body: Center(
79-
child: Column(
80-
mainAxisAlignment: MainAxisAlignment.center,
81-
children: [
82-
Text('Count: ${count.value}'),
83-
Text('Doubled: ${doubled.value}'),
84-
],
150+
child: SignalBuilder(
151+
builder: (context, child) {
152+
return Column(
153+
mainAxisAlignment: MainAxisAlignment.center,
154+
children: [
155+
Text('Count: ${count.value}'),
156+
Text('Doubled: ${doubled.value}'),
157+
],
158+
);
159+
},
85160
),
86161
),
87162
floatingActionButton: FloatingActionButton(
@@ -96,7 +171,7 @@ class Example extends HookWidget {
96171
The widget will automatically rebuild when the value changes.
97172
The computed will get disposed when the widget gets unmounted.
98173

99-
## useSolidartEffect
174+
## **useSolidartEffect**
100175

101176
How to create a new effect inside of a hook widget:
102177

@@ -109,7 +184,13 @@ class Example extends HookWidget {
109184
debugPrint('Effect triggered! Count: ${count.value}');
110185
});
111186
return Scaffold(
112-
body: Center(child: Text('Count: ${count.value}')),
187+
body: Center(
188+
child: SignalBuilder(
189+
builder: (context, child) {
190+
return Text('Count: ${count.value}');
191+
},
192+
),
193+
),
113194
floatingActionButton: FloatingActionButton(
114195
onPressed: () => count.value++,
115196
child: const Icon(Icons.add),
@@ -119,7 +200,7 @@ class Example extends HookWidget {
119200
}
120201
```
121202

122-
## useListSignal
203+
## **useListSignal**
123204

124205
How to create a new list signal inside of a hook widget:
125206

@@ -129,7 +210,13 @@ class Example extends HookWidget {
129210
Widget build(BuildContext context) {
130211
final items = useListSignal<String>(['Item1', 'Item2']);
131212
return Scaffold(
132-
body: Center(child: Text('Items: ${items.value.join(', ')}')),
213+
body: Center(
214+
child: SignalBuilder(
215+
builder: (context, child) {
216+
return Text('Items: ${items.value.join(', ')}');
217+
},
218+
),
219+
),
133220
floatingActionButton: FloatingActionButton(
134221
onPressed: () => items.add('Item${items.value.length + 1}'),
135222
child: const Icon(Icons.add),
@@ -142,7 +229,7 @@ class Example extends HookWidget {
142229
The widget will automatically rebuild when the list changes.
143230
The signal will get disposed when the widget gets unmounted.
144231

145-
## useSetSignal
232+
## **useSetSignal**
146233

147234
How to create a new set signal inside of a hook widget:
148235

@@ -152,7 +239,13 @@ class Example extends HookWidget {
152239
Widget build(BuildContext context) {
153240
final uniqueItems = useSetSignal<String>({'Item1', 'Item2'});
154241
return Scaffold(
155-
body: Center(child: Text('Items: ${uniqueItems.value.join(', ')}')),
242+
body: Center(
243+
child: SignalBuilder(
244+
builder: (context, child) {
245+
return Text('Items: ${uniqueItems.value.join(', ')}');
246+
},
247+
),
248+
),
156249
floatingActionButton: FloatingActionButton(
157250
onPressed: () => uniqueItems.add('Item${uniqueItems.value.length + 1}'),
158251
child: const Icon(Icons.add),
@@ -165,7 +258,7 @@ class Example extends HookWidget {
165258
The widget will automatically rebuild when the set changes.
166259
The signal will get disposed when the widget gets unmounted.
167260

168-
## useMapSignal
261+
## **useMapSignal**
169262

170263
How to create a new map signal inside of a hook widget:
171264

@@ -176,8 +269,19 @@ class Example extends HookWidget {
176269
final userRoles = useMapSignal<String, String>({'admin': 'John'});
177270
return Scaffold(
178271
body: Center(
179-
child: Text('Roles: ${userRoles.value.entries.map((e) => '${e.key}:${e.value}').join(', ')}'),
180-
),
272+
child: Column(
273+
mainAxisAlignment: MainAxisAlignment.center,
274+
children: [
275+
SignalBuilder(
276+
builder: (context, child) {
277+
return Text(
278+
'Roles: ${userRoles.value.entries.map((e) => '${e.key}:${e.value}').join(', ')}',
279+
);
280+
},
281+
),
282+
],
283+
),
284+
)
181285
floatingActionButton: FloatingActionButton(
182286
onPressed: () => userRoles['user${userRoles.value.length}'] = 'User${userRoles.value.length}',
183287
child: const Icon(Icons.add),
@@ -190,7 +294,7 @@ class Example extends HookWidget {
190294
The widget will automatically rebuild when the map changes.
191295
The signal will get disposed when the widget gets unmounted.
192296

193-
## useResource
297+
## **useResource**
194298

195299
How to create a new resource inside of a hook widget:
196300

@@ -205,10 +309,14 @@ class Example extends HookWidget {
205309
206310
return Scaffold(
207311
body: Center(
208-
child: userResource.state.on(
209-
ready: (data) => Text('Result: $data'),
210-
error: (error, stackTrace) => Text('Error: $error'),
211-
loading: () => const CircularProgressIndicator(),
312+
child: SignalBuilder(
313+
builder: (context, child) {
314+
return userResource.state.on(
315+
ready: (data) => Text('Result: $data'),
316+
error: (error, stackTrace) => Text('Error: $error'),
317+
loading: () => const CircularProgressIndicator(),
318+
);
319+
},
212320
),
213321
),
214322
floatingActionButton: FloatingActionButton(
@@ -220,10 +328,9 @@ class Example extends HookWidget {
220328
}
221329
```
222330

223-
The widget will automatically rebuild when the resource state changes.
224331
The resource will get disposed when the widget gets unmounted.
225332

226-
## useResourceStream
333+
## **useResourceStream**
227334

228335
How to create a new resource from a stream inside of a hook widget:
229336

@@ -237,10 +344,14 @@ class Example extends HookWidget {
237344
238345
return Scaffold(
239346
body: Center(
240-
child: streamResource.state.on(
241-
ready: (data) => Text('Stream value: $data'),
242-
error: (error, stackTrace) => Text('Error: $error'),
243-
loading: () => const CircularProgressIndicator(),
347+
child: SignalBuilder(
348+
builder: (context, child) {
349+
return streamResource.state.on(
350+
ready: (data) => Text('Stream value: $data'),
351+
error: (error, stackTrace) => Text('Error: $error'),
352+
loading: () => const CircularProgressIndicator(),
353+
);
354+
},
244355
),
245356
),
246357
floatingActionButton: FloatingActionButton(
@@ -252,10 +363,9 @@ class Example extends HookWidget {
252363
}
253364
```
254365

255-
The widget will automatically rebuild when the resource state changes.
256366
The resource will get disposed when the widget gets unmounted.
257367

258-
## useExistingSignal
368+
## **useExistingSignal**
259369

260370
How to bind an existing signal inside of a hook widget:
261371

@@ -276,7 +386,13 @@ class _UseExistingSignalExampleState extends State<UseExistingSignalExample> {
276386
final boundSignal = useExistingSignal(existingSignal);
277387
278388
return Scaffold(
279-
body: Center(child: Text('Value: ${boundSignal.value}')),
389+
body: Center(
390+
child: SignalBuilder(
391+
builder: (context, child) {
392+
return Text('Value: ${boundSignal.value}');
393+
},
394+
),
395+
),
280396
floatingActionButton: FloatingActionButton(
281397
onPressed: () => existingSignal.value++,
282398
child: const Icon(Icons.add),
@@ -292,5 +408,4 @@ class _UseExistingSignalExampleState extends State<UseExistingSignalExample> {
292408
}
293409
```
294410

295-
The widget will automatically rebuild when the value changes.
296411
The signal will NOT get disposed when the widget gets unmounted (unless autoDispose is true).

packages/solidart_hooks/CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,30 @@
1+
## 3.0.0
2+
3+
- **BREAKING CHANGE**: `SignalHook` no longer calls `setState` to trigger a rebuild when the signal changes. Instead, you should use `SignalBuilder` to listen to signal changes and rebuild the UI accordingly. This change improves performance and reduces unnecessary rebuilds. You can also use `useListenable` if you want to trigger a rebuild on signal changes.
4+
### Migration Guide
5+
6+
**Before (v2.x):**
7+
```dart
8+
final count = useSignal(0);
9+
return Text('Count: ${count.value}'); // Auto-rebuilds
10+
```
11+
**After (v3.x):**
12+
```dart
13+
final count = useSignal(0);
14+
return SignalBuilder(
15+
builder: (context, child) => Text('Count: ${count.value}'),
16+
);
17+
```
18+
19+
Or use `useListenable` for full widget rebuild:
20+
```dart
21+
final count = useSignal(0);
22+
useListenable(count);
23+
return Text('Count: ${count.value}');
24+
```
25+
This is inline with the behaviour of `useValueNotifier` from `flutter_hooks`.
26+
27+
128
## 2.0.0
229

330
- **FEAT**: Added `useResource`, `useResourceStream`, `useListSignal`, `useSetSignal` and `useMapSignal` hooks.

0 commit comments

Comments
 (0)