spindle_chat 0.1.0
spindle_chat: ^0.1.0 copied to clipboard
Composable, themeable chat UI for Flutter. Sealed message types, controller-driven state, InheritedWidget DI, animated list with date separators — no codegen, no heavy deps.
spindle_chat #
Composable, themeable chat UI for Flutter.
Features #
- Sealed message types —
TextMessage,ImageMessage,FileMessage,SystemMessage,CustomMessagewith exhaustiveswitch. - Controller-driven —
ChatControllerinterface +InMemoryChatControllerwith animated list operations (insert, remove, update, set). - Theming —
ChatTheme.light(),ChatTheme.dark(), orChatTheme.fromThemeData(theme)to match your app. - Custom builders — Override any widget via
ChatBuilders(message bubble, avatar, input, system message, date separator, empty state). - Localization —
ChatL10nfor translatable strings (input hint, empty state, date labels). - Animated list —
SliverAnimatedListwith fade/slide transitions, date separators, and a scroll-to-bottom button. - Accessibility —
Semanticson bubbles, buttons, and system messages. - Message groups — Consecutive messages from the same author are grouped visually.
- Delivery status —
sent,delivered,seen,errorindicators on outgoing messages. - Edited indicator — Shows "edited" label when
updatedAt != createdAt. - Pagination —
onLoadMorecallback for lazy loading. - Attachments —
onAttachmentTapcallback for photo/file picker integration. - No codegen, no heavy deps — Only
metaandintl.
Installation #
dependencies:
spindle_chat: ^0.1.0
flutter pub get
Quick start #
import 'package:spindle_chat/spindle_chat.dart';
// 1. Create a controller
final controller = InMemoryChatController();
// 2. Add the ChatView
ChatView(
controller: controller,
currentUserId: 'user-123',
resolveAuthor: (id) => ChatAuthor(
id: id,
displayName: 'User',
),
onSend: (text) {
controller.insertMessage(
TextMessage(
id: const Uuid().v4(),
authorId: 'user-123',
createdAt: DateTime.now(),
text: text,
),
);
},
);
That's it. ChatView renders the message list, date separators, and the input bar.
Architecture #
┌──────────────────────────────────────────┐
│ ChatView │
│ ┌─────────────────┐ ┌───────────────┐ │
│ │ ChatMessageList │ │ ChatComposer │ │
│ │ (SliverAnimated) │ │ (TextField) │ │
│ └────────┬─────────┘ └──────┬────────┘ │
│ │ │ │
│ ChatScope (InheritedWidget – DI) │
│ ┌────────┴──────────────────┴────────┐ │
│ │ controller · theme · builders │ │
│ │ resolveAuthor · l10n · callbacks │ │
│ └────────────────────────────────────┘ │
└──────────────────────────────────────────┘
│
ChatController (interface)
│
InMemoryChatController
├─ messages: List<ChatMessage>
├─ insertMessage()
├─ removeMessage()
├─ updateMessage()
└─ setMessages()
Message types #
All message types are subtypes of the sealed class ChatMessage:
| Type | Key fields |
|---|---|
TextMessage |
text |
ImageMessage |
imageUrl, width, height |
FileMessage |
fileName, fileUrl, mimeType, fileSizeBytes |
SystemMessage |
text |
CustomMessage |
metadata |
Exhaustive matching with Dart 3 switch:
switch (message) {
case TextMessage(:final text) => Text(text),
case ImageMessage(:final imageUrl) => Image.network(imageUrl),
case FileMessage(:final fileName) => Text(fileName),
case SystemMessage(:final text) => Text(text, style: italicStyle),
case CustomMessage(:final metadata) => CustomWidget(metadata),
}
Theming #
From your app theme #
ChatView(
theme: ChatTheme.fromThemeData(Theme.of(context)),
// ...
)
Custom colors #
ChatView(
theme: ChatTheme(
colors: ChatColors(
sentBubble: Colors.indigo,
sentText: Colors.white,
receivedBubble: Colors.grey.shade200,
receivedText: Colors.black87,
surface: Colors.white,
onSurface: Colors.black,
inputBackground: Colors.grey.shade100,
inputText: Colors.black87,
timestamp: Colors.grey,
system: Colors.grey,
divider: Colors.grey.shade300,
seen: Colors.blue,
),
typography: ChatTypography.standard(),
),
// ...
)
Custom builders #
Override any widget by passing a ChatBuilders:
ChatView(
builders: ChatBuilders(
textMessageBuilder: (context, message, isMine) =>
MyCustomBubble(message),
avatarBuilder: (context, author) =>
CircleAvatar(child: Text(author.displayName[0])),
dateSeparatorBuilder: (context, date) =>
Center(child: Chip(label: Text(formatDate(date)))),
emptyStateBuilder: (context) =>
const Center(child: Text('Start chatting!')),
systemMessageBuilder: (context, message) =>
Text(message.text, style: TextStyle(color: Colors.grey)),
),
// ...
)
Localization #
ChatView(
l10n: const ChatL10n(
inputHint: 'Сообщение',
emptyStateText: 'Нет сообщений',
today: 'Сегодня',
yesterday: 'Вчера',
),
// ...
)
Message lifecycle #
// Insert (triggers slide-in animation)
controller.insertMessage(message);
// Update (e.g. delivery status)
controller.updateMessage(
message.copyWith(status: MessageGroupStatus.delivered),
);
// Remove (triggers fade-out animation)
controller.removeMessage(message.id);
// Replace all (e.g. initial history load)
controller.setMessages(historyList);
Delivery status #
Each outgoing message can carry a MessageGroupStatus:
| Status | Icon |
|---|---|
sending |
(none) |
sent |
✓ |
delivered |
✓✓ |
seen |
✓✓ (colored) |
error |
✗ |
Callbacks #
| Callback | Description |
|---|---|
onSend |
User submits text from the composer |
onAttachmentTap |
User taps the attachment button ("+") |
onMessageTap |
Tap on a message bubble |
onMessageLongPress |
Long-press on a message bubble |
onLoadMore |
Scrolled to top — load earlier messages |
API reference #
Models #
ChatMessage— Sealed base classChatAuthor— Author withid,displayName,avatarUrlChatController/InMemoryChatController— State managementChatOperation— Insert / Remove / Update / SetMessageGroupStatus— Delivery status enum
Theme #
ChatTheme— Colors + typography + input decorationChatColors— 12 configurable color slotsChatTypography— 4 text styles
Widgets #
ChatView— Main entry point (list + composer)ChatMessageList— Animated scrollable listChatComposer— Text input barChatMessageBubble— Individual message renderingChatScope— InheritedWidget providing config
Config #
ChatBuilders— Widget overridesChatL10n— Localization strings
License #
MIT — see LICENSE for details.
