flutter_chats_ui

Production-grade Flutter chat UI framework β€” plugin-based message types, command-pattern undo/redo, transport-agnostic, optimistic UI, offline outbox, threads, reactions, voice notes, polls, and more.

pub version Flutter Dart SDK License



6

πŸ’» Desktop Demo (Windows)

You can try a ready-to-run Windows demo of the chat UI:

πŸ‘‰ Download Demo (.exe)
https://github.com/Brah-Timo/flutter_chats-ui/releases/tag/untagged-eefa7c5889d1fa5ca488

File: chat_demo.exe

How to run

  1. Download the .exe file from the link above
  2. Double-click to launch
  3. No installation required

Notes

  • Built with Flutter Windows
  • Uses in-memory transport for demo purposes
  • No backend required

Features

Feature Details
Clean Architecture Domain β†’ Engine β†’ BLoC β†’ UI β€” no layer leaks
MVVM / BLoC ChatBloc + ChatEvent surface; StreamBuilder-friendly ChatState
Command Pattern Undo/redo stacks (max 50), SendMessage, EditMessage, Delete, React, Pin, MarkRead, Reply, Batch
12 Message Plugins Text, Image, Video, Audio, Voice Note, File, Location, Poll, Contact, Code, Sticker, System Event
Transport Adapters In-Memory (tests/demos), WebSocket, REST, Firestore stub
Offline Outbox Queued sends with automatic flush on reconnect
Headless Engine ChatEngine has zero Flutter dependency β€” pure Dart, fully testable
Threads & Replies First-class thread support with reply counts and ThreadPanel
Reactions Per-message emoji reactions with toggle (add/remove) and undo
Full-Text Search SearchEngine β€” case-insensitive, registry-aware fallback text
Theming ChatTheme with light/dark presets, copyWith, and adaptive factory
Accessibility RepaintBoundary on every bubble; SafeArea-aware composer

Getting Started

Installation

Add to your pubspec.yaml:

dependencies:
  flutter_chats_ui: ^1.0.0

Then run:

flutter pub get

Minimal Example

import 'package:flutter_chats_ui/flutter_chats_ui.dart';
import 'package:flutter_chats_ui/plugins.dart';
import 'package:flutter_chats_ui/transports.dart';

final conversation = Conversation(
  id: 'conv-1',
  kind: ConversationKind.direct,
  members: [me, peer],
  messages: [],
);

// FlutterChatsUi is the ready-made StatefulWidget entry point.
FlutterChatsUi(
  initialConversation: conversation,
  currentUserId: me.id,
  transport: MemoryTransport(seed: conversation),
)

Using ChatScreen directly

import 'package:flutter_chats_ui/flutter_chats_ui.dart';
import 'package:flutter_chats_ui/plugins.dart';
import 'package:flutter_chats_ui/transports.dart';

class MyChatPage extends StatefulWidget { /* ... */ }

class _MyChatPageState extends State<MyChatPage> {
  late final ChatBloc _bloc;

  @override
  void initState() {
    super.initState();
    _bloc = ChatBloc(
      initialConversation: widget.conversation,
      transport: MyWebSocketTransport(url: 'wss://chat.example.com'),
      currentUserId: widget.me.id,
    );
  }

  @override
  void dispose() {
    _bloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ChatScreen(
      bloc: _bloc,
      registry: MessageTypeRegistry.defaults(),
      theme: ChatTheme.adaptive(context),
    );
  }
}

Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                          UI Layer                           β”‚
β”‚  ChatScreen  ChatAppBar  ChatMessageList  MessageComposer   β”‚
β”‚  ChatBubble  ThreadPanel  PinnedMessagesPanel  ...          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                            β”‚ ChatEvent / ChatState
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        BLoC Layer                           β”‚
β”‚                ChatBloc   ChatEvent   ChatState             β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚ Commands           β”‚ Engine               β”‚ Transport
β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚CommandMgr   β”‚  β”‚   ChatEngine        β”‚  β”‚TransportAdapter  β”‚
β”‚ undo stack  β”‚  β”‚ MessageGrouper      β”‚  β”‚ Memory           β”‚
β”‚ redo stack  β”‚  β”‚ DateSeparator       β”‚  β”‚ WebSocket        β”‚
β”‚             β”‚  β”‚ SearchEngine        β”‚  β”‚ REST             β”‚
β”‚             β”‚  β”‚ UnreadMarker        β”‚  β”‚ Firestore stub   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                            β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                       Domain Layer                          β”‚
β”‚  ChatUser  Message  Conversation  Reaction  Attachment      β”‚
β”‚  Thread    MessagePayload  MessageTypePlugin  ChatCommand   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Message Plugins

Register built-in plugins or add custom ones:

// Use all 12 built-in plugins
final registry = MessageTypeRegistry.defaults();

// Or start empty and add selectively
final registry = MessageTypeRegistry.empty()
  ..register(TextMessagePlugin())
  ..register(ImageMessagePlugin())
  ..register(MyCustomPlugin());

Custom Plugin

class MyStarredPayload extends MessagePayload {
  final String note;
  const MyStarredPayload(this.note);

  @override String get typeId => 'starred';
  @override Map<String, Object?> toJson() => {'note': note};
}

class MyStarredPlugin extends MessageTypePlugin {
  const MyStarredPlugin();

  @override String get id => 'starred';
  @override String get displayName => 'Starred Note';
  @override IconData get icon => Icons.star_rounded;

  @override
  Widget bubbleBuilder(BuildContext ctx, Message msg, MessageRenderContext rCtx) {
    final p = msg.payload as MyStarredPayload;
    return Text('⭐ ${p.note}');
  }

  @override
  Widget previewBuilder(BuildContext ctx, Message msg) =>
      const Text('Starred note');

  @override
  Map<String, Object?> serialize(MessagePayload p) => (p as MyStarredPayload).toJson();

  @override
  MessagePayload deserialize(Map<String, Object?> json) =>
      MyStarredPayload(json['note'] as String);

  @override
  String fallbackText(Message msg) => '⭐ ${(msg.payload as MyStarredPayload).note}';
}

Transport Adapters

WebSocket

final transport = WebSocketTransport(
  url: 'wss://chat.example.com/ws',
  conversationId: 'conv-1',
  currentUserId: me.id,
);

REST (polling)

final transport = RestTransport(
  baseUrl: 'https://api.example.com',
  conversationId: 'conv-1',
  pollInterval: const Duration(seconds: 5),
);

Firestore (stub)

Wire the FirestoreTransport stub to cloud_firestore in your host app.
The file is not exported by default; copy and adapt lib/src/transport/adapters/firestore_transport.dart.

Custom Transport

class MyTransport implements TransportAdapter {
  @override String get id => 'my_transport';

  @override Future<void> connect() async { /* ... */ }
  @override Future<void> disconnect() async { /* ... */ }
  @override Stream<TransportEvent> get events => _controller.stream;

  @override Future<DeliveryStatus> sendMessage(Message m) async {
    // call your API
    return DeliveryStatus.ok(serverAssignedId);
  }
  // implement editMessage, deleteMessage, fetchHistory, etc.
}

Theming

// Built-in presets
ChatTheme.light   // default
ChatTheme.dark    // dark mode
ChatTheme.adaptive(context)  // follows system brightness

// Custom theme
const myTheme = ChatTheme(
  ownBubbleColor: Color(0xFF6200EE),
  otherBubbleColor: Color(0xFFF3E5F5),
  backgroundColor: Color(0xFFFAFAFA),
  showAvatars: true,
  showSenderNames: false,
);

// Incremental overrides
final myTheme = ChatTheme.light.copyWith(
  ownBubbleColor: Colors.deepPurple,
  maxBubbleWidthFactor: 0.65,
);

Command Pattern (Undo / Redo)

// Dispatch via BLoC (recommended)
bloc.add(const UndoEvent());
bloc.add(const RedoEvent());

// Check availability
if (bloc.canUndo) { /* show undo button */ }
if (bloc.canRedo) { /* show redo button */ }

// Label for undo toast
final label = bloc.undoLabel; // e.g. "Undo send"

// Direct engine access
engine.undo();
engine.redo();

Offline Mode

final bloc = ChatBloc(
  initialConversation: conv,
  transport: transport,
  currentUserId: me.id,
  config: const ChatConfig(offlineMode: true),
);
// Failed sends are enqueued in the Outbox and flushed on reconnect.

File Structure

lib/
β”œβ”€β”€ flutter_chats_ui.dart        ← main public barrel
β”œβ”€β”€ plugins.dart                 ← 12 built-in plugins barrel
β”œβ”€β”€ transports.dart              ← transport adapters barrel
└── src/
    β”œβ”€β”€ bloc/
    β”‚   β”œβ”€β”€ chat_bloc.dart
    β”‚   └── chat_event.dart
    β”œβ”€β”€ cache/
    β”‚   β”œβ”€β”€ cache_adapter.dart
    β”‚   β”œβ”€β”€ memory_cache.dart
    β”‚   └── outbox.dart
    β”œβ”€β”€ core/
    β”‚   β”œβ”€β”€ attachment.dart
    β”‚   β”œβ”€β”€ chat_config.dart
    β”‚   β”œβ”€β”€ chat_exception.dart
    β”‚   β”œβ”€β”€ chat_state.dart
    β”‚   β”œβ”€β”€ chat_user.dart
    β”‚   β”œβ”€β”€ conversation.dart
    β”‚   β”œβ”€β”€ message.dart
    β”‚   β”œβ”€β”€ reaction.dart
    β”‚   β”œβ”€β”€ thread.dart
    β”‚   └── history/
    β”‚       β”œβ”€β”€ command.dart
    β”‚       β”œβ”€β”€ command_manager.dart
    β”‚       └── commands/
    β”‚           β”œβ”€β”€ batch_command.dart
    β”‚           β”œβ”€β”€ delete_message_command.dart
    β”‚           β”œβ”€β”€ edit_message_command.dart
    β”‚           β”œβ”€β”€ mark_read_command.dart
    β”‚           β”œβ”€β”€ pin_command.dart
    β”‚           β”œβ”€β”€ react_command.dart
    β”‚           β”œβ”€β”€ reply_command.dart
    β”‚           └── send_message_command.dart
    β”œβ”€β”€ engine/
    β”‚   β”œβ”€β”€ chat_engine.dart
    β”‚   β”œβ”€β”€ date_separator_inserter.dart
    β”‚   β”œβ”€β”€ message_grouper.dart
    β”‚   β”œβ”€β”€ search_engine.dart
    β”‚   └── unread_marker.dart
    β”œβ”€β”€ integration/
    β”‚   β”œβ”€β”€ debouncer.dart
    β”‚   β”œβ”€β”€ encryption_adapter.dart
    β”‚   β”œβ”€β”€ i18n_adapter.dart
    β”‚   β”œβ”€β”€ media_adapter.dart
    β”‚   └── notification_bridge.dart
    β”œβ”€β”€ messages/
    β”‚   β”œβ”€β”€ message_type_plugin.dart
    β”‚   β”œβ”€β”€ message_type_registry.dart
    β”‚   └── plugins/
    β”‚       β”œβ”€β”€ audio_message_plugin.dart
    β”‚       β”œβ”€β”€ code_plugin.dart
    β”‚       β”œβ”€β”€ contact_plugin.dart
    β”‚       β”œβ”€β”€ file_message_plugin.dart
    β”‚       β”œβ”€β”€ image_message_plugin.dart
    β”‚       β”œβ”€β”€ location_plugin.dart
    β”‚       β”œβ”€β”€ poll_plugin.dart
    β”‚       β”œβ”€β”€ sticker_plugin.dart
    β”‚       β”œβ”€β”€ system_event_plugin.dart
    β”‚       β”œβ”€β”€ text_message_plugin.dart
    β”‚       β”œβ”€β”€ video_message_plugin.dart
    β”‚       └── voice_note_plugin.dart
    β”œβ”€β”€ transport/
    β”‚   β”œβ”€β”€ delivery_status.dart
    β”‚   β”œβ”€β”€ transport_adapter.dart
    β”‚   β”œβ”€β”€ transport_event.dart
    β”‚   β”œβ”€β”€ transport_registry.dart
    β”‚   └── adapters/
    β”‚       β”œβ”€β”€ firestore_transport.dart
    β”‚       β”œβ”€β”€ memory_transport.dart
    β”‚       β”œβ”€β”€ rest_transport.dart
    β”‚       └── websocket_transport.dart
    └── ui/
        β”œβ”€β”€ flutter_chats_ui.dart
        β”œβ”€β”€ theming/
        β”‚   β”œβ”€β”€ chat_colors.dart
        β”‚   β”œβ”€β”€ chat_theme.dart
        β”‚   └── chat_typography.dart
        └── widgets/
            β”œβ”€β”€ chat_app_bar.dart
            β”œβ”€β”€ chat_bubble.dart
            β”œβ”€β”€ chat_message_list.dart
            β”œβ”€β”€ chat_screen.dart
            β”œβ”€β”€ emoji_picker_widget.dart
            β”œβ”€β”€ error_banner_widget.dart
            β”œβ”€β”€ long_press_actions_sheet.dart
            β”œβ”€β”€ mention_overlay.dart
            β”œβ”€β”€ message_composer.dart
            β”œβ”€β”€ pinned_messages_panel.dart
            β”œβ”€β”€ presence_indicator.dart
            β”œβ”€β”€ scroll_to_bottom_fab.dart
            β”œβ”€β”€ search_bar_widget.dart
            β”œβ”€β”€ swipe_to_reply.dart
            β”œβ”€β”€ thread_panel.dart
            β”œβ”€β”€ typing_dots.dart
            β”œβ”€β”€ unread_chip.dart
            └── waveform_widget.dart
test/
β”œβ”€β”€ chat_engine_test.dart
β”œβ”€β”€ command_manager_test.dart
β”œβ”€β”€ message_grouper_test.dart
β”œβ”€β”€ outbox_test.dart
β”œβ”€β”€ search_engine_test.dart
β”œβ”€β”€ transport_adapter_test.dart
β”œβ”€β”€ integration/
β”‚   └── send_receive_cycle_test.dart
└── widget/
    β”œβ”€β”€ chat_bubble_test.dart
    └── chat_screen_test.dart

Requirements

Dependency Version
Dart SDK >=3.3.0 <4.0.0
Flutter >=3.19.0
equatable ^2.0.5
uuid ^4.4.0
collection ^1.18.0
meta ^1.12.0

Contributing

  1. Fork the repository
  2. Create your feature branch: git checkout -b feature/my-feature
  3. Commit your changes: git commit -m 'feat: add my feature'
  4. Push: git push origin feature/my-feature
  5. Open a Pull Request

Please follow the existing code style (Clean Architecture, immutable domain models, const everywhere possible).


License

This project is licensed under the Apache License 2.0 β€” see LICENSE for details.

Β© 2026 timsoft-org

Libraries

flutter_chats_ui
plugins
transports
flutter_chats_ui β€” Transport adapters barrel.