How to Build an AI Chat App in Flutter (Streaming, From Scratch vs Pre-Wired)
A streaming ChatGPT-style chat is mostly plumbing: SSE parsing, token buffering, a secure key proxy, and BLoC state that doesn't rebuild on every chunk. This guide shows the from-scratch build, then the pre-wired Flutter Kit version.
To build an AI chat app in Flutter, you stream OpenAI completions over server-sent events (SSE) into a BLoC/Cubit-managed message list, rendering each token as it arrives. You can wire this from scratch — a Flask proxy that hides your API key, a Dart SSE parser, and incremental UI rebuilds — or skip the plumbing with The Flutter Kit, a $69 one-time boilerplate where streaming OpenAI chat, a secure proxy, DALL·E, and GPT-4 Vision ship pre-built with full source ownership.
Why "from scratch" is mostly invisible plumbing
A streaming chat UI looks like a weekend project — bubbles, a text field, a send button. The deceptive part is everything you can't see. You can't ship your OpenAI key in the bundle, so you need a proxy backend before a single token streams. You can't await the full response, so you need a Dart SSE parser that handles partial frames, keep-alive lines, and the `[DONE]` sentinel. And you can't call setState per token on a long thread, so you need BLoC state that repaints only the in-flight bubble. None of this is glamorous, but it's where most from-scratch builds lose a week — the parser eats half a JSON frame, the list jank-rebuilds at 60 deltas a second, or the key ends up in version control. The Flutter Kit's value isn't the chat screen; it's that this exact plumbing is already written, debugged on real devices with Impeller, and feature-flagged so you can ship AI later without ripping it out.
- The proxy must exist before any token streams — no client-safe OpenAI key
- The SSE parser is the most common silent bug in DIY builds
- Incremental rendering (BlocSelector/buildWhen) prevents long-thread jank
- Persistence, abort, and 429 retries are easy to forget until production
When building from scratch is the right call
Skip the boilerplate if your chat is the product's core differentiator and you need a non-standard transport — function calling with tool loops, a self-hosted model, or a custom streaming protocol your backend already speaks. Writing the SSE layer yourself also teaches you exactly where latency and token-buffering bugs live, which is worth it if you'll maintain this for years. And if you're on a stack The Flutter Kit doesn't assume — say you've standardized on Riverpod instead of BLoC, or you're not using Firebase at all — you may spend more time adapting the kit than writing the 200 lines of streaming code. The Flutter Kit uses BLoC + get_it and Firebase by default (Supabase is swappable since you own the source), so an opinionated existing codebase can make a clean from-scratch module the faster path. For everyone else — indie makers shipping an AI app this month — the pre-wired version saves the week the plumbing would have cost.
Streaming OpenAI chat in Flutter: from scratch vs pre-wired
The hard part of an AI chat app isn't the chat bubbles — it's streaming tokens without leaking your API key or jank-rebuilding the whole list on every chunk. Below is the from-scratch path; the pre-wired Flutter Kit version collapses steps 1-5 into one configured module you own the source to.
- 1
Never ship your OpenAI key — put a proxy in front
OpenAI has no client-safe key. Anything in the app bundle can be extracted, so route requests through a tiny Flask proxy that holds the secret server-side and forwards the SSE stream. The Flutter Kit ships exactly this proxy; from scratch you write it yourself.
pip install flask openai gunicorn # proxy exposes POST /chat -> forwards to OpenAI with stream=true gunicorn -w 2 -b 0.0.0.0:8080 app:app - 2
Open a streaming HTTP request from Dart
Use http.Client().send() with a streamed Request so you receive bytes as they arrive instead of awaiting the full body. Point it at your proxy, not api.openai.com.
final req = http.Request('POST', Uri.parse('$proxyUrl/chat')) ..headers['Content-Type'] = 'application/json' ..body = jsonEncode({'messages': history, 'stream': true}); final res = await http.Client().send(req); - 3
Parse the SSE 'data:' frames into tokens
OpenAI streams lines like `data: {"choices":[{"delta":{"content":"He"}}]}` ending with `data: [DONE]`. Split the byte stream on newlines, strip the `data: ` prefix, ignore keep-alives, and decode each delta. This parser is the piece most from-scratch builds get subtly wrong.
await for (final line in res.stream.transform(utf8.decoder).transform(const LineSplitter())) { if (!line.startsWith('data: ')) continue; final payload = line.substring(6); if (payload == '[DONE]') break; final delta = jsonDecode(payload)['choices'][0]['delta']['content']; if (delta != null) yield delta as String; } - 4
Manage chat state with a Cubit, not setState
Emit an incremental state per token. Append the delta to the in-flight assistant message and emit; keep the user/assistant history immutable so retries and context windows stay clean. With flutter_bloc this is a single ChatCubit + get_it registration.
stream.listen((delta) { _buffer += delta; emit(state.copyWith(streaming: _buffer)); }, onDone: () => emit(state.commit(_buffer))); - 5
Render tokens without rebuilding the whole list
Use a BlocSelector (or buildWhen) so only the streaming bubble repaints as tokens land — not the entire ListView. Auto-scroll on new content and render Markdown for code blocks. This is where naive builds jank on long conversations.
- 6
Add persistence, retries, and abort
Persist threads to Firestore so chats survive app restarts, debounce a Firestore write on stream completion, and expose a cancel that closes the HTTP client mid-stream. Handle 429/timeout with a retry on the same message history.
- 7
Or: skip the plumbing with The Flutter Kit
Steps 1-6 — the Flask proxy, SSE parser, ChatCubit, incremental rendering, and Firestore persistence — ship pre-wired and feature-flagged in The Flutter Kit. You add your OpenAI key to the proxy, flip the AI module on, and own every line of Dart. DALL·E image gen and GPT-4 Vision use the same secured backend.
Frequently Asked Questions
What does streaming actually add over a normal OpenAI request in Flutter?
Why do I need a Flask proxy instead of calling OpenAI directly from Dart?
How do I parse OpenAI's SSE stream correctly in Dart?
Will streaming tokens make my chat list rebuild and jank?
Can I add DALL·E image generation and GPT-4 Vision to the same app?
Is it faster to build this from scratch or start from The Flutter Kit?
Keep exploring
Ship the AI chat, skip the plumbing
The streaming OpenAI proxy, SSE parser, ChatCubit, DALL·E, and GPT-4 Vision are pre-wired in The Flutter Kit — $69 one-time, unlimited projects, full source ownership. Build from scratch if your transport is exotic; otherwise start shipping today.
Get The Flutter Kit — $69One-time purchase · Lifetime updates · Unlimited projects