Skip to content

Commit 94c58f4

Browse files
committed
✨ enhance ItemHolder to use ManagedStream for improved stream handling and error management
1 parent f862b2d commit 94c58f4

File tree

9 files changed

+1570
-206
lines changed

9 files changed

+1570
-206
lines changed

packages/hyper_storage/docs/reactivity.md

Lines changed: 59 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,14 @@ container.removeListener(onSettingsChanged);
104104
Item Holder also supports streaming changes using Dart's `Stream` API. This allows you to listen to changes in a more
105105
flexible way, such as using `StreamBuilder` in Flutter.
106106

107-
`ItemHolder<E>` implements `Stream<E?>`, so you can listen to it directly.
107+
`ItemHolder<E>` extends `ManagedStream<E?>` and implements `Stream<E?>`, so you can listen to it directly.
108+
109+
## Implementation Details
110+
111+
ItemHolder uses a `BehaviorSubject` (from rxdart) internally, which provides several benefits:
112+
- **Automatic value caching**: The latest value is cached and automatically emitted to new listeners
113+
- **Multiple concurrent listeners**: Efficiently supports many listeners without duplicating work
114+
- **Lazy activation**: Only starts fetching values when someone is actually listening
108115

109116
```dart
110117
final itemHolder = storage.itemHolder<String>('status');
@@ -118,7 +125,9 @@ final subscription = itemHolder.listen((newStatus) {
118125
subscription.cancel();
119126
```
120127

121-
Using with StreamBuilder in Flutter:
128+
## Using with StreamBuilder in Flutter
129+
130+
ItemHolder is specifically designed for use with Flutter's `StreamBuilder`:
122131

123132
```dart
124133
StreamBuilder<String?>(
@@ -127,15 +136,17 @@ StreamBuilder<String?>(
127136
if (snapshot.connectionState == ConnectionState.waiting) {
128137
return CircularProgressIndicator();
129138
}
130-
if (snapshot.hasError) {
131-
return Text('Error: ${snapshot.error}');
132-
}
139+
// Note: ItemHolder doesn't emit errors - they're handled silently
133140
final status = snapshot.data ?? 'Unknown';
134141
return Text('Status: $status');
135142
},
136143
);
137144
```
138145

146+
**Note**: Unlike traditional streams, ItemHolder does **not** emit errors during value retrieval. This prevents
147+
transient failures (like network issues) from being cached and replayed to future listeners. The stream simply
148+
retains its last valid value and retries on the next update.
149+
139150
## Converting Item Holder to a ValueNotifier
140151

141152
You can convert an `ItemHolder` to a `ValueNotifier` for easier integration with Flutter's state management.
@@ -192,39 +203,24 @@ subscription.cancel();
192203

193204
## ⚠️ Important: Using Streams with Flutter's StreamBuilder
194205

195-
When using streams with Flutter's `StreamBuilder`, it's crucial to understand the difference between safe and unsafe patterns.
206+
When using streams with Flutter's `StreamBuilder`, it's important to understand the best patterns and practices.
196207

197-
### **Unsafe Pattern: Calling `stream()` directly in build method**
208+
### **The `stream()` Method is Now Safe**
198209

199-
**DO NOT do this:**
210+
As of the latest version, the `stream()` method returns a **cached `ItemHolder` instance**, making it safe to call repeatedly:
200211

201212
```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+
// These both return the exact same ItemHolder object:
214+
final stream1 = storage.stream<String>('name');
215+
final stream2 = storage.stream<String>('name');
216+
print(identical(stream1, stream2)); // true
213217
```
214218

215-
**Why this is problematic:**
219+
This means calling `storage.stream('key')` directly in a build method is now technically safe, as it returns the same cached instance each time. However, for code clarity and best practices, we still recommend the patterns below.
216220

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
221+
### **Recommended Pattern: Use ItemHolder Directly**
224222

225-
### **Safe Pattern 1: Use ItemHolder (Recommended)**
226-
227-
The recommended approach is to use `ItemHolder`, which is specifically designed for this use case:
223+
The clearest and most explicit approach is to use `ItemHolder` directly:
228224

229225
```dart
230226
class MyWidget extends StatefulWidget {
@@ -233,20 +229,18 @@ class MyWidget extends StatefulWidget {
233229
}
234230
235231
class _MyWidgetState extends State<MyWidget> {
236-
// Create ItemHolder once - it's a persistent stream
232+
// Create ItemHolder once - it's a persistent stream with value caching
237233
late final itemHolder = storage.itemHolder<String>('name');
238234
239235
@override
240236
Widget build(BuildContext context) {
241237
return StreamBuilder<String?>(
242-
stream: itemHolder, // ✅ SAFE: ItemHolder is the same instance every time
238+
stream: itemHolder, // ✅ BEST: Clear intent, uses ItemHolder directly
243239
builder: (context, snapshot) {
244240
if (snapshot.connectionState == ConnectionState.waiting) {
245241
return CircularProgressIndicator();
246242
}
247-
if (snapshot.hasError) {
248-
return Text('Error: ${snapshot.error}');
249-
}
243+
// Note: ItemHolder doesn't emit errors - no need to check snapshot.hasError
250244
return Text(snapshot.data ?? 'Unknown');
251245
},
252246
);
@@ -260,16 +254,18 @@ class _MyWidgetState extends State<MyWidget> {
260254
}
261255
```
262256

263-
**Why ItemHolder is safe:**
257+
**Why ItemHolder is recommended:**
264258

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
259+
- **Clear intent**: Makes it obvious you're using a managed stream
260+
- **Value caching**: Uses `BehaviorSubject` - new listeners get the cached value immediately
261+
- **Efficient**: Single persistent stream with lazy activation
262+
- **Multiple listeners**: Supports many concurrent listeners without duplicating work
263+
- **No error emissions**: Transient failures don't get cached and replayed
269264

270-
### **Safe Pattern 2: Cache the stream in a variable**
265+
### **Also Acceptable: Cache the stream in a variable**
271266

272-
If you prefer to use the `stream()` method, cache it in a `late final` variable:
267+
If you prefer to use the `stream()` method, you can cache it in a `late final` variable.
268+
Since `stream()` returns a cached `ItemHolder`, this is now equivalent to Pattern 1:
273269

274270
```dart
275271
class MyWidget extends StatefulWidget {
@@ -278,24 +274,33 @@ class MyWidget extends StatefulWidget {
278274
}
279275
280276
class _MyWidgetState extends State<MyWidget> {
281-
// Cache the stream - created once and reused
277+
// Cache the stream - actually returns the same ItemHolder as itemHolder<String>('name')
282278
late final Stream<String?> nameStream = storage.stream<String>('name');
283279
284280
@override
285281
Widget build(BuildContext context) {
286282
return StreamBuilder<String?>(
287-
stream: nameStream, // ✅ SAFE: Same stream instance reused
283+
stream: nameStream, // ✅ SAFE: Same cached ItemHolder instance
288284
builder: (context, snapshot) {
289285
return Text(snapshot.data ?? 'Unknown');
290286
},
291287
);
292288
}
289+
290+
@override
291+
void dispose() {
292+
// If using stream(), you can cast to dispose if needed
293+
if (nameStream is ItemHolder) {
294+
(nameStream as ItemHolder).dispose();
295+
}
296+
super.dispose();
297+
}
293298
}
294299
```
295300

296-
### **Safe Pattern 3: Create stream in `initState()`**
301+
### **Create stream in `initState()`**
297302

298-
Alternatively, create the stream in `initState()`:
303+
You can also create the stream in `initState()`:
299304

300305
```dart
301306
class _MyWidgetState extends State<MyWidget> {
@@ -304,13 +309,13 @@ class _MyWidgetState extends State<MyWidget> {
304309
@override
305310
void initState() {
306311
super.initState();
307-
nameStream = storage.stream<String>('name');
312+
nameStream = storage.stream<String>('name'); // Returns cached ItemHolder
308313
}
309314
310315
@override
311316
Widget build(BuildContext context) {
312317
return StreamBuilder<String?>(
313-
stream: nameStream, // ✅ SAFE: Same stream instance
318+
stream: nameStream, // ✅ SAFE: Same cached instance
314319
builder: (context, snapshot) {
315320
return Text(snapshot.data ?? 'Unknown');
316321
},
@@ -321,12 +326,12 @@ class _MyWidgetState extends State<MyWidget> {
321326

322327
### Summary: Which pattern should you use?
323328

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. |
329+
| Pattern | Recommended? | Notes |
330+
|---------|-------------|-------|
331+
| **ItemHolder directly** |**Best** | Most explicit. Clear intent. Direct access to ItemHolder API. |
332+
| **Cached stream (late final)** | ✅ Good | Same as above but less explicit. `stream()` returns ItemHolder internally. |
333+
| **initState stream** | ✅ Good | Useful for complex initialization. Still returns cached ItemHolder. |
334+
| **Direct stream() in build()** | ⚠️ **Acceptable but discouraged** | Now safe (returns cached instance) but not recommended for code clarity. |
330335

331336
## Streaming with Serializable Containers
332337

0 commit comments

Comments
 (0)