Skip to content

Commit f862b2d

Browse files
committed
✨ update documentation to clarify safe usage patterns for Flutter's StreamBuilder with streams
1 parent f13bfd0 commit f862b2d

File tree

3 files changed

+183
-0
lines changed

3 files changed

+183
-0
lines changed

packages/hyper_storage/docs/reactivity.md

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- [Streaming Item Holder Changes](#streaming-item-holder-changes)
77
- [Converting Item Holder to a ValueNotifier](#converting-item-holder-to-a-valuenotifier)
88
- [Streaming key changes](#streaming-key-changes)
9+
- [⚠️ Important: Using Streams with Flutter's StreamBuilder](#️-important-using-streams-with-flutters-streambuilder)
910
- [Streaming with Serializable Containers](#streaming-with-serializable-containers)
1011

1112
## Reactivity
@@ -189,6 +190,144 @@ final subscription = emailStream.listen((newEmail) {
189190
subscription.cancel();
190191
```
191192

193+
## ⚠️ Important: Using Streams with Flutter's StreamBuilder
194+
195+
When using streams with Flutter's `StreamBuilder`, it's crucial to understand the difference between safe and unsafe patterns.
196+
197+
### **Unsafe Pattern: Calling `stream()` directly in build method**
198+
199+
**DO NOT do this:**
200+
201+
```dart
202+
class MyWidget extends StatelessWidget {
203+
@override
204+
Widget build(BuildContext context) {
205+
return StreamBuilder<String?>(
206+
stream: storage.stream<String>('name'), // ❌ BAD: Creates new stream every rebuild!
207+
builder: (context, snapshot) {
208+
return Text(snapshot.data ?? 'Unknown');
209+
},
210+
);
211+
}
212+
}
213+
```
214+
215+
**Why this is problematic:**
216+
217+
- Every time `build()` is called, a **new stream instance** is created
218+
- `StreamBuilder` detects a different stream and recreates the subscription
219+
- This causes:
220+
- Unnecessary memory allocations (new `StreamController` each time)
221+
- Performance overhead (creating/destroying subscriptions repeatedly)
222+
- The initial value is re-fetched unnecessarily
223+
- Potential UI flickering as the stream restarts
224+
225+
### **Safe Pattern 1: Use ItemHolder (Recommended)**
226+
227+
The recommended approach is to use `ItemHolder`, which is specifically designed for this use case:
228+
229+
```dart
230+
class MyWidget extends StatefulWidget {
231+
@override
232+
State<MyWidget> createState() => _MyWidgetState();
233+
}
234+
235+
class _MyWidgetState extends State<MyWidget> {
236+
// Create ItemHolder once - it's a persistent stream
237+
late final itemHolder = storage.itemHolder<String>('name');
238+
239+
@override
240+
Widget build(BuildContext context) {
241+
return StreamBuilder<String?>(
242+
stream: itemHolder, // ✅ SAFE: ItemHolder is the same instance every time
243+
builder: (context, snapshot) {
244+
if (snapshot.connectionState == ConnectionState.waiting) {
245+
return CircularProgressIndicator();
246+
}
247+
if (snapshot.hasError) {
248+
return Text('Error: ${snapshot.error}');
249+
}
250+
return Text(snapshot.data ?? 'Unknown');
251+
},
252+
);
253+
}
254+
255+
@override
256+
void dispose() {
257+
itemHolder.dispose(); // Clean up when done
258+
super.dispose();
259+
}
260+
}
261+
```
262+
263+
**Why ItemHolder is safe:**
264+
265+
- `ItemHolder` **is** a `Stream` - it implements `Stream<E?>`
266+
- It uses a single, persistent `StreamController.broadcast()`
267+
- The same `ItemHolder` instance is reused on every build
268+
- Efficient: no unnecessary object creation or subscription cycling
269+
270+
### **Safe Pattern 2: Cache the stream in a variable**
271+
272+
If you prefer to use the `stream()` method, cache it in a `late final` variable:
273+
274+
```dart
275+
class MyWidget extends StatefulWidget {
276+
@override
277+
State<MyWidget> createState() => _MyWidgetState();
278+
}
279+
280+
class _MyWidgetState extends State<MyWidget> {
281+
// Cache the stream - created once and reused
282+
late final Stream<String?> nameStream = storage.stream<String>('name');
283+
284+
@override
285+
Widget build(BuildContext context) {
286+
return StreamBuilder<String?>(
287+
stream: nameStream, // ✅ SAFE: Same stream instance reused
288+
builder: (context, snapshot) {
289+
return Text(snapshot.data ?? 'Unknown');
290+
},
291+
);
292+
}
293+
}
294+
```
295+
296+
### **Safe Pattern 3: Create stream in `initState()`**
297+
298+
Alternatively, create the stream in `initState()`:
299+
300+
```dart
301+
class _MyWidgetState extends State<MyWidget> {
302+
late Stream<String?> nameStream;
303+
304+
@override
305+
void initState() {
306+
super.initState();
307+
nameStream = storage.stream<String>('name');
308+
}
309+
310+
@override
311+
Widget build(BuildContext context) {
312+
return StreamBuilder<String?>(
313+
stream: nameStream, // ✅ SAFE: Same stream instance
314+
builder: (context, snapshot) {
315+
return Text(snapshot.data ?? 'Unknown');
316+
},
317+
);
318+
}
319+
}
320+
```
321+
322+
### Summary: Which pattern should you use?
323+
324+
| Pattern | Recommended? | When to use |
325+
|---------|-------------|-------------|
326+
| **ItemHolder** |**Best** | Default choice for most cases. Clean, efficient, purpose-built for Flutter. |
327+
| **Cached stream (late final)** | ✅ Good | When you need the `stream()` method specifically. |
328+
| **initState stream** | ✅ Good | When initialization logic is complex. |
329+
| **Direct stream() call in build()** |**Never** | Don't use - causes performance issues. |
330+
192331
## Streaming with Serializable Containers
193332

194333
You can also stream changes in a `SerializableContainer`. This is useful when you want to listen to changes in a complex

packages/hyper_storage/lib/src/hyper_storage.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,28 @@ class HyperStorage extends _HyperStorageImpl {
398398
/// to avoid memory leaks. This can be done by cancelling the subscription
399399
/// to the stream.
400400
///
401+
/// ⚠️ **Flutter StreamBuilder Usage:**
402+
/// Do NOT call this method directly in a build method. Each call creates a
403+
/// new stream instance, causing unnecessary rebuilds and performance issues.
404+
///
405+
/// Instead, either:
406+
/// 1. Use [ItemHolder] which is designed for StreamBuilder (recommended)
407+
/// 2. Cache the stream in a `late final` variable or create it in `initState()`
408+
///
409+
/// Example of the recommended approach:
410+
/// ```dart
411+
/// late final itemHolder = storage.itemHolder<String>('key');
412+
///
413+
/// Widget build(BuildContext context) {
414+
/// return StreamBuilder<String?>(
415+
/// stream: itemHolder, // ✅ Safe: ItemHolder is a persistent stream
416+
/// builder: (context, snapshot) => Text(snapshot.data ?? ''),
417+
/// );
418+
/// }
419+
/// ```
420+
///
421+
/// See the reactivity documentation for more examples and patterns.
422+
///
401423
/// Note that only supported types are allowed for [E].
402424
/// Supported types are:
403425
/// - String

packages/hyper_storage/lib/src/hyper_storage_container.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,28 @@ final class HyperStorageContainer extends StorageContainer with ItemHolderMixin,
270270
/// to avoid memory leaks. This can be done by cancelling the subscription
271271
/// to the stream.
272272
///
273+
/// ⚠️ **Flutter StreamBuilder Usage:**
274+
/// Do NOT call this method directly in a build method. Each call creates a
275+
/// new stream instance, causing unnecessary rebuilds and performance issues.
276+
///
277+
/// Instead, either:
278+
/// 1. Use [ItemHolder] which is designed for StreamBuilder (recommended)
279+
/// 2. Cache the stream in a `late final` variable or create it in `initState()`
280+
///
281+
/// Example of the recommended approach:
282+
/// ```dart
283+
/// late final itemHolder = container.itemHolder<String>('key');
284+
///
285+
/// Widget build(BuildContext context) {
286+
/// return StreamBuilder<String?>(
287+
/// stream: itemHolder, // ✅ Safe: ItemHolder is a persistent stream
288+
/// builder: (context, snapshot) => Text(snapshot.data ?? ''),
289+
/// );
290+
/// }
291+
/// ```
292+
///
293+
/// See the reactivity documentation for more examples and patterns.
294+
///
273295
/// Note that only supported types are allowed for [E].
274296
/// Supported types are:
275297
/// - String

0 commit comments

Comments
 (0)