home / skills / rodydavis / skills / snippets_flutter-stream-widget

snippets_flutter-stream-widget skill

/skills/snippets_flutter-stream-widget

This skill helps you build reactive Flutter UIs by streaming widgets in the build method, enabling dynamic updates and efficient data handling.

npx playbooks add skill rodydavis/skills --skill snippets_flutter-stream-widget

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
4.1 KB
---
name: flutter-stream-widget
description: Learn how to build dynamic Flutter UIs by directly using streams within your widget's build method, enabling reactive screen updates and more efficient data handling.
metadata:
  url: https://rodydavis.com/posts/snippets/flutter-stream-widget
  last_modified: Tue, 03 Feb 2026 20:04:29 GMT
---

# Flutter Stream Widget


Work with streams directly in the build method of a [Flutter](https://flutter.dev/) widget:

```
import 'dart:async';

import 'package:flutter/widgets.dart';

abstract class StreamWidget extends StatefulWidget {
  const StreamWidget({Key? key}) : super(key: key);

  Stream<Widget> build(BuildContext context);

  void initState() {}

  void dispose() {}

  void reassemble() {}

  Widget? buildEmpty(BuildContext context) => null;

  Widget? buildError(BuildContext context, Object? error) => null;

  @override
  State<StreamWidget> createState() => _StreamWidgetState();
}

class _StreamWidgetState extends State<StreamWidget> {
  @override
  void initState() {
    widget.initState.call();
    super.initState();
  }

  @override
  void dispose() {
    widget.dispose.call();
    super.dispose();
  }

  @override
  void reassemble() {
    widget.reassemble.call();
    super.reassemble();
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder(
      stream: widget.build(context),
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          final result = widget.buildError(context, snapshot.error);
          if (result != null) return result;
        }
        if (snapshot.hasData) {
          return snapshot.data!;
        } else {
          final result = widget.buildEmpty(context);
          if (result != null) return result;
        }
        return const SizedBox.shrink();
      },
    );
  }
}
```

This could also be applied to Future widgets, but for reactive screens, streams are closer to what is actually happening.

## Riverpod Example

```
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'generated.g.dart';

@riverpod
class GeneratedWidget extends _$GeneratedWidget {
  @override
  Widget build(BuildContext context) {
    return const Text('Generated widget!');
  }
}

@riverpod
class StreamWidget extends _$StreamWidget {
  @override
  Stream<Widget> build(BuildContext context) async* {
    final controller = StreamController<int>();
    final timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      controller.add(timer.tick);
    });
    yield* controller.stream.map((event) => Text('Stream widget: $event'));
    timer.cancel();
    await controller.close();
  }
}

@riverpod
class FutureWidget extends _$FutureWidget {
  @override
  Future<Widget> build(BuildContext context) async {
    await Future.delayed(const Duration(seconds: 3));
    return const Text('Future completed!');
  }
}

class Example extends StatelessWidget {
  const Example({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Column(
          children: [
            Consumer(builder: (context, ref, child) {
              final generated = ref.watch(generatedWidgetProvider(context));
              return generated;
            }),
            Consumer(builder: (context, ref, child) {
              final stream = ref.watch(streamWidgetProvider(context));
              return stream.when(
                data: (data) => data,
                error: (error, stack) => Text(error.toString()),
                loading: () => const CircularProgressIndicator(),
              );
            }),
            Consumer(builder: (context, ref, child) {
              final future = ref.watch(futureWidgetProvider(context));
              return future.when(
                data: (data) => data,
                error: (error, stack) => Text(error.toString()),
                loading: () => const CircularProgressIndicator(),
              );
            }),
          ],
        ),
      ),
    );
  }
}
```

Overview

This skill teaches how to build dynamic Flutter UIs by emitting Widgets from a Stream inside a widget's build method. It demonstrates a small StreamWidget base class that integrates with Flutter lifecycle hooks and handles empty and error states. The examples include a Riverpod-powered implementation showing stream, future, and generated widgets together. The approach encourages reactive, incremental UI updates instead of rebuilding whole subtrees synchronously.

How this skill works

The StreamWidget exposes a build(BuildContext) method that returns Stream<Widget>. A private State uses StreamBuilder to listen to that stream and render snapshots. Optional callbacks provide initState, dispose, reassemble, buildEmpty, and buildError so you can manage resources and handle loading or error visuals. The pattern can also be adapted for Future<Widget> but streams fit continuous, time-based, or event-driven UI changes better.

When to use it

  • When UI should update incrementally from a sequence of events (timers, sockets, sensors).
  • When you want to separate widget-producing logic into asynchronous producers.
  • When integrating with Riverpod providers that produce streams of Widgets.
  • For dashboards or live feeds where parts of the UI change without full rebuilds.
  • When you need explicit lifecycle control for stream controllers or timers.

Best practices

  • Keep stream-to-widget mapping lightweight to avoid expensive rebuilds each tick.
  • Close controllers and cancel timers in dispose to prevent leaks.
  • Use buildEmpty to show a placeholder or loader while awaiting initial data.
  • Use buildError to present friendly error UI and optionally retry controls.
  • Avoid emitting large widget trees on every event; emit small, composable widgets.

Example use cases

  • A live counter or clock where each tick yields a small Text widget.
  • A chat message pane that yields new message row widgets as they arrive.
  • A sensor dashboard that updates individual metric cards from streams.
  • Integrating with Riverpod providers that expose Stream<Widget> for composition.
  • Progressive content loading where portions of the screen appear as data streams in.

FAQ

Can I use this with Future<Widget> instead of Stream<Widget>?

Yes. The same pattern applies: adapt the State to use FutureBuilder or convert a Future to a single-item stream.

How do I manage resources like StreamController or Timer?

Create them inside your build stream or initState and ensure you cancel timers and close controllers in the provided dispose callback.

Is emitting widgets from streams performant?

Emitting small, focused widgets is performant. Avoid sending large subtrees frequently and rely on composition to minimize work.