flutter_stream_buffer 0.1.0 copy "flutter_stream_buffer: ^0.1.0" to clipboard
flutter_stream_buffer: ^0.1.0 copied to clipboard

Frame-batched text streaming for Flutter AI responses, live captions, and logs. Includes cancellation, bounded accumulation, and a lifecycle-aware builder.

example/lib/main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_stream_buffer/flutter_stream_buffer.dart';

void main() => runApp(const StreamLabApp());

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

  @override
  Widget build(BuildContext context) => MaterialApp(
    title: 'Stream Lab',
    debugShowCheckedModeBanner: false,
    theme: ThemeData(
      colorScheme: ColorScheme.fromSeed(
        seedColor: const Color(0xFF8CF3CA),
        brightness: Brightness.dark,
        surface: const Color(0xFF101519),
      ),
      scaffoldBackgroundColor: const Color(0xFF101519),
      useMaterial3: true,
    ),
    home: const StreamLab(),
  );
}

class StreamLab extends StatefulWidget {
  const StreamLab({super.key});

  @override
  State<StreamLab> createState() => _StreamLabState();
}

class _StreamLabState extends State<StreamLab> {
  final _controller = StreamTextController(maxCodeUnits: 16000);
  int _notifications = 0;
  String _scenario = 'Ready when you are';

  static const _answer = '''A quieter way to stream.

Your backend sends small pieces of text. The buffer keeps every accepted piece in order, then delivers one combined snapshot on the next display frame.

Use your existing text or Markdown renderer. Keep your own network client, provider, and state management. The package only owns text accumulation and the stream lifecycle.

Try another response while this one is running. The previous subscription is cancelled, and its late events cannot overwrite the new response.

If the connection fails, the partial answer stays readable. If a response exceeds the configured limit, the oversized chunk is rejected and the subscription stops.

Built for AI responses, live captions, and streamed text. No API key is needed for this demo.''';

  @override
  void initState() {
    super.initState();
    _controller.addListener(_countNotification);
  }

  void _countNotification() => _notifications++;

  Stream<String> _response({bool fail = false}) async* {
    final words = _answer.split(' ');
    for (var index = 0; index < words.length; index++) {
      if (index % 4 == 0) {
        await Future<void>.delayed(const Duration(milliseconds: 65));
      }
      if (fail && index == 28) {
        throw StateError(
          'Simulated connection lost. Accepted text is retained.',
        );
      }
      yield '${words[index]}${index == words.length - 1 ? '' : ' '}';
    }
  }

  void _start(String scenario, Stream<String> stream) {
    setState(() {
      _notifications = 0;
      _scenario = scenario;
    });
    _controller.start(stream);
  }

  @override
  void dispose() {
    _controller.removeListener(_countNotification);
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => Scaffold(
    body: SafeArea(
      child: Center(
        child: ConstrainedBox(
          constraints: const BoxConstraints(maxWidth: 1120),
          child: SingleChildScrollView(
            padding: const EdgeInsets.all(24),
            child: ValueListenableBuilder<StreamTextSnapshot>(
              valueListenable: _controller,
              builder: (context, snapshot, _) => Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Row(
                    children: [
                      Icon(Icons.waves_rounded, color: Color(0xFF8CF3CA)),
                      SizedBox(width: 12),
                      Expanded(
                        child: Text(
                          'flutter_stream_buffer  /  0.1.0',
                          style: TextStyle(
                            fontFamily: 'monospace',
                            fontSize: 13,
                          ),
                        ),
                      ),
                    ],
                  ),
                  const SizedBox(height: 48),
                  Text(
                    'Fast streams.\nCalm interfaces.',
                    style: Theme.of(context).textTheme.displaySmall?.copyWith(
                      fontWeight: FontWeight.w700,
                      letterSpacing: -1.5,
                    ),
                  ),
                  const SizedBox(height: 16),
                  ConstrainedBox(
                    constraints: const BoxConstraints(maxWidth: 640),
                    child: const Text(
                      'One frame at a time. Explore text buffering with a '
                      'simulated response, a burst of 1,000 chunks, or a '
                      'connection failure. Everything runs locally.',
                      style: TextStyle(fontSize: 17, height: 1.6),
                    ),
                  ),
                  const SizedBox(height: 32),
                  Wrap(
                    spacing: 10,
                    runSpacing: 10,
                    children: [
                      FilledButton.icon(
                        onPressed: () =>
                            _start('Streaming response', _response()),
                        icon: const Icon(Icons.play_arrow_rounded),
                        label: const Text('Replay response'),
                      ),
                      OutlinedButton.icon(
                        onPressed: () => _start(
                          '1,000 chunks in one burst',
                          Stream.fromIterable(
                            List.generate(1000, (index) => '${index + 1} '),
                          ),
                        ),
                        icon: const Icon(Icons.bolt_rounded),
                        label: const Text('1,000-chunk burst'),
                      ),
                      OutlinedButton(
                        onPressed: () => _start(
                          'Partial response with a simulated failure',
                          _response(fail: true),
                        ),
                        child: const Text('Simulate error'),
                      ),
                      OutlinedButton(
                        onPressed: () => _start(
                          '16,000-code-unit limit',
                          Stream.fromIterable([
                            'This accepted text survives the oversized chunk.',
                            'x' * 16001,
                          ]),
                        ),
                        child: const Text('Test size limit'),
                      ),
                      TextButton.icon(
                        onPressed: snapshot.isStreaming
                            ? () => unawaited(_controller.cancel())
                            : null,
                        icon: const Icon(Icons.stop_rounded),
                        label: const Text('Cancel'),
                      ),
                    ],
                  ),
                  const SizedBox(height: 28),
                  Wrap(
                    spacing: 12,
                    runSpacing: 12,
                    children: [
                      _Metric(
                        label: 'SOURCE CHUNKS',
                        value: '${snapshot.receivedChunks}',
                      ),
                      _Metric(label: 'SNAPSHOTS', value: '$_notifications'),
                      _Metric(
                        label: 'UTF-16 CODE UNITS',
                        value: '${snapshot.text.length}',
                      ),
                      _Metric(label: 'STATUS', value: snapshot.status.name),
                    ],
                  ),
                  const SizedBox(height: 24),
                  Container(
                    width: double.infinity,
                    padding: const EdgeInsets.all(24),
                    decoration: BoxDecoration(
                      color: const Color(0xFF192126),
                      borderRadius: BorderRadius.circular(24),
                      border: Border.all(color: const Color(0xFF31413F)),
                    ),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          _scenario,
                          style: const TextStyle(
                            color: Color(0xFF8CF3CA),
                            fontWeight: FontWeight.w600,
                          ),
                        ),
                        const SizedBox(height: 20),
                        ConstrainedBox(
                          constraints: const BoxConstraints(
                            minHeight: 220,
                            maxHeight: 420,
                          ),
                          child: SingleChildScrollView(
                            child: SelectableText(
                              snapshot.text.isEmpty
                                  ? 'Choose a scenario above to see the buffer in action.'
                                  : snapshot.text,
                              style: const TextStyle(fontSize: 17, height: 1.7),
                            ),
                          ),
                        ),
                        if (snapshot.hasError) ...[
                          const Divider(height: 32),
                          Text(
                            snapshot.error.toString(),
                            style: TextStyle(
                              color: Theme.of(context).colorScheme.error,
                            ),
                          ),
                        ],
                      ],
                    ),
                  ),
                  const SizedBox(height: 20),
                  const Text(
                    'Counters show input events and delivered snapshots, not FPS. '
                    'The buffer does not measure rendering speed or network latency.',
                    style: TextStyle(color: Color(0xFFABBDB5), height: 1.5),
                  ),
                  const SizedBox(height: 36),
                  const Text(
                    'Your stream → frame buffer → your renderer',
                    style: TextStyle(fontFamily: 'monospace', height: 1.6),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    ),
  );
}

class _Metric extends StatelessWidget {
  const _Metric({required this.label, required this.value});

  final String label;
  final String value;

  @override
  Widget build(BuildContext context) => Container(
    width: 174,
    padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16),
    decoration: BoxDecoration(
      borderRadius: BorderRadius.circular(16),
      border: Border.all(color: const Color(0xFF31413F)),
    ),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(label, style: const TextStyle(fontSize: 10, letterSpacing: 1.1)),
        const SizedBox(height: 6),
        Text(
          value,
          style: Theme.of(
            context,
          ).textTheme.headlineSmall?.copyWith(color: const Color(0xFF8CF3CA)),
        ),
      ],
    ),
  );
}
1
likes
160
points
0
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Frame-batched text streaming for Flutter AI responses, live captions, and logs. Includes cancellation, bounded accumulation, and a lifecycle-aware builder.

Repository (GitHub)
View/report issues
Contributing

Topics

#streaming #ai #performance #widget #chat

License

MIT (license)

Dependencies

flutter

More

Packages that depend on flutter_stream_buffer