convokit_flutter_ui 0.4.0 copy "convokit_flutter_ui: ^0.4.0" to clipboard
convokit_flutter_ui: ^0.4.0 copied to clipboard

Extensible, plug-and-play Flutter UI components for ConvoKit conversations and messaging.

ConvoKit Flutter UI #

Plug-and-play, extensible Flutter UI for the convokit_flutter SDK. The package provides a conversation inbox and a complete selected-conversation surface without taking control away from the host app.

UI 0.4.0 requires convokit_flutter 0.4.x and the coordinated backend release. Private room/app topics are discovered and refreshed automatically by the core SDK.

Included #

  • SDK-backed and controlled conversation-list components
  • Offset pagination with automatic scroll-to-load-more
  • Search, archived, participant, predicate, and custom-sort filters
  • SDK-backed and controlled conversation components
  • Older-message pagination and realtime message, typing, and read events
  • Text sending and structured image/file/media rendering
  • Default read indicators calculated from participant read positions
  • Theme tokens plus builders for every major state and component
  • A replaceable client boundary for caching, analytics, offline state, custom authorization, or an alternative state-management layer

The package is deliberately split into two layers:

Layer Use when
ConvoKitConversationList / ConvoKitConversation You want working SDK-backed UI with minimal setup.
ConvoKitConversationListView / ConvoKitConversationView Your application already owns state and operations.

These are real renders of the package widgets using backend-free fixture data. The complete, runnable implementation is in lib/showcase/component_showcase_app.dart, with widget coverage in test/component_showcase_test.dart.

Standard components #

Standard ConvoKit conversation list and chat components

The default conversation rows, header, message bubbles, structured file card, read receipts, and composer. This version also demonstrates onRefresh, onAddAttachment, readAtByUserId, and reverseMessages: true.

Branded customer support #

Branded ConvoKit customer support interface

The same controlled widgets styled as a support workspace. It replaces only selected pieces through itemBuilder, headerBuilder, mediaBlockBuilder, readReceiptBuilder, and composerBuilder.

Compact operations #

Compact ConvoKit operations interface

A dense dashboard treatment using custom padding, separators, message rows, typing indicator, and composer, with reverseMessages: false.

Run the public examples repository:

git clone https://github.com/ConvoKitApp/ConvoKit-Flutter-UI-Examples.git
cd ConvoKit-Flutter-UI-Examples
flutter run -d chrome

On Flutter web, append ?variant=standard, ?variant=branded, or ?variant=compact to open a specific configuration directly.

Install #

Add both the core SDK and UI package:

flutter pub add convokit_flutter convokit_flutter_ui

Configure and connect the core SDK before constructing an SDK-backed UI controller. The app's client secret belongs only on the token server; never put it in Flutter.

ConvoKit.configure(
  clientId: 'public-client-id',
  tokenProvider: (appUserId) => tokenService.issueToken(appUserId),
);
await ConvoKit.connectUser(currentAppUser.id);

The core SDK uses ConvoKit's managed https://api.convokit.app endpoint. Set backendUrl only for local testing or a self-hosted deployment.

SDK-backed conversations render an outgoing message immediately, reconcile it with the server response and realtime echo, and restore an unchanged draft if the send fails.

Plug-and-play UI #

Use the list at the top level, then open a conversation selected by the user:

class Inbox extends StatelessWidget {
  const Inbox({super.key});

  @override
  Widget build(BuildContext context) {
    return ConvoKitConversationList(
      onConversationSelected: (conversation) {
        Navigator.of(context).push(
          MaterialPageRoute<void>(
            builder: (_) => Scaffold(
              body: ConvoKitConversation(
                conversationId: conversation.id,
                onBack: () => Navigator.of(context).pop(),
                onAttachmentTap: (context, message, attachment) {
                  // Open the URL with the host app's browser/download policy.
                },
              ),
            ),
          ),
        );
      },
    );
  }
}

The list requests the next conversation page near its bottom. The conversation requests older message pages near its oldest edge. Controllers de-duplicate records and ignore stale asynchronous results.

Filtering and pagination #

Own a controller when filters need to change after construction:

final conversations = ConvoKitConversationListController(
  pageSize: 25,
  initialFilter: const ConvoKitConversationFilter(archived: false),
);

await conversations.setQuery('design');

await conversations.setFilter(
  ConvoKitConversationFilter(
    participantIds: const {'app_user_42'},
    predicate: (conversation) => conversation.description != null,
    comparator: (a, b) => b.updatedAt.compareTo(a.updatedAt),
  ),
);

archived is sent to the SDK. Text, participants, predicates, and ordering are applied locally. If a local filter produces no result in the current source page, the controller keeps paging until it finds a match or reaches the end.

For server-side search or cursor translation, provide pageLoader:

final conversations = ConvoKitConversationListController(
  pageLoader: (request) async {
    return repository.searchConversations(
      query: request.filter.query,
      limit: request.limit,
      offset: request.offset,
    );
  },
);

UI customization #

The controlled widgets let the host replace only what it needs:

ConvoKitConversationView(
  conversation: state.conversation,
  messages: state.messages,
  currentUserId: state.userId,
  readAtByUserId: state.readAtByUserId,
  typingUserIds: state.typingUserIds,
  onSendMessage: controller.sendText,
  onLoadOlder: controller.loadOlder,
  hasOlderMessages: state.hasOlder,
  headerBuilder: (context, conversation, onBack, onRefresh) {
    return MyConversationHeader(conversation: conversation);
  },
  messageBuilder: (context, message, index, isMine, sender, readers) {
    return MyMessageBubble(message: message, readers: readers);
  },
  mediaBlockBuilder: (context, block, message, isMine) {
    return block['type'] == 'poll' ? MyPoll(block: block) : null;
  },
  composerBuilder: (context, text, isSending, send, addAttachment) {
    return MyComposer(controller: text, onSend: send);
  },
);

Available replacement points include conversation rows, separators, loading, empty and error states, header, message row, individual media blocks, read receipts, typing indicator, composer, attachment taps, user-name resolution, scroll controllers, padding, thresholds, and list direction.

To style the defaults, install the theme extension:

MaterialApp(
  theme: ThemeData(
    extensions: const [
      ConvoKitUiThemeData.light(),
    ],
  ),
  home: const Inbox(),
);

Use copyWith to replace individual color and sizing tokens.

Functional customization #

Implement ConvoKitUiClient and pass it to either controller when the UI should use a repository, cache, offline queue, analytics wrapper, or a custom authorization policy. DefaultConvoKitUiClient delegates directly to the existing static ConvoKit SDK.

Externally supplied controllers remain owned by the host and must be disposed there. Controllers created internally by plug-and-play widgets are disposed by the widgets.

Optimistic outgoing rows display Sending… until the backend confirms them through HTTP, live updates or authorized history. The server timestamp is then rendered in the viewer's local timezone. Custom message builders can use isConvoKitPendingMessage(message) to present the same state.

Read receipts #

ConvoKitConversationController.state.readAtByUserId stores the latest known read position from conversation participants and realtime read events. state.readerIdsFor(message) returns the users whose read timestamp includes that message. The default outgoing bubble displays one check when delivered and two checks plus a reader count after another participant has read it. Replace readReceiptBuilder for avatars, detailed labels, or product-specific rules.

Live inbox updates #

SDK-backed inboxes listen to inboxChanges on the shared private app channel. Room creation, membership changes, metadata changes and deletion/cascades trigger an authorized REST refresh; each verified initial join/rejoin also refreshes the list. Notifications contain no room contents or identifiers. Open room controllers reconcile metadata/history too, so removed access clears their view.

Refresh replaces the loaded window atomically, retaining visible rows during loading, local filters and previously loaded pages. The default backend's stable creation-time/ID order preserves the old loaded boundary when new rooms are inserted. Custom page loaders preserve their loaded page count and receive the current filter on each request. Bursts coalesce; a change arriving during a fetch schedules another pass. Transient errors retain rows; access denial or session replacement clears them.

Reconnect and session recovery #

SDK-backed room controllers refetch persisted receipts and page through the currently viewed history range after either private room channel joins/rejoins. refresh() / reconcile() use the same non-destructive recovery path. Existing messages and pending sends stay visible; state.isReconciling reports progress. Newer live changes win over an older HTTP response, and read positions only advance. A device-clock timestamp is never treated as a confirmed read receipt.

Raw Postgres events omit the related media table. The UI shows text immediately and fetches the complete authorized message through getMessage(id). It retains attachments during provisional edits, and waits for the full response before displaying a media-only message. The server's updatedAt revision prevents old responses from rewinding edits. Lookups are coalesced per ID and limited to eight in flight per controller, including across reloads; this adds REST requests for observed row changes. Retired, deleted and superseded responses are discarded.

The controller creates a UUID clientMessageId before displaying the pending row and forwards it unchanged to the core/backend. A matching canonical message from the same sender and room replaces that row atomically, even before the HTTP acknowledgement. Identical text or attachment counts never identify a send. Media-only echoes retain the pending preview until authorized hydration finishes. If a confirmed send's HTTP response is lost, the composer treats it as success without restoring the draft or resurrecting a subsequently deleted message. Typing-stop remains independent of send completion.

Continued composer input renews typing at most once per half typingTimeout (1.5 seconds with the default 3-second timeout), not on every keystroke. Idle input sends no keepalives; idle timeout or explicit stop clears typing. Disconnect, disposal and session replacement cancel the timers, and failures from older typing requests cannot reset a newer request's state.

Room controllers also consume the core SDK's private ID-only deletion stream. Deleted messages disappear without manufacturing an old message body. Deletion markers last until an explicit initial reload/session reset; delayed send responses, history pages, and older Realtime rows cannot restore those IDs. Realtime does not replay missed events, so reconnect recovery still reconciles edits/deletions within loaded history. The new inbox stream also invalidates cascade changes. If REST recovery fails, the error is exposed and a subsequent refresh or rejoin retries it; an interrupted join is not a successful recovery.

Custom ConvoKitUiClient implementations must provide a stable sessionIdentity for one login (including app identity), return null on logout, and replace it on every new login even when the user ID is unchanged. Close connectionEvents when that session ends. Emit logical messages:<room> / conversation:<room> join statuses; closed as an event may be a recoverable channel replacement, whereas stream completion ends the session. For getMessages(before:), use the cursor's (createdAt, id) values, offset zero, and strict descending order. Implement onMessageDeleted() as Stream<MessageDeletedEvent> containing only id and conversationId; onMessage() now handles inserts/updates only. Implement getMessage(id) with the same app/room authorization as history, returning the complete current message and authoritative related media list. Preserve Message.updatedAt when present; it is distinct from creation time. For 0.4.0, implement inboxChanges with mutation and verified-join signals, forward sendMessage(clientMessageId:), and preserve that ID on HTTP/history/live messages. These additions require the 0.4.0 core/backend contract. No raw-DELETE fallback is used.

Controllers clear cached data after session replacement or history access denial; they never silently bind to another user. Connect the intended user and call loadInitial() again, or construct new controllers. A custom inbox pageLoader used with a connected client shares that client's lifecycle and inbox signals. Offline custom loaders and fully controlled widgets remain owned by their host.

Media #

Default image and file cards tolerate missing URLs, names, and numeric/string file sizes. onAttachmentTap intentionally delegates opening and downloading to the host app, where authentication and platform behavior belong. Unknown structured types receive a safe fallback; return a widget from mediaBlockBuilder to support custom blocks such as polls, locations, contacts, audio, or commerce cards.

Verification #

dart format --output=none --set-exit-if-changed lib test
flutter analyze
flutter test

See the public ConvoKit-Flutter-UI-Examples repository for runnable default, branded, compact, and SDK-backed application configurations.

0
likes
150
points
494
downloads

Documentation

API reference

Publisher

verified publisherconvokit.app

Weekly Downloads

Extensible, plug-and-play Flutter UI components for ConvoKit conversations and messaging.

Topics

#chat #messaging #realtime #ui

License

Apache-2.0 (license)

Dependencies

convokit_flutter, flutter

More

Packages that depend on convokit_flutter_ui