A Dart and Flutter SDK for integrating comment threads, nested replies, voting systems, and content moderation. Features automatic UI state management, network retries, and multi-account session recovery.
Add commentum_client to your pubspec.yaml referencing your Git repository or local package path:
Using Git URL:
dependencies:
commentum_client:
git:
url: https://github.com/commentum/commentum_client.gitUsing Local Submodule Path:
dependencies:
commentum_client:
path: packages/commentum_clientimport 'package:commentum_client/commentum_client.dart';
// 1. Create client with in-memory storage for quick testing
final client = CommentumClient(
config: const CommentumConfig(baseUrl: 'https://api.yourdomain.com/v1'),
storage: InMemoryCommentumStorage(),
preferredProvider: CommentumProvider.anilist,
);
void main() async {
await client.init(); // Hydrate sessions
// 2. Create a discussion controller for a specific media item
final controller = client.getDiscussionController(
mediaId: '10123',
mediaProvider: 'anilist',
);
// 3. Load initial comments
await controller.loadInitial();
// 4. Post a comment (automatically updates controller state)
await controller.postComment('Great episode!');
}Configure the client with your API base URL, automatic retry options, and token storage engine.
final client = CommentumClient(
config: const CommentumConfig(
baseUrl: 'https://api.yourdomain.com/v1',
appClient: 'my_flutter_app',
enableLogging: true, // Prints network logs in debug mode
autoRetry: true, // Retries failed network requests automatically
maxRetries: 2, // Number of retry attempts before throwing error
),
storage: InMemoryCommentumStorage(), // Use persistent storage for production
preferredProvider: CommentumProvider.anilist,
// Optional: Callback triggered when a session expires to fetch a fresh token
onProviderTokenRefreshRequired: (provider) async {
return await myOAuthService.getFreshToken(provider);
},
);
// Call init() once at app startup before rendering UI
await client.init();The CommentumDiscussionController manages loading states, pagination cursors, reply tree insertions, and optimistic vote score updates automatically.
Controller State Properties:
state.comments: List of top-level comments (each containing nestedreplies).state.isLoading:trueduring the initial load.state.isMoreLoading:truewhile paginating more comments.state.hasMore:trueif there are more comments available to load.state.error: Error message string if a request failed.
Example Flutter Widget:
class CommentsSection extends StatefulWidget {
final String mediaId;
const CommentsSection({super.key, required this.mediaId});
@override
State<CommentsSection> createState() => _CommentsSectionState();
}
class _CommentsSectionState extends State<CommentsSection> {
late final CommentumDiscussionController _controller;
@override
void initState() {
super.initState();
_controller = client.getDiscussionController(
mediaId: widget.mediaId,
mediaProvider: 'anilist',
);
_controller.loadInitial(); // Fetch first 20 comments
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return StreamBuilder<CommentumDiscussionState>(
stream: _controller.stream,
initialData: _controller.state,
builder: (context, snapshot) {
final state = snapshot.data!;
if (state.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state.error != null && state.comments.isEmpty) {
return Center(child: Text('Error: ${state.error}'));
}
return ListView.builder(
itemCount: state.comments.length + (state.hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index == state.comments.length) {
return TextButton(
onPressed: () => _controller.loadMore(),
child: const Text('Load More Comments'),
);
}
final comment = state.comments[index];
return ListTile(
title: Text(comment.user?.username ?? 'Anonymous'),
subtitle: Text(comment.content),
trailing: IconButton(
icon: const Icon(Icons.thumb_up),
onPressed: () => _controller.vote(comment.id, 1),
),
);
},
);
},
);
}
}Authenticate users using OAuth access tokens from providers like AniList or MyAnimeList. Multiple accounts can be connected simultaneously.
// Connect an account by passing the platform's OAuth access token
await client.auth.login(CommentumProvider.anilist, 'user_anilist_oauth_token');
// Connect a second account
await client.auth.login(CommentumProvider.myanimelist, 'user_mal_oauth_token');
// Check logged-in providers
print('Logged in as: ${client.auth.loggedInProviders}');
// Switch active commenting account
client.auth.switchProvider(CommentumProvider.myanimelist);
// Get profiles for all connected accounts
final profiles = await client.auth.getAllLoggedInProfiles();
profiles.forEach((provider, user) {
print('${provider.displayName}: ${user.username}');
});
// Logout
await client.auth.logout(CommentumProvider.anilist); // Logout single
await client.auth.logoutAll(); // Logout allTo persist sessions across app launches and support background token refresh, implement CommentumStorage using secure storage.
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:commentum_client/commentum_client.dart';
class SecureCommentumStorage implements CommentumStorage {
final _storage = const FlutterSecureStorage();
String _jwtKey(CommentumProvider p) => 'jwt_${p.name}';
String _oauthKey(CommentumProvider p) => 'oauth_${p.name}';
@override
Future<void> saveToken(CommentumProvider provider, String token) =>
_storage.write(key: _jwtKey(provider), value: token);
@override
Future<String?> getToken(CommentumProvider provider) =>
_storage.read(key: _jwtKey(provider));
@override
Future<void> deleteToken(CommentumProvider provider) =>
_storage.delete(key: _jwtKey(provider));
@override
Future<void> saveProviderToken(CommentumProvider provider, String token) =>
_storage.write(key: _oauthKey(provider), value: token);
@override
Future<String?> getProviderToken(CommentumProvider provider) =>
_storage.read(key: _oauthKey(provider));
@override
Future<void> deleteProviderToken(CommentumProvider provider) =>
_storage.delete(key: _oauthKey(provider));
@override
Future<Map<CommentumProvider, String>> getAllTokens() async {
final map = <CommentumProvider, String>{};
for (final p in CommentumProvider.values) {
final t = await getToken(p);
if (t != null && t.isNotEmpty) map[p] = t;
}
return map;
}
@override
Future<void> clearAll() async {
for (final p in CommentumProvider.values) {
await deleteToken(p);
await deleteProviderToken(p);
}
}
}If you prefer using custom state managers (like Riverpod or BLoC) instead of CommentumDiscussionController, you can call API methods directly.
// Fetch comments
final response = await client.comments.listComments(
mediaId: '10123',
episodeNumber: 1, // Optional filter
limit: 20,
);
List<Comment> comments = response.data;
String? nextCursor = response.nextCursor;
// Post top-level comment
final comment = await client.comments.createComment(
mediaId: '10123',
mediaProvider: 'anilist',
content: 'Direct API posting!',
);
// Post reply
final reply = await client.comments.createReply(
parentId: comment.id,
content: 'Replying directly!',
);
// Vote (1 for upvote, -1 for downvote)
await client.interactions.voteComment(commentId: comment.id, voteType: 1);Use CommentTreeUtils to safely transform nested comment hierarchies without mutating state:
// Insert reply into nested tree
final updatedList = CommentTreeUtils.insertReply(currentComments, parentId, newReply);
// Optimistically update vote score
final votedList = CommentTreeUtils.updateVote(currentComments, commentId, 1);
// Remove deleted comment from tree
final filteredList = CommentTreeUtils.deleteComment(currentComments, commentId);Convenience interaction methods available directly on Comment instances:
final comment = comments.first;
await comment.upVote(client);
await comment.downVote(client);
await comment.report(client, 'Spoiler');
await comment.delete(client);try {
await client.comments.createComment(
mediaId: '101',
mediaProvider: 'anilist',
content: '',
);
} on CommentumValidationException catch (e) {
print('Validation Error: ${e.message}');
} on CommentumAuthException catch (e) {
print('Auth Error [${e.statusCode}]: Please log in.');
} on CommentumServerException catch (e) {
print('Server Error [${e.statusCode}]: ${e.message}');
} on CommentumNetworkException catch (e) {
print('Network Error: Check internet connection.');
}This project is licensed under the MIT License - see the LICENSE file for details.