Skip to content

Repository files navigation

Flow UI logo

flow_ui

pub packagepub pointsgithub starslicense: MIT

📚 Documentation · 🧩 Playground · 🤓 API Reference · 🗺️ Roadmap

Flow UI is an open-source Flutter UI library to build production-grade Chat & AI assistant interfaces.

The flow_ui chat surface

Important

flow_ui is pre-1.0. The API is still settling, and minor releases may carry breaking changes — pin a minor version and read the changelog when upgrading.

What's in the box

ComponentWhat it does
FlowChatViewThe full chat surface: bounded thread over a composer, centred at a readable width, with a zero state (greeting, lifted composer, starters) and a jump-to-latest button
FlowThreadScrollable conversation — reads from the top, anchoring to the newest message once it outgrows the viewport
FlowMessageOne turn — ink-wash user bubble, plain assistant, error bubble, typed content parts
FlowStreamingTextAnimated text reveal while a reply arrives
FlowThinkingIndicatorTurning, breathing asterisk with a shimmering label
FlowShimmerTextSweeping text highlight, static once settled
FlowCodeBlockFenced code with built-in synchronous highlighting, a header label, and a copy affordance — languages host-extensible
FlowMarkdownAssistant prose typeset from a built-in parser — headings, emphasis, lists, quotes, tables, links, and fences composing the code block; assistant turns render it by default and it streams gracefully
FlowErrorStateFailure card with a host-written message and retry pill — failed turns render it automatically
FlowMessageActionsCopy / regenerate / edit / feedback row under a message
FlowComposerMultiline input with send/stop, attachments strip, and leading/trailing action slots
FlowMenuIcon-triggered menu with groups, submenus, and toggles — anchored card on desktop, bottom sheet on phones
FlowModelSelectorModel picker with effort and overflow submenus, sheet on phones
FlowPillRemovable pill for an enabled tool or mode in the composer's action row — label auto-drops on phones
FlowAttachmentGroupImage and file tiles with a type pill
FlowAttachmentPreviewFull-screen image viewer with zoom and paging
FlowSuggestion / FlowSuggestionGroupPrompt starters — plain or outlined; scroll, wrap, or column layouts
FlowGreetingZero-state headline
FlowThemeDesign tokens (colors and typography) as a ThemeExtension, with light and dark presets

Getting started

dependencies:
flow_ui: ^0.2.0

Install the theme once (optional — without it, components fall back to a preset matching the ambient brightness):

MaterialApp(
theme:ThemeData(extensions: [FlowTheme.light()]),
darkTheme:ThemeData(
brightness:Brightness.dark,
extensions: [FlowTheme.dark()],
),
)

The default typography — Google Sans and Google Sans Code — arrives through google_fonts: each cut is fetched on first use and cached on the device, so there is no font to bundle. The fetch needs network access: android.permission.INTERNET in an Android app's main manifest (Flutter's template grants it only to debug and profile) and the com.apple.security.network.client entitlement in a sandboxed macOS app; without it text falls back to the platform face. To render offline on first launch, ship the files under a google_fonts/ asset folder.

Build a chat screen

Messages are pure view models. Your app maps its own transport into FlowMessageData, and streaming is data, not streams: while a reply arrives, rebuild with copyWith carrying the grown text.

classChatPageextendsStatefulWidget {
constChatPage({super.key});
@overrideState<ChatPage> createState() =>_ChatPageState();
}
class_ChatPageStateextendsState<ChatPage> {
finalScrollController _scroll =ScrollController();
List<FlowMessageData> _messages =const [];
bool _generating =false;
void_send(String text) async {
final id =DateTime.now().microsecondsSinceEpoch.toString();
setState(() {
_messages = [
..._messages,
FlowMessageData.text(id: id, role:FlowMessageRole.user, text: text),
// An empty pending reply renders the thinking indicator.FlowMessageData(
id:'$id-reply',
role:FlowMessageRole.assistant,
status:FlowMessageStatus.pending,
),
];
_generating =true;
});
// Feed chunks from your backend as they arrive.var streamed ='';
awaitfor (final chunk in myBackend.reply(text)) {
streamed += chunk;
setState(() {
_messages = [
..._messages.sublist(0, _messages.length -1),
_messages.last.copyWith(
parts: [FlowTextPart(streamed)],
status:FlowMessageStatus.streaming,
),
];
});
}
setState(() {
_messages = [
..._messages.sublist(0, _messages.length -1),
_messages.last.copyWith(status:FlowMessageStatus.complete),
];
_generating =false;
});
}
@overrideWidgetbuild(BuildContext context) {
returnScaffold(
body:FlowChatView(
empty: _messages.isEmpty,
greeting:constFlowGreeting(
icon:Icons.wb_twilight,
text:'Good afternoon',
),
suggestions:FlowSuggestionGroup(
layout:FlowSuggestionLayout.column,
suggestions: [
FlowSuggestion(
label:'Write an essay about life and enjoyment',
icon:Icons.edit_note,
onTap: () =>_send('Write an essay about life and enjoyment'),
),
FlowSuggestion(
label:'Create a Monday briefing from my tasks',
icon:Icons.event_available,
onTap: () =>_send('Create a Monday briefing from my tasks'),
),
],
),
thread:FlowThread(
messages: _messages,
controller: _scroll,
thinkingLabel:'Thinking…',
),
threadController: _scroll,
jumpToLatestTooltip:'Jump to latest',
composer:FlowComposer(
placeholder:'How can I help you today?',
isStreaming: _generating,
onSend: _send,
onStop: myBackend.stop,
),
),
);
}
}

FlowChatView is body-only — it builds no Scaffold and no app bar, so your app keeps the chrome, the background, and the keyboard inset. See example/lib/main.dart for a complete runnable version of this page, and the live playground for a demo of every component with variants and code snippets.

Composer accessories

The composer takes leading and trailing action slots. Drop in a FlowMenu (attachments, toggles) and a FlowModelSelector — both render an anchored card on wide layouts and a bottom sheet on phones:

FlowComposer(
onSend: _send,
leadingActions: [
FlowMenu(
icon:Icons.add,
tooltip:'Add to chat',
entries:const [
FlowMenuOption(id:'files', icon:Icons.attach_file, label:'Add files'),
FlowMenuDivider(),
FlowMenuOption(id:'web', icon:Icons.public, label:'Web search', selected:true),
],
onSelected: _handleMenu,
),
],
trailingActions: [
FlowModelSelector(
models:const [
FlowModelOption(id:'fast', label:'Fast', description:'Quick answers'),
FlowModelOption(id:'smart', label:'Smart', description:'Hard problems'),
],
selectedId: _modelId,
onSelected: (id) =>setState(() => _modelId = id),
),
],
)

Message content is typed parts

A message holds an ordered list of sealed FlowMessageParts — FlowTextPart, FlowAttachmentPart, and FlowCustomPart for anything the package doesn't know about. Custom parts render through a builder you supply, so hosts can inject arbitrary widgets (tool cards, citations, charts) without forking the message renderer:

FlowThread(
messages: _messages,
customPartBuilder: (context, message, part) {
returnswitch (part.type) {
'order-card'=>OrderCard(order: part.data asOrder),
_ =>null, // unknown parts are skipped
};
},
)

Attachments carry an ImageProvider, so network, file, memory, and asset images all work — the package never loads anything itself:

FlowMessageData(
id:'m1',
role:FlowMessageRole.user,
parts: [
FlowAttachmentPart([
FlowAttachment(id:'a1', thumbnail:NetworkImage(url), kind:'JPG', label:'sunset.jpg'),
]),
FlowTextPart('What do you think of this shot?'),
],
)

Theming

FlowTheme carries two token sets — colors and typography. Role names follow Material 3's ColorScheme, so an existing scheme maps across, with one addition: the design draws content at three ink levels (onSurface, onSurfaceVariant, onSurfaceMuted) where M3 names two. Start from a preset and override what your brand needs:

FlowTheme(
colors:FlowColors.dark.copyWith(primary:constColor(0xFF6C5CE7)),
typography:FlowTypography.standard,
)

Spacing and corner radii are deliberately not tokens. Following Material's structure, each component bakes its own metrics from the Flow UI design file and exposes per-widget overrides (padding:, borderRadius:) where hosts retheme. Strings shown to the user (tooltips, placeholders, labels) are host-supplied, so localization stays in your app — the one exception, the model selector's effortLabel and moreModelsLabel English defaults, is overridable the same way.

Docs & playground

Full documentation lives at flowui.stac.dev, and every component has a stage in the live playground — variant pills and code snippets included. The playground is also in the repo to run locally:

cd playground && flutter run -d chrome

License

Code is released under the MIT License. Google Sans and Google Sans Code are fetched from Google Fonts under the SIL Open Font License, not bundled.

About

Flutter UI library for AI Chat Interfaces

Topics

Resources

Stars

35 stars

Watchers

3 watching

Forks

Releases

Packages

Contributors

Languages