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
- Flutter SDK ≥ 3.10 / Dart ≥ 3.3
- A GoodVibesLab Cloud account
- An install key from the dashboard (starts with
cmt_live_)
Installation
Add the package to your pubspec.yaml:
dependencies:
gvl_comments: ^0.9.7Then run:
flutter pub getQuick 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-446655440000article:01BX5ZZKBK6T0JR2GVCCCCCCCCInvalid
post-123(too short, guessable)article-42(sequential)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.
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
- Go to Dashboard → Projects → your project
- Click Add webhook and enter your HTTPS endpoint URL
- Select which events to subscribe to
- 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 commentcomment.mentionedA user is mentioned via a depth-2 flattened replyPayload 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:
| Event | Who to notify | Field to use |
|---|---|---|
comment.created | Content owner | data.thread_key |
comment.replied | Parent comment author | data.parent_external_user_id |
comment.mentioned | Mentioned user | data.metadata.reply_to_user_id |
comment.liked | Comment author | data.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: off → error → info → debug → trace
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.
CommentsList Widget
The main widget. All parameters except
threadKeyanduserare optional.