Skip to content

Streaming

Amayyas edited this page Dec 5, 2025 · 1 revision

Streaming Responses

Flutter AI SDK supports real-time streaming for all providers, allowing you to display responses as they're generated.

Basic Streaming

await for (final chunk in ai.streamChat('Tell me a story')) {
  if (chunk.isDelta) {
    print(chunk.delta); // Print each text chunk
  }
}

Stream Event Types

The SDK emits different event types during streaming:

enum StreamEventType {
  start,        // Stream started
  delta,        // Text content chunk
  toolCallDelta, // Tool call chunk
  done,         // Stream completed
  error,        // Error occurred
}

Handling All Events

await for (final chunk in ai.streamChat('Write a poem')) {
  switch (chunk.type) {
    case StreamEventType.start:
      print('Starting response...');
      break;
      
    case StreamEventType.delta:
      stdout.write(chunk.delta); // No newline for smooth output
      break;
      
    case StreamEventType.done:
      print('\n\nDone!');
      print('Tokens: ${chunk.usage?.totalTokens}');
      print('Finish reason: ${chunk.finishReason}');
      break;
      
    case StreamEventType.error:
      print('Error: ${chunk.error}');
      break;
      
    case StreamEventType.toolCallDelta:
      print('Tool call: ${chunk.toolCallDelta}');
      break;
  }
}

StreamChunk Properties

Property Type Description
type StreamEventType Event type
delta String? Text content (for delta events)
toolCallDelta ToolCallContent? Tool call data
finishReason FinishReason? Why generation stopped
usage Usage? Token usage (in done event)
error Object? Error details

Convenience Getters

chunk.isStart     // true if type == start
chunk.isDelta     // true if type == delta
chunk.isDone      // true if type == done
chunk.isError     // true if type == error

Flutter Widget Example

Here's how to use streaming in a Flutter app:

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

class ChatScreen extends StatefulWidget {
  @override
  _ChatScreenState createState() => _ChatScreenState();
}

class _ChatScreenState extends State<ChatScreen> {
  final ai = FlutterAI(
    provider: AIProvider.openai,
    config: AIConfig(apiKey: 'your-key'),
  );
  
  String _response = '';
  bool _isLoading = false;
  
  Future<void> _sendMessage(String message) async {
    setState(() {
      _isLoading = true;
      _response = '';
    });
    
    try {
      await for (final chunk in ai.streamChat(message)) {
        if (chunk.isDelta && chunk.delta != null) {
          setState(() {
            _response += chunk.delta!;
          });
        }
      }
    } catch (e) {
      setState(() {
        _response = 'Error: $e';
      });
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          Expanded(
            child: SingleChildScrollView(
              child: Text(_response),
            ),
          ),
          if (_isLoading)
            LinearProgressIndicator(),
          _buildInputField(),
        ],
      ),
    );
  }
  
  Widget _buildInputField() {
    // ... TextField implementation
  }
  
  @override
  void dispose() {
    ai.dispose();
    super.dispose();
  }
}

Streaming with State Management

Using ValueNotifier

class StreamingController {
  final ai = FlutterAI(
    provider: AIProvider.anthropic,
    config: AIConfig(apiKey: 'your-key'),
  );
  
  final response = ValueNotifier<String>('');
  final isStreaming = ValueNotifier<bool>(false);
  
  Future<void> send(String message) async {
    response.value = '';
    isStreaming.value = true;
    
    try {
      await for (final chunk in ai.streamChat(message)) {
        if (chunk.isDelta) {
          response.value += chunk.delta ?? '';
        }
      }
    } finally {
      isStreaming.value = false;
    }
  }
  
  void dispose() {
    response.dispose();
    isStreaming.dispose();
    ai.dispose();
  }
}

Using StreamBuilder

class StreamingChatWidget extends StatelessWidget {
  final Stream<StreamChunk> stream;
  
  const StreamingChatWidget({required this.stream});
  
  @override
  Widget build(BuildContext context) {
    return StreamBuilder<String>(
      stream: stream
          .where((chunk) => chunk.isDelta)
          .map((chunk) => chunk.delta ?? '')
          .scan((acc, delta) => acc + delta, ''),
      builder: (context, snapshot) {
        return Text(snapshot.data ?? '');
      },
    );
  }
}

Cancelling Streams

You can cancel a stream by breaking out of the loop:

bool shouldCancel = false;

Future<void> sendWithCancel(String message) async {
  shouldCancel = false;
  
  await for (final chunk in ai.streamChat(message)) {
    if (shouldCancel) {
      break; // Exits the stream
    }
    
    if (chunk.isDelta) {
      print(chunk.delta);
    }
  }
}

void cancel() {
  shouldCancel = true;
}

Collecting Full Response

If you need both streaming and the full response:

Future<String> streamAndCollect(String message) async {
  final buffer = StringBuffer();
  
  await for (final chunk in ai.streamChat(message)) {
    if (chunk.isDelta && chunk.delta != null) {
      buffer.write(chunk.delta);
      print(chunk.delta); // Real-time output
    }
  }
  
  return buffer.toString();
}

Error Handling in Streams

try {
  await for (final chunk in ai.streamChat(message)) {
    if (chunk.isError) {
      throw chunk.error!;
    }
    // Handle chunk...
  }
} on AIRateLimitError catch (e) {
  print('Rate limited: ${e.retryAfter}');
} on AIError catch (e) {
  print('AI Error: ${e.message}');
} catch (e) {
  print('Unexpected error: $e');
}

Performance Tips

  1. Use StringBuffer for collecting responses instead of string concatenation
  2. Throttle UI updates if needed for very fast streams
  3. Dispose properly to avoid memory leaks
  4. Handle errors within the stream loop

Related

Clone this wiki locally