The Flutter Kit logoThe Flutter Kit
Guide

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.

Last updated: 2026-06-10 9 min read By Ahmed Gagan, Flutter Engineer
Quick Answer

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.

Hardest part
SSE token parsing + key-safe proxy, not the UI
Stack used
OpenAI streaming, Flask proxy, BLoC/Cubit, Firestore
Pre-wired option
The Flutter Kit — $69 one-time, source included

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. 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. 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. 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. 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. 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. 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. 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?
A non-streaming call awaits the full completion, so the user stares at a spinner for several seconds. Streaming over SSE renders each token the instant it arrives, which is the difference between feeling like ChatGPT and feeling broken. The cost is a real SSE parser and incremental BLoC state — both of which The Flutter Kit ships pre-built.
Why do I need a Flask proxy instead of calling OpenAI directly from Dart?
Because there is no client-safe OpenAI API key. Anything compiled into your iOS, Android, or Web bundle can be extracted and abused on your bill. A thin proxy keeps the secret server-side and just forwards the SSE stream. The Flutter Kit includes this proxy so your key never ships in the app.
How do I parse OpenAI's SSE stream correctly in Dart?
Pipe the byte stream through utf8.decoder then LineSplitter, keep only lines starting with `data: `, strip that prefix, break on `[DONE]`, and JSON-decode each delta's content. The subtle bugs are partial frames across chunk boundaries and treating keep-alive lines as data — which is why the kit's tested parser exists.
Will streaming tokens make my chat list rebuild and jank?
It will if you setState the whole ListView per token. The fix is a BlocSelector or buildWhen that repaints only the streaming bubble while the rest of the thread stays static. On long conversations with Impeller this keeps things smooth; the kit wires this rendering pattern out of the box.
Can I add DALL·E image generation and GPT-4 Vision to the same app?
Yes — both ride the same secured Flask proxy. Once the key-safe backend exists, adding image generation or vision is another endpoint, not another architecture. The Flutter Kit ships DALL·E and GPT-4 Vision alongside the streaming chat module, all behind the same proxy and feature flags.
Is it faster to build this from scratch or start from The Flutter Kit?
From scratch you'll spend roughly a week on the proxy, SSE parser, ChatCubit, incremental rendering, and persistence before polishing anything. The Flutter Kit collapses that into a configured, source-owned module for a $69 one-time price. Build from scratch only if your transport is non-standard or your stack diverges from BLoC + Firebase.

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 — $69

One-time purchase · Lifetime updates · Unlimited projects