|
6 | 6 | - [Streaming Item Holder Changes](#streaming-item-holder-changes) |
7 | 7 | - [Converting Item Holder to a ValueNotifier](#converting-item-holder-to-a-valuenotifier) |
8 | 8 | - [Streaming key changes](#streaming-key-changes) |
| 9 | +- [⚠️ Important: Using Streams with Flutter's StreamBuilder](#️-important-using-streams-with-flutters-streambuilder) |
9 | 10 | - [Streaming with Serializable Containers](#streaming-with-serializable-containers) |
10 | 11 |
|
11 | 12 | ## Reactivity |
@@ -189,6 +190,144 @@ final subscription = emailStream.listen((newEmail) { |
189 | 190 | subscription.cancel(); |
190 | 191 | ``` |
191 | 192 |
|
| 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 | + |
192 | 331 | ## Streaming with Serializable Containers |
193 | 332 |
|
194 | 333 | You can also stream changes in a `SerializableContainer`. This is useful when you want to listen to changes in a complex |
|
0 commit comments