kreiseck_chat
Reusable, backend-agnostic chat UI widgets and models for Flutter apps.
This package contains only UI and plain data models — no networking, storage,
or vendor-specific logic. Wire it up to whatever transport (REST, sockets,
a database, ...) your app already uses by feeding it lists of ChatMessage
and ChatConversation and handling the onSend/onOpen callbacks.
Features
-
Models —
ChatMessage,ChatConversation,ChatParticipant,ChatPresence,ChatAttachment,MessageReaction,ChatMessageRef,TypingState: immutable data classes withcopyWithand value equality.ChatMessagecarries an optionalsender(for group threads), typedattachments, areplyToreference,reactions, and delivery/read timestamps — pluschannel,errorText,kind,translation, andheadline(see below).ChatConversationcarrieschannels, a list ofChatChannelfeeding into it. -
Voice messages — an
AttachmentKind.audioattachment renders as aVoiceBubble(play/pause, draggable progress bar, elapsed / total time, speed chip). The package ships no audio plugin:ChatAudioPlaybackControlleris a scaffold you subclass over your own player, and it enforces that only one voice message plays at a time. See "Voice messages" below. -
ChatChannel— a value object describing a channel a message travelled over: an id, display label, icon, and brand color. Ready-made templates —ChatChannel.whatsApp(),.sms(),.email(),.inApp(),.voice()— cover the channels most apps have; override anything viacopyWithto match your own branding. Rendered consistently byChatChannelBadge. -
ChatCapabilities— a single flag set (reactions,replies,media,typingIndicator,presence,readReceipts) that tells the widgets which affordances a channel actually supports, so the UI never offers something the backend can't do. Built-in profiles:ChatCapabilities.twilioSms()(plain text only),ChatCapabilities.twilioWhatsApp()(media, replies, reactions, read receipts, no typing/presence), andChatCapabilities.inApp()(everything on). Use the default constructor orChatCapabilities.custom(...)for anything in between. -
ChatTheme— a layered visual configuration composed ofChatColors,ChatTypography,ChatIcons, andChatMotion, plus aBubbleStyle(whatsApp,iMessage,minimal),bubbleRadius,showBubbleTail, andimageProvider— how a remote image URL becomes anImageProvider, defaulting to the disk-cachingcachedChatImageso photos are fetched once; swap inplainChatImageor your own to change that. Ready-made presets:ChatTheme.light(),ChatTheme.dark(),ChatTheme.whatsApp(), andChatTheme.iMessage()(the latter two take abrightness). Override just the sub-config you need; the pre-0.3 flat color getters (accentColor,incomingBubbleColor, ...) are preserved for existing call sites.ChatColorsnow also carrieserrorandwarning.showBubbleTailis off by default in every preset, includingwhatsAppandiMessage— the classic speech-bubble tail is opt-in, not drawn automatically just because a preset is styled after an app that has one. -
BubbleInk/ChatColors.inkFor— colors that sit on a bubble (message text, muted timestamps/status, quote lines/dividers) are not fixed values:ChatColors.inkFor(bubble)returns aBubbleInkderived from that bubble's own fill color viaBubbleInk.forSurface, so a host picking any customoutgoingBubble/incomingBubblecolor still gets legible text without hand-picking a matching text color. The derived tones follow the polarity of that bubble's own text color (outgoingText/incomingText), so timestamp, quote bar and reply name always match the message text instead of independently chasing raw contrast.lineis guaranteed at least 3:1;textandmutedreach 4.5:1 wherever that polarity allows it, and fall back to the same 3:1 floor where it doesn't — e.g. on the default outgoing blue, white text only reaches 4.07:1, an accepted trade-off for one consistent tone over maximum contrast. SetincomingInk/outgoingInkonChatColorsto override the derivation for a given bubble. The lower-levelreadableOn(surface, desired, {target})andcontrastRatio(a, b)helpers do the same for colors that must keep their own hue (e.g. an error red or the read-tick blue) rather than being replaced outright — used throughout the bubble, channel badge, and banner for exactly that. -
MessageBubble— timestamp, delivery status icon, sender name/avatar in group threads, quoted replies, reaction chips, and media attachments — each gated byChatCapabilities. Also renders a boldheadlineabove the text, aChatChannelBadgewhen the message has achannel, theerrorTextunder a failed bubble with anonRetrycallback that shows a "Erneut senden" action next to it (omit it to show just the failure reason, with no retry action), and — whentranslationis set — a tap-to-toggle between original text and translation (which side it opens on followsChatTranslation.showFirst). PasstextBuilderto draw the body yourself — e.g. linkified text — instead of a plainText, andChatColors.incomingBubbleBorder/outgoingBubbleBorderfor a hairline around bubbles whose fill is close to the background. -
showChatImageViewer— opens images full screen: pinch to zoom, swipe between them, tap to close, counter when there is more than one. Wire it toChatThreadView.onOpenImages;ChatAvatarcalls it by itself when it shows a real picture. -
ChatSystemCard— a centered card for non-message content in the thread: system notes, logged events, summaries. Messages withkind: ChatMessageKind.systemrender as this automatically. -
ChatBanner— a notice bar (ChatBannerVariant.info/.warning) for states affecting the reply: a restricted channel, a disconnected connection, a locked thread. -
Composer— an auto-growing text field with a send button, an attach button gated oncapabilities.media, typing-change callbacks, and an optionalleadingwidget (e.g. a channel picker). -
ChatThreadView— a scrollable message list that groups consecutive messages by sender, inserts date separators, shows aTypingIndicator, and renders an optionalheader. The list is drawn reversed, so the newest message is the anchor: opening lands at the bottom, an opening keyboard or a late-loading image doesn't push it out of view, and a reader who scrolled up keeps their place — no scroll juggling needed on the host side. Consecutive image-only messages are folded into one album (groupMedia, on by default; the merge itself is the publicgroupMediaRuns).customMessageBuilderlets the host render its own widget forkind: ChatMessageKind.custommessages (falling back to the normal bubble if it returnsnull);composerBannerandcomposerLeadingare forwarded to the composer;readOnlyhides the composer entirely for archive or supervisory views. -
ChatHeader— a conversation header with avatar, title, and a presence/typing subtitle. -
ConversationListView— a conversation inbox list with avatars, presence dots, unread badges, and typing previews.onLongPressopens an action sheet from a row;titleBadgeBuilderrenders a small badge before the title (e.g. a spam marker); each row shows a compact strip of channel badges built fromChatConversation.channelsin the subtitle line (channelBadgestill wins when supplied, thenchannels, then the plainchannelLabeltext).timeLabelBuilderputs a time at the end of the row (you format it), andunreadStyle: UnreadStyle.dotmarks unread rows with a dot instead of a count. -
ChatStrings— every word the widgets put on screen (reply/copy/retry, today/yesterday, the composer's hint and attachment labels, the typing indicator, presence/last-seen wording, and the mail widgets' subject/to/ body/spam labels) lives in one immutable class, resolved from the ambientLocale. A host that already sets its app locale needs to pass nothing — but a plainMaterialApp.localeisn't quite enough on its own, sinceTextFields (like the composer's) also needMaterialLocalizations, which Flutter only ships pre-built for English. One thing this doesn't cover:ChatChannel's templates (.whatsApp(),.sms(), ...) carry their ownlabel(e.g..voice()'s default is the German "Anruf") — channel names are data on aconstvalue object rather thanChatStringswording, so translate them by passing your ownlabelper instance.# pubspec.yaml of the app (not this package — it adds none of its own # dependencies), so MaterialLocalizations has real resources to load for # 'uk' instead of just falling through to English. dependencies: flutter_localizations: sdk: flutterimport 'package:flutter_localizations/flutter_localizations.dart'; MaterialApp( locale: const Locale('uk'), supportedLocales: const [Locale('uk'), Locale('en')], localizationsDelegates: GlobalMaterialLocalizations.delegates, home: ChatThreadView(messages: messages, onSend: send), ); // Or override a single word: ChatThreadView( messages: messages, onSend: send, strings: const ChatStrings.de().copyWith(retry: 'Nochmal versuchen'), );Not keen on the extra dependency?
example/lib/main.dartshows the delegate pair this package's own tests use to preview every locale without depending onflutter_localizations— it hands the framework's English-only resources back for any locale, unconditionally.Built-in presets cover English, German, Croatian, Turkish, and Ukrainian (
ChatStrings.en()….uk()); any other locale falls back to English.copyWithalso takes aclearXswitch (e.g.clearAvatarUrl: true) to empty a nullable field on the various model classes across this package — somethingfield ?? this.fieldalone can't express, since there's no way to tell "leave it" apart from "set it back to null".
Installation
dependencies:
kreiseck_chat: ^0.3.0
Usage
import 'package:kreiseck_chat/kreiseck_chat.dart';
ChatThreadView(
messages: messages, // List<ChatMessage>, oldest first
onSend: (text) {
// append a new outgoing ChatMessage to your own state/store
},
theme: ChatTheme.iMessage(brightness: Brightness.dark),
capabilities: const ChatCapabilities.inApp(),
)
ConversationListView(
conversations: conversations, // List<ChatConversation>
onOpen: (conversation) {
// navigate to the thread for this conversation
},
theme: ChatTheme.whatsApp(),
capabilities: const ChatCapabilities.twilioWhatsApp(),
)
Channel-aware threads with a restricted composer, a channel picker, and custom message rendering:
ChatThreadView(
messages: messages,
onSend: send,
capabilities: const ChatCapabilities.twilioSms(),
composerBanner: ChatBanner(
text: 'Über SMS sind keine Bilder möglich.',
variant: ChatBannerVariant.warning,
theme: theme, // ChatThreadView can't hand its theme down to a
// pre-built child widget, so pass the same theme explicitly.
),
composerLeading: myChannelPicker,
customMessageBuilder: (m) => m.kind == ChatMessageKind.custom
? MyCallCard(message: m)
: null,
)
See example/lib/main.dart for a self-contained demo gallery that switches
between bubble presets, capability profiles, and light/dark mode against
seeded data (presence, typing, reactions, a reply, channels, translations,
system/custom messages, and a conversation list with pinned rows and channel
badges).
Voice messages
Audio attachments render as a voice bubble. The package draws it but plays
nothing — subclass ChatAudioPlaybackController over the player of your
choice and hand it to ChatThreadView. The controller already holds the
playback state and guarantees that only one voice message plays at a time.
import 'package:just_audio/just_audio.dart';
import 'package:kreiseck_chat/kreiseck_chat.dart';
class JustAudioChatController extends ChatAudioPlaybackController {
final AudioPlayer _player = AudioPlayer();
JustAudioChatController() {
_player.positionStream.listen(reportPosition);
_player.durationStream.listen((d) {
if (d != null) reportDuration(d);
});
_player.playerStateStream.listen((s) {
if (s.processingState == ProcessingState.completed) reportEnded();
});
}
@override
Future<void> loadSource(String url) => _player.setUrl(url);
@override
Future<void> resume() => _player.play();
@override
Future<void> pause() => _player.pause();
@override
Future<void> seekSource(Duration position) => _player.seek(position);
@override
Future<void> applySpeed(double speed) => _player.setSpeed(speed);
@override
void dispose() {
_player.dispose();
super.dispose();
}
}
ChatThreadView(
messages: messages,
onSend: send,
audioController: controller, // dispose it with your screen
capabilities: const ChatCapabilities.twilioWhatsApp(),
)
The bubble shows --:-- until the player reports a duration; nothing invents
a length. example/ runs this wiring against a seeded voice message.
Additional information
Widgets never hard-code colors; everything is driven through ChatTheme so
apps can support light and dark mode and match a channel's native look
(WhatsApp, iMessage, or a fully custom minimal base) without forking the
widgets. Pair a theme with the ChatCapabilities profile that matches your
transport so the UI only offers what the channel can actually deliver.
License
Apache License 2.0 — see LICENSE. Contributions are accepted under the same terms.
Libraries
- kreiseck_chat
- Reusable, backend-agnostic chat UI widgets and models for Flutter apps.