Creating a Real-time Streaming Chat UI in Flutter for LLM Responses
Enhance your user experience by implementing real-time, streaming chat responses in your Flutter application using Streams and StreamBuilder.
Posted on: 2026-03-21

When building AI-powered chat applications, user experience is paramount. Waiting for a large language model (LLM) to generate a complete response before displaying it can feel sluggish and frustrating for users. Streaming—where tokens are displayed as they are generated—significantly reduces perceived latency and makes your app feel much more responsive and “alive.”
In this guide, we’ll implement a simple, real-time streaming chat UI in Flutter using Dart Streams and the StreamBuilder widget.
Why Streaming Matters
- Reduced Perceived Latency: Users see progress immediately instead of waiting for a blank screen to fill.
- Modern Interaction: It mimics the behavior of popular AI tools like ChatGPT and Gemini.
- Better Feedback: Provides visual confirmation that the system is working.
1. The Foundation: Dart Streams
At its core, a stream is a sequence of asynchronous events. In the context of an LLM, these events are “chunks” of text.
Stream<String> mockLlmStream(String prompt) async* {
final words = "This is a simulated streaming response from an LLM. It shows how chunks of text appear over time.".split(' ');
for (var word in words) {
await Future.delayed(const Duration(milliseconds: 100));
yield "$word ";
}
}
2. Implementing the Chat UI
We’ll build a ChatScreen that uses a StreamBuilder to update the latest message in real-time.
import 'package:flutter/material.dart';
class ChatScreen extends StatefulWidget {
const ChatScreen({super.key});
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
final List<String> _messages = [];
final TextEditingController _controller = TextEditingController();
Stream<String>? _currentStream;
String _currentResponse = "";
void _sendMessage() {
if (_controller.text.isEmpty) return;
setState(() {
_messages.add("User: ${_controller.text}");
_currentResponse = "";
_currentStream = mockLlmStream(_controller.text);
_controller.clear();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Streaming Chat')),
body: Column(
children: [
Expanded(
child: ListView.builder(
itemCount: _messages.length + (_currentStream != null ? 1 : 0),
itemBuilder: (context, index) {
if (index < _messages.length) {
return ListTile(title: Text(_messages[index]));
}
return StreamBuilder<String>(
stream: _currentStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
_currentResponse += snapshot.data!;
}
if (snapshot.connectionState == ConnectionState.done) {
// Optionally add the complete response to the message list
}
return ListTile(
title: Text("AI: $_currentResponse"),
trailing: snapshot.connectionState == ConnectionState.waiting
? const CircularProgressIndicator()
: null,
);
},
);
},
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(child: TextField(controller: _controller)),
IconButton(icon: const Icon(Icons.send), onPressed: _sendMessage),
],
),
),
],
),
);
}
}
3. Best Practices for Streaming UIs
- Auto-Scrolling: As the stream grows, ensure the
ListViewautomatically scrolls to the bottom so the newest text is always visible. - Error Handling: Use the
onErrorparameter inStreamBuilderto gracefully handle network issues or API failures. - Visual Polish: Use a cursor effect (like a blinking underscore) at the end of the streaming text to further enhance the “typing” feel.
- Debouncing: If you’re updating state too frequently, it might impact UI performance on lower-end devices.
Conclusion
Implementing a streaming chat UI in Flutter is surprisingly straightforward thanks to Dart’s built-in support for Streams. By showing responses as they’re generated, you create a significantly more engaging and professional user experience for your AI applications.
What’s Next? Try integrating this with a real LLM API like the Gemini API or a local Ollama instance!