Flutter SDK

Drop-in comments UI for Flutter — powered by CommentsKit and the ready-to-use CommentsList widget. Handles fetching, posting, threading, reactions, moderation, and pagination out of the box.

Prerequisites

Installation

Add the package to your pubspec.yaml:

dependencies:
  gvl_comments: ^0.9.7

Then run:

flutter pub get

Quick Start

Initialize the SDK once at startup, then drop CommentsList anywhere in your widget tree.

import 'package:flutter/material.dart';
import 'package:gvl_comments/gvl_comments.dart';
import 'package:gvl_comments/l10n/gvl_comments_l10n.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await CommentsKit.initialize(
    installKey: const String.fromEnvironment('GVL_INSTALL_KEY'),
  );

  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // Required for built-in localized strings
      localizationsDelegates: GvlCommentsL10n.localizationsDelegates,
      supportedLocales: GvlCommentsL10n.supportedLocales,
      home: const CommentsPage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Comments')),
      body: CommentsList(
        threadKey: 'post:550e8400-e29b-41d4-a716-446655440000',
        user: UserProfile(
          id: 'user-123',
          name: 'Ada Lovelace',
          avatarUrl: 'https://example.com/avatar.png',
        ),
      ),
    );
  }
}

Pass the install key at build time with --dart-define=GVL_INSTALL_KEY=cmt_live_xxx, or hardcode it for quick testing.

Thread Key

The threadKey is a string that identifies a comment thread. It must be at least 20 characters, high-entropy, and only use a-zA-Z0-9:_-.

Valid

post:550e8400-e29b-41d4-a716-446655440000
article:01BX5ZZKBK6T0JR2GVCCCCCCCC

Invalid

post-123(too short, guessable)
article-42(sequential)

CommentsList Widget

The main widget. All parameters except threadKey and user are optional.

CommentsList(
  // Required
  threadKey: 'post:550e8400-e29b-...',
  user: UserProfile(id: 'user-123', name: 'Ada'),

  // Layout
  newestAtBottom: false,       // false = feed, true = chat
  limit: 30,                   // comments per page (1-100)
  padding: EdgeInsets.all(16),
  scrollController: myController,

  // Features
  reactionsEnabled: true,      // show reaction bar

  // Theming
  theme: GvlCommentsThemeData.bubble(context),

  // Builders (all optional)
  commentItemBuilder: (context, comment, meta) => ...,
  avatarBuilder: (context, comment, size) => ...,
  sendButtonBuilder: (context, onPressed, isSending) => ...,
  composerBuilder: (context, {controller, onSubmit, ...}) => ...,
  separatorBuilder: (context) => ...,
)

CommentCount Widget

Displays the number of approved comments in a thread. Useful for list views, cards, or anywhere you want to show a count without loading the full comment list.

CommentCount(
  threadKey: 'post:550e8400-e29b-...',
  user: UserProfile(id: 'user-123'),

  // Custom builder (optional)
  builder: (context, count) => Text('$count comments'),

  // Default: displays "N comments" with a message icon
)

Batch prefetch for lists

When showing counts in a ListView, prefetch them in one call to avoid N+1 requests:

// Prefetch counts for all visible items
await CommentsKit.I().prefetchCounts(
  threadKeys: items.map((i) => i.threadKey).toList(),
  user: user,
);

// CommentCount widgets will use the cached value
CommentCount(
  threadKey: item.threadKey,
  user: user,
)

TopComment Widget

Displays the single most-engaged comment in a thread (highest reaction count). Useful for previews or summaries.

TopComment(
  threadKey: 'post:550e8400-e29b-...',
  user: UserProfile(id: 'user-123'),

  // Optional
  reactionsEnabled: true,
  showTimestamp: true,
  onTap: () => navigateToThread(),

  // Custom builders
  builder: (context, comment, refresh) => ...,
  loadingBuilder: (context) => ...,
  emptyBuilder: (context) => ...,
  errorBuilder: (context, error, retry) => ...,
)

Theming

Pass a GvlCommentsThemeData to any widget, or wrap your tree with GvlCommentsTheme for global theming.

Built-in presets

.defaults(ctx)

Adapts to your app theme

.neutral(ctx)

Minimal, clean

.compact(ctx)

Dense / dashboards

.card(ctx)

Elevated cards

.bubble(ctx)

Chat-style bubbles

Usage: GvlCommentsThemeData.bubble(context)

Custom theme

CommentsList(
  threadKey: '...',
  user: user,
  theme: GvlCommentsThemeData(
    bubbleColor: Colors.blue.shade900,
    bubbleAltColor: Colors.grey.shade900,
    avatarSize: 32,
    showAvatars: true,
    spacing: 10,
    bubbleRadius: BorderRadius.circular(16),
    authorStyle: TextStyle(fontWeight: FontWeight.bold),
    bodyStyle: TextStyle(fontSize: 14),
  ),
)

Reactions

Six reactions are available out of the box. Users long-press a comment to open the reaction picker.

likelovelaughwowsadangry

Disable with reactionsEnabled: false on CommentsList or TopComment.

User Identity

Pass a UserProfile to each widget. If the user changes (login, logout, account switch), invalidate the cached token and call identify().

// Switch user
CommentsKit.I().invalidateToken();

final newUser = UserProfile(
  id: 'user-456',
  name: 'Bob',
  avatarUrl: 'https://example.com/bob.png',
);

await CommentsKit.I().identify(newUser);

CommentsKit.I() and CommentsKit.instance are equivalent.

Programmatic API

Use CommentsKit directly if you need full control beyond the drop-in widgets.

final kit = CommentsKit.I();
final user = UserProfile(id: 'user-123');

// List comments (paginated)
final comments = await kit.listByThreadKey(
  'post:550e8400-e29b-...',
  user: user,
  limit: 30,
);

// Load next page
if (kit.lastHasMore) {
  final next = await kit.listByThreadKey(
    'post:550e8400-e29b-...',
    user: user,
    cursor: kit.lastNextCursor,
  );
}

// Post a comment
final created = await kit.post(
  threadKey: 'post:550e8400-e29b-...',
  body: 'Great article!',
  user: user,
);

// Post a reply
final reply = await kit.post(
  threadKey: 'post:550e8400-e29b-...',
  body: 'Thanks!',
  user: user,
  parentId: created.id,
);

// Get top comment
final top = await kit.topComment(
  'post:550e8400-e29b-...',
  user: user,
);

// React to a comment
await kit.setCommentReaction(
  commentId: created.id,
  user: user,
  reaction: 'like',  // or null to remove
);

// Report a comment
await kit.report(
  commentId: created.id,
  user: user,
  reason: 'Spam',
);

Webhooks

Receive real-time notifications on your server when comment events happen. Configure webhook endpoints from the dashboard under your project settings.

Setup

  1. Go to Dashboard → Projects → your project
  2. Click Add webhook and enter your HTTPS endpoint URL
  3. Select which events to subscribe to
  4. Copy the signing secret (shown once)

You can configure up to 5 webhooks per project.

Events

comment.createdA new comment is posted (root or reply)
comment.repliedA reply is posted to an existing comment (parent_id is set)
comment.likedA user reacts to a comment
comment.mentionedA user is mentioned via a depth-2 flattened reply

Payload format

All events are sent as a POST request with a JSON body and signed headers:

// Headers
X-Webhook-Event: comment.created
X-Webhook-Timestamp: 1774294700
X-Webhook-Signature: sha256=<hex>

// Body (comment.created / comment.replied / comment.mentioned)
{
  "event": "comment.created",
  "timestamp": "1774294700",
  "data": {
    "id": "uuid",
    "thread_id": "uuid",
    "thread_key": "post:550e8400-e29b-...",
    "external_user_id": "user-123",
    "author_name": "Ada",
    "body": "Great article!",
    "parent_id": null,
    "parent_external_user_id": null,
    "depth": 0,
    "status": "approved",
    "metadata": {},
    "created_at": "2026-03-23T19:38:20Z"
  }
}

// Body (comment.liked)
{
  "event": "comment.liked",
  "timestamp": "1774294700",
  "data": {
    "comment_id": "uuid",
    "reaction": "like",
    "reactor": {
      "external_user_id": "user-456",
      "display_name": "Bob"
    },
    "comment": {
      "thread_key": "post:550e8400-e29b-...",
      "external_user_id": "user-123",
      "author_name": "Ada",
      "body": "Great article!",
      "parent_id": null,
      "depth": 0
    }
  }
}

Verifying signatures

To verify that a webhook comes from GoodVibesLab, compute the HMAC-SHA256 of the payload and compare it to the signature header:

// The signed payload is: "${timestamp}.${body}"
// where timestamp = X-Webhook-Timestamp header
// and body = raw request body string

import crypto from 'crypto';

function verifyWebhook(secret, timestamp, body, signature) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(timestamp + '.' + body)
    .digest('hex');

  return signature === 'sha256=' + expected;
}

Notification mapping

Use the webhook data to route notifications to the right users:

EventWho to notifyField to use
comment.createdContent ownerdata.thread_key
comment.repliedParent comment authordata.parent_external_user_id
comment.mentionedMentioned userdata.metadata.reply_to_user_id
comment.likedComment authordata.comment.external_user_id

Moderation

Every comment has a status: pending, approved, or rejected. The SDK automatically renders placeholders for reported and moderated comments. Paid plans additionally unlock AI-powered automatic moderation.

Users can report comments via long-press. Configure moderation thresholds and AI sensitivity from the dashboard.

Localization

The SDK ships with English strings. Register the delegates in your MaterialApp to enable built-in text (timestamps, hints, error messages).

import 'package:gvl_comments/l10n/gvl_comments_l10n.dart';

MaterialApp(
  localizationsDelegates: GvlCommentsL10n.localizationsDelegates,
  supportedLocales: GvlCommentsL10n.supportedLocales,
  // ...
)

Logging

Enable verbose logging during development. Defaults to error in release and debug in debug mode.

await CommentsKit.initialize(
  installKey: 'cmt_live_xxx',
  logLevel: CommentsLogLevel.trace,
);

Levels: offerrorinfo debugtrace

App Binding (Security)

On native platforms, the SDK automatically collects your app's signature (Android SHA-256 / iOS Team ID) and sends it during token acquisition. Configure allowed bindings in the dashboard to prevent unauthorized use of your install key.