AppMsgr Flutter SDK

Official Flutter/Dart SDK for AppMsgr chat infrastructure.

🔐 Authentication Modes

The SDK supports two authentication modes:

1. API Key Authentication (Tenant-Level)

Use your tenant API key for operations on behalf of your organization:

final client = AppMsgrClient.withApiKey(
  apiKey: 'appmsgr_live_xxxxx',
);

2. User Token Authentication (User-Level) ⭐ NEW

Use JWT user tokens for user-scoped operations (recommended for production):

// Get JWT from your GraphQL backend
final userToken = await yourGraphQL.query(appmsgrApiKeyQuery);

final client = AppMsgrClient.withUserToken(
  userToken: userToken,
);

Recommended Pattern: Get user tokens from your GraphQL backend, use SDK to communicate directly with AppMsgr API.

⚠️ Architecture Recommendation

For Production Apps: We recommend the Backend + SDK Pattern where:

  1. Your Flutter app authenticates with your GraphQL backend
  2. Backend issues user-scoped JWT tokens for AppMsgr
  3. Flutter SDK uses JWT tokens to communicate directly with AppMsgr API

This provides:

  • 🔒 Better Security - API keys stay server-side, users get scoped tokens
  • 🎮 Custom Business Logic - Backend controls token issuance and user permissions
  • 🔧 Flexibility - Backend manages authentication, SDK handles real-time chat
  • Performance - Direct SDK→AppMsgr communication, no double compute
Your Flutter App → Your Backend (Auth/Tokens) → Issues JWT
     ↓                                              ↓
AppMsgr SDK ───────────────────────────────────→ AppMsgr API
              (Direct communication with JWT)

This SDK is best for:

  • 🔬 Prototyping and testing (API key mode)
  • 🚀 Production apps with user authentication (user token mode)
  • 🛠️ Internal tools and admin dashboards
  • 📱 Apps with or without existing backend infrastructure

See the Integration Patterns section below for detailed architecture guidance.

Features

  • ✅ Complete REST API client (Channels, Messages, Reactions)
  • ✅ GraphQL client for real-time subscriptions
  • ✅ Automatic reconnection and error handling
  • ✅ Offline message queue
  • ✅ Type-safe API with strong typing
  • ✅ Cross-platform (iOS, Android, Web, Desktop)

Installation

Add to your pubspec.yaml:

dependencies:
  appmsgr_flutter: ^1.0.0

Or install via command:

flutter pub add appmsgr_flutter

Quick Start

Initialize Client

import 'package:appmsgr_flutter/appmsgr_flutter.dart';

// Option 1: API Key Authentication (tenant-level)
final client = AppMsgrClient.withApiKey(
  apiKey: 'appmsgr_live_xxxxx',
  baseUrl: 'https://api.appmsgr.com', // Optional
);

// Option 2: User Token Authentication (user-level, recommended)
final userToken = await yourBackend.getUserToken(userId);
final client = AppMsgrClient.withUserToken(
  userToken: userToken,
  baseUrl: 'https://api.appmsgr.com', // Optional
);

// With custom configuration
final client = AppMsgrClient.withUserToken(
  userToken: userToken,
  baseUrl: 'https://api.appmsgr.com',
  config: AppMsgrConfig(
    timeout: Duration(seconds: 30),
    retryAttempts: 3,
    enableOfflineQueue: true,
  ),
);

Example: Game Backend Integration

import 'package:appmsgr_flutter/appmsgr_flutter.dart';

class ChatService {
  late AppMsgrClient _appmsgr;

  Future<void> initialize(String userId) async {
    // Get user token from GraphQL backend
    final token = await graphQLClient.query(
      appmsgrApiKeyQuery,
      variables: {'userId': userId},
    );

    // Initialize AppMsgr SDK with user token
    _appmsgr = AppMsgrClient.withUserToken(
      userToken: token,
    );

    debugPrint('✅ AppMsgr SDK initialized');
  }

  Future<void> sendMessage(String channelId, String message) async {
    await _appmsgr.messages.sendMessage(
      channelId: channelId,
      content: message,
    );
  }
}

Send Messages

// Send a text message
final message = await client.messages.send(
  channelId: 'channel-123',
  request: SendMessageRequest(
    userId: 'player-456',
    username: 'DragonSlayer',
    content: 'Ready for the raid!',
    messageType: MessageType.text,
  ),
);

debugPrint('Message sent: ${message.messageId}');

Get Messages (Paginated)

// Get latest messages
final response = await client.messages.getMessages(
  channelId: 'channel-123',
  limit: 50,
);

debugPrint('Loaded ${response.messages.length} messages');
debugPrint('Has more: ${response.hasMore}');

// Load next page
if (response.hasMore) {
  final nextPage = await client.messages.getMessages(
    channelId: 'channel-123',
    limit: 50,
    before: response.nextCursor,
  );
}

Add Reactions

// Add emoji reaction
final reaction = await client.reactions.add(
  messageId: 'msg-123',
  request: AddReactionRequest(
    playerId: 'player-456',
    playerUsername: 'DragonSlayer',
    emojiId: 'U+1F44D',
    emojiPlatform: EmojiPlatform.ios,
    emojiUnicode: '👍',
    emojiName: 'thumbs_up',
  ),
);

// Get reactions grouped by emoji
final reactions = await client.reactions.getReactions(
  messageId: 'msg-123',
  groupByEmoji: true,
);

for (final group in reactions.reactions) {
  debugPrint('${group.emojiUnicode}: ${group.count} reactions');
}

Manage Channels

// Create a channel
final channel = await client.channels.create(
  tenantId: 'tenant-123',
  request: CreateChannelRequest(
    channelType: ChannelType.alliance,
    channelName: 'Guild Chat',
    serverId: 'your-game-us-west',
    scope: ChannelScope.server,
    groupId: 'alliance-456',
    settings: ChannelSettings(
      allowReactions: true,
      allowAttachments: true,
      allowTranslation: true,
    ),
  ),
);

// List channels
final channels = await client.channels.list(
  tenantId: 'tenant-123',
  type: ChannelType.alliance,
  status: ChannelStatus.active,
  limit: 50,
);

// Update channel
final updated = await client.channels.update(
  channelId: channel.channelId,
  request: UpdateChannelRequest(
    channelName: 'Eternal Warriors - Updated',
    settings: ChannelSettings(
      messageRetentionDays: 60,
    ),
  ),
);

GraphQL Subscriptions (Real-time)

// Subscribe to new messages
final subscription = client.graphql.subscribeToMessages(
  channelId: 'channel-123',
);

await for (final message in subscription) {
  debugPrint('New message from ${message.username}: ${message.content}');
  // Update your UI
}

// Subscribe to reactions
final reactionSub = client.graphql.subscribeToReactions(
  messageId: 'msg-123',
);

await for (final reaction in reactionSub) {
  debugPrint('${reaction.playerUsername} reacted with ${reaction.emojiUnicode}');
}

Architecture

Backend Integration

Flutter App → AppMsgr Backend GraphQL → AppMsgr REST API

The Flutter SDK communicates with your backend GraphQL server, which acts as a proxy to AppMsgr's REST API. This allows you to:

  • Add custom authentication
  • Implement rate limiting
  • Cache responses
  • Add custom business logic

Example Backend GraphQL Schema

type Mutation {
  sendMessage(channelId: ID!, content: String!): Message!
  addReaction(messageId: ID!, emoji: String!): Reaction!
}

type Subscription {
  messageAdded(channelId: ID!): Message!
  reactionAdded(messageId: ID!): Reaction!
}

Error Handling

try {
  final message = await client.messages.send(
    channelId: 'channel-123',
    request: SendMessageRequest(
      userId: 'player-456',
      username: 'DragonSlayer',
      content: 'Hello!',
    ),
  );
} on AppMsgrApiException catch (e) {
  if (e.statusCode == 404) {
    debugPrint('Channel not found');
  } else if (e.statusCode == 401) {
    debugPrint('Unauthorized - check API key');
  } else if (e.statusCode == 429) {
    debugPrint('Rate limited - retry after ${e.retryAfter}');
  } else {
    debugPrint('Error: ${e.message}');
  }
} on AppMsgrNetworkException catch (e) {
  debugPrint('Network error: ${e.message}');
  // Message will be queued if offline queue is enabled
}

Offline Support

The SDK automatically queues messages when offline and syncs them when connectivity is restored.

⚠️ Security note — queued messages are stored unencrypted on disk. The offline queue uses Hive for local persistence. Message content, channel IDs, and user IDs sit in the app's sandbox as plaintext JSON. Auth tokens are NOT stored, but message content is — on a rooted Android or jailbroken iOS device, that data is readable.

On logout, set AppMsgrConfig(clearOfflineQueueOnDispose: true) (or call client.clearQueue() manually) so the departing user's messages do not remain readable by the next user of the device.

For strong data-at-rest protection, wrap the SDK init in your own encryption setup using flutter_secure_storage. Hive-level encryption is planned for a future major release.

final client = AppMsgrClient.withApiKey(
  apiKey: 'your_api_key',
  config: AppMsgrConfig(
    enableOfflineQueue: true,
    maxOfflineQueueSize: 100,
    clearOfflineQueueOnDispose: true, // recommended for multi-user devices
  ),
);

// Initialize to start offline queue
await client.initialize();

// Use client.sendMessage (not messages.sendMessage) for offline support
// This will be queued if offline
await client.sendMessage(
  channelId: 'channel-123',
  content: 'Sent while offline',
);

// Get queue status
final status = client.getQueueStatus();
debugPrint('Pending messages: ${status?.pendingCount}');
debugPrint('Failed messages: ${status?.failedCount}');

// Get all queued messages
final queued = client.getQueuedMessages();
for (final msg in queued ?? []) {
  debugPrint('Queued: ${msg.content} (${msg.status})');
}

// Manual sync
await client.processPendingMessages();

// Clear failed messages
await client.clearFailedMessages();

Testing

Unit Tests

import 'package:appmsgr_flutter/appmsgr_flutter.dart';
import 'package:test/test.dart';

void main() {
  test('should send message successfully', () async {
    final client = AppMsgrClient(apiKey: 'test_key');

    final message = await client.messages.send(
      channelId: 'test-channel',
      request: SendMessageRequest(
        userId: 'test-user',
        username: 'TestUser',
        content: 'Test message',
      ),
    );

    expect(message.messageId, isNotEmpty);
    expect(message.content, equals('Test message'));
  });
}

Mock Client for Testing

import 'package:appmsgr_flutter/appmsgr_flutter.dart';
import 'package:mockito/mockito.dart';

class MockAppMsgrClient extends Mock implements AppMsgrClient {}

void main() {
  test('should handle message send in app', () async {
    final mockClient = MockAppMsgrClient();

    when(mockClient.messages.send(any, any))
      .thenAnswer((_) async => Message(
        messageId: 'mock-123',
        channelId: 'channel-123',
        userId: 'user-123',
        content: 'Mocked message',
        sentAt: DateTime.now(),
      ));

    // Test your app logic with mock client
    final result = await yourAppFunction(mockClient);
    expect(result, isTrue);
  });
}

Example: Chat Screen

import 'package:flutter/material.dart';
import 'package:appmsgr_flutter/appmsgr_flutter.dart';

class ChatScreen extends StatefulWidget {
  final String channelId;

  const ChatScreen({required this.channelId});

  @override
  _ChatScreenState createState() => _ChatScreenState();
}

class _ChatScreenState extends State<ChatScreen> {
  final client = AppMsgrClient(apiKey: 'your_api_key');
  final textController = TextEditingController();
  List<Message> messages = [];
  bool isLoading = false;
  bool hasMore = true;
  String? nextCursor;

  @override
  void initState() {
    super.initState();
    _loadMessages();
    _subscribeToMessages();
  }

  Future<void> _loadMessages() async {
    if (isLoading || !hasMore) return;

    setState(() => isLoading = true);

    try {
      final response = await client.messages.getMessages(
        channelId: widget.channelId,
        limit: 50,
        before: nextCursor,
      );

      setState(() {
        messages.addAll(response.messages);
        hasMore = response.hasMore;
        nextCursor = response.nextCursor;
        isLoading = false;
      });
    } catch (e) {
      debugPrint('Error loading messages: $e');
      setState(() => isLoading = false);
    }
  }

  void _subscribeToMessages() {
    client.graphql
      .subscribeToMessages(channelId: widget.channelId)
      .listen((message) {
        setState(() {
          messages.insert(0, message);
        });
      });
  }

  Future<void> _sendMessage() async {
    final content = textController.text.trim();
    if (content.isEmpty) return;

    textController.clear();

    try {
      await client.messages.send(
        channelId: widget.channelId,
        request: SendMessageRequest(
          userId: 'current-user-id',
          username: 'CurrentUser',
          content: content,
          messageType: MessageType.text,
        ),
      );
    } catch (e) {
      debugPrint('Error sending message: $e');
      // Show error to user
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Chat')),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
              reverse: true,
              itemCount: messages.length + (isLoading ? 1 : 0),
              itemBuilder: (context, index) {
                if (index == messages.length) {
                  return Center(child: CircularProgressIndicator());
                }

                final message = messages[index];
                return ListTile(
                  title: Text(message.username),
                  subtitle: Text(message.content),
                  trailing: Text(
                    message.sentAt.toString(),
                    style: TextStyle(fontSize: 12),
                  ),
                );
              },
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: textController,
                    decoration: InputDecoration(
                      hintText: 'Type a message...',
                    ),
                  ),
                ),
                IconButton(
                  icon: Icon(Icons.send),
                  onPressed: _sendMessage,
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

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

Performance Tips

  1. Use Pagination: Always paginate messages with reasonable limits (50-100)
  2. Cache Locally: Store messages in local database (sqflite, hive)
  3. Lazy Load: Only load more messages when user scrolls near top
  4. Debounce Typing: Don't send typing indicator on every keystroke
  5. Batch Reactions: Group reaction updates to reduce API calls

Integration Patterns

Best for: Production games, enterprise apps, apps with existing backend

Flutter App → Your Backend → AppMsgr API

Your Backend (Node.js/Express example):

// Your backend exposes GraphQL/REST that Flutter calls
app.post('/api/chat/send-message', async (req, res) => {
  const { channelId, content, userId } = req.body;

  // Add your business logic here
  if (!await validateUser(userId)) {
    return res.status(403).json({ error: 'Unauthorized' });
  }

  // Apply profanity filter, rate limits, etc.
  const cleanContent = profanityFilter(content);

  // Call AppMsgr API (API key stays server-side!)
  const response = await fetch('https://api.appmsgr.com/api/v1/messages', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.APPMSGR_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ channelId, content: cleanContent })
  });

  const message = await response.json();
  res.json(message);
});

Your Flutter App:

// Call YOUR backend, not AppMsgr directly
Future<void> sendMessage(String channelId, String content) async {
  final response = await http.post(
    Uri.parse('https://your-backend.com/api/chat/send-message'),
    headers: {'Authorization': 'Bearer ${yourUserToken}'},
    body: jsonEncode({
      'channelId': channelId,
      'content': content,
      'userId': currentUser.id,
    }),
  );
  // Your backend handles the AppMsgr integration
}

Advantages:

  • ✅ API keys never exposed in client
  • ✅ Add game-specific logic (anti-cheat, validation)
  • ✅ Rate limiting per player
  • ✅ Caching and optimization
  • ✅ Version control without app updates
  • ✅ Combine multiple services in one API

Pattern 2: Direct SDK (Good for Prototyping)

Best for: Prototypes, MVPs, internal tools, simple apps

Flutter App (with SDK) → AppMsgr API directly
// Direct SDK usage (what this package provides)
final client = AppMsgrClient(
  apiKey: 'your_api_key', // ⚠️ Can be extracted from app!
  baseUrl: 'https://api.appmsgr.com',
);

await client.messages.sendMessage(
  channelId: 'channel-123',
  content: 'Hello!',
);

Advantages:

  • ✅ Quick to set up
  • ✅ No backend needed
  • ✅ Good for prototyping

Disadvantages:

  • ❌ API keys in client (security risk)
  • ❌ Hard to add custom logic
  • ❌ Updates require app store releases
  • ❌ No caching/optimization layer

Pattern 3: Hybrid (Best of Both)

Best for: Apps transitioning from prototype to production

Flutter App → Your Backend (authenticated endpoints)
           ↘ AppMsgr API (read-only subscriptions)
  • Write operations (send message, add reaction) go through your backend
  • Read operations and subscriptions can use SDK directly
  • API keys for write operations stay server-side
  • Real-time subscriptions connect directly for low latency

Production Example

For a production game:

Game Client (Flutter / Godot / Unity)
    ↓ (Your custom GraphQL or REST schema)
Your Game Backend (Node.js / Go / etc.)
    ↓ (AppMsgr API Key — server-side only)
AppMsgr API

Your Backend GraphQL Schema:

type Mutation {
  sendGuildMessage(guildId: ID!, content: String!): Message
  addReaction(messageId: ID!, emoji: String!): Reaction
}

type Subscription {
  guildMessageAdded(guildId: ID!): Message
}

Implementation:

// Your GraphQL resolver
const resolvers = {
  Mutation: {
    sendGuildMessage: async (_, { guildId, content }, context) => {
      // Verify player is in guild
      const player = context.player;
      if (!await isPlayerInGuild(player.id, guildId)) {
        throw new Error('Not in guild');
      }

      // Call AppMsgr API
      const message = await appMsgrClient.messages.sendMessage({
        channelId: guildId,
        content,
        metadata: {
          playerId: player.id,
          playerName: player.name,
          playerLevel: player.level
        }
      });

      return message;
    }
  }
};

This way you control player authentication, guild membership, and can add game-specific data to messages.

Migration from Unity SDK

If migrating from Unity C#:

Unity (C#) Flutter (Dart)
AppMsgrClient AppMsgrClient
await client.Messages.SendAsync() await client.messages.send()
Message[] List<Message>
async/await async/await (same!)
UnityWebRequest http package
JsonUtility dart:convert

Mail System

AppMsgr includes a comprehensive mail/inbox system for delivering rewards, notifications, and messages to players with attachment claiming and webhook integration.

Send Mail with Attachments

// Send reward mail with attachments
final mail = await client.mail.sendMail(
  tenantId: 'your-tenant-id',
  senderId: 'system',
  senderType: 'system',
  senderDisplayName: 'Game Rewards',
  subject: 'Daily Login Reward',
  body: 'Thank you for logging in! Here is your reward.',
  mailType: 'reward',
  category: 'daily_rewards',
  priority: 'high',
  recipientType: 'player',
  recipientIds: ['player-123', 'player-456'],
  icon: 'icons/gift.png',
  highlightColor: '#FFD700',
  metadata: {
    'attachments': [
      {
        'type': 'gold',
        'id': 'currency_gold',
        'name': 'Gold',
        'amount': 1000,
        'icon': 'icons/gold.png',
      },
      {
        'type': 'item',
        'id': 'item_sword_legendary',
        'name': 'Legendary Sword',
        'amount': 1,
        'icon': 'icons/sword.png',
        'metadata': {
          'rarity': 'legendary',
          'level': 50,
        },
      },
    ],
  },
  webhookTriggers: ['mail.attachments.claimed'],
);

Get Inbox

// Get player's inbox with pagination
final inbox = await client.mail.getInbox(
  tenantId: 'your-tenant-id',
  playerId: 'player-123',
  limit: 50,
  offset: 0,
  unreadOnly: false,
  includeArchived: false,
);

debugPrint('Loaded ${inbox.mail.length} messages');
debugPrint('Unread: ${inbox.mail.where((m) => !m.isRead).length}');

// Display mail list
for (final mail in inbox.mail) {
  debugPrint('${mail.subject} - ${mail.senderDisplayName}');
  if (mail.metadata?['attachments'] != null) {
    debugPrint('  📦 Has attachments');
  }
}

Claim Attachments

// Player claims mail attachments (triggers webhook)
try {
  final result = await client.mail.claimAttachments(
    mailId: 'mail-123',
    playerId: 'player-456',
  );

  debugPrint('Claimed ${result.attachments.length} attachments!');

  // Process attachments in your game
  for (final attachment in result.attachments) {
    switch (attachment.type) {
      case 'gold':
        addCurrency('gold', attachment.amount ?? 0);
        break;
      case 'item':
        addItemToInventory(attachment.id, attachment.amount ?? 1);
        break;
      case 'troop':
        recruitTroops(attachment.id, attachment.amount ?? 1);
        break;
    }
  }
} catch (e) {
  // Already claimed or mail not found
  debugPrint('Could not claim attachments: $e');
}

Webhook Integration

When a player claims mail attachments, AppMsgr sends a webhook to your game backend:

POST https://your-game-backend.com/webhooks/appmsgr
Content-Type: application/json
X-AppMsgr-Signature: sha256=...

{
  "event": "mail.attachments.claimed",
  "timestamp": "2025-11-10T12:34:56.789Z",
  "tenantId": "your-tenant-id",
  "data": {
    "mailId": "mail-123",
    "playerId": "player-456",
    "attachments": [
      {
        "type": "gold",
        "id": "currency_gold",
        "name": "Gold",
        "amount": 1000
      }
    ],
    "claimedAt": "2025-11-10T12:34:56.789Z"
  }
}

Backend Implementation:

// Your game backend webhook handler
app.post('/webhooks/appmsgr', async (req, res) => {
  const { event, data } = req.body;

  if (event === 'mail.attachments.claimed') {
    const { playerId, attachments } = data;

    // Grant rewards to player in your game database
    for (const attachment of attachments) {
      await grantReward(playerId, attachment);
    }

    // Mark as processed
    await saveWebhookLog(req.body);
  }

  res.status(200).send('OK');
});

Mail Operations

// Mark as read
await client.mail.markAsRead(
  mailId: 'mail-123',
  playerId: 'player-456',
);

// Delete mail (soft delete)
await client.mail.deleteMail(
  mailId: 'mail-123',
  playerId: 'player-456',
);

// Get unread count (for badge)
final count = await client.mail.getUnreadCount(
  tenantId: 'your-tenant-id',
  playerId: 'player-456',
);
debugPrint('Unread: ${count.unreadCount}');

Mail Types & Categories

Organize mail using types and categories:

// System mail
mailType: 'system',
category: 'maintenance',

// Reward mail
mailType: 'reward',
category: 'daily_login', // daily_rewards, event_rewards, achievement_rewards

// Player-to-player mail
mailType: 'player',
category: 'trade', // trade, gift, message

// Alliance/guild mail
mailType: 'alliance',
category: 'announcement', // announcement, war, recruitment

// Admin broadcast
mailType: 'admin',
category: 'news', // news, update, warning

Example: Mail Screen Widget

See the example app for a complete implementation of:

  • Mail inbox with unread badges
  • Attachment claiming UI
  • Mail detail modal
  • Refresh and pagination

Troubleshooting

Common Issues

"API key is invalid"

  • Verify your API key is correct in the client initialization
  • Check that the API key hasn't been revoked in the AppMsgr dashboard
  • Ensure you're using the correct environment (staging vs production)

Messages not appearing in real-time

  • Verify GraphQL subscription is connected: client.graphql.isConnected
  • Check that you're subscribed to the correct channel ID
  • Ensure WebSocket connection isn't blocked by firewall/proxy
  • Try reconnecting: await client.graphql.connect()

Offline queue not working

  • Ensure offline queue is enabled in config: config.enableOfflineQueue = true
  • Call await client.initialize() before sending messages
  • Check queue status: final status = client.getQueueStatus()
  • Review queued messages: final messages = client.getQueuedMessages()

High memory usage

  • Implement pagination for message lists - don't load all messages at once
  • Unsubscribe from GraphQL subscriptions when not needed
  • Clear offline queue periodically: await client.clearQueue()
  • Dispose client when done: await client.dispose()

Slow API responses

  • Check your internet connection
  • Verify baseUrl points to nearest region
  • Consider implementing caching in your backend
  • Use pagination instead of loading all data

JSON serialization errors

  • Ensure all model classes have proper fromJson/toJson methods
  • Check that API response format matches expected model structure
  • Validate that custom metadata fields are JSON-serializable

Debug Mode

Enable detailed logging:

import 'package:logger/logger.dart';

final client = AppMsgrClient(
  apiKey: 'your_api_key',
  config: AppMsgrConfig(
    logger: Logger(
      level: Level.debug,
      printer: PrettyPrinter(
        methodCount: 0,
        printTime: true,
      ),
    ),
  ),
);

Performance Tips

  1. Batch Operations: Send multiple messages in quick succession? Consider batching on your backend
  2. Pagination: Always use pagination for large message lists
  3. Subscriptions: Only subscribe to channels the user is actively viewing
  4. Offline Queue: Set reasonable maxOfflineQueueSize (default 100)
  5. Caching: Implement local caching for channels and messages to reduce API calls

Testing

For unit tests, use mockito to mock the HTTP client:

import 'package:mockito/mockito.dart';
import 'package:http/http.dart' as http;

final mockClient = MockClient();
when(mockClient.post(any, headers: anyNamed('headers'), body: anyNamed('body')))
    .thenAnswer((_) async => http.Response('{"id": "msg-123"}', 200));

final client = AppMsgrClient(
  apiKey: 'test-key',
  httpClient: mockClient,
);

Support

License

MIT License - See LICENSE file for details

Libraries

appmsgr_flutter
AppMsgr Flutter SDK