# Message Center Implement Message Center in your Flutter app to provide an inbox for persistent, rich messages. # Get started with Message Center on Flutter > Learn how to display Message Center, manage messages, and implement common inbox functionality in your Flutter app. Message Center provides an inbox for rich, HTML-based messages. Learn more about Message Center in our [feature guide](https://www.airship.com/docs/guides/features/messaging/message-center/). ## Display the Message Center The Flutter plugin provides a simple way to display the built-in Message Center UI. ### Using the default UI Display the built-in Message Center UI with a single method call. This is perfect for quickly adding Message Center functionality without custom design work: ```dart // Display the default Message Center UI Airship.messageCenter.display(); ``` This method can be called from anywhere in your app: ```dart // Example: Add a button to your app bar AppBar( title: Text('My App'), actions: [ IconButton( icon: Icon(Icons.inbox), onPressed: () { Airship.messageCenter.display(); }, ), ], ) ``` ### Display a specific message You can also display a specific message directly by providing its message ID: ```dart Airship.messageCenter.display(messageId: "specific-message-id"); ``` To customize the Message Center UI or navigation, see [Embedding the Message Center](https://www.airship.com/docs/developer/sdk-integration/flutter/message-center/embedding/). ## Fetch messages Retrieve all messages from the inbox: ```dart List messages = await Airship.messageCenter.messages; // Display messages in your UI for (var message in messages) { print('Message: ${message.title}'); print('ID: ${message.id}'); print('Unread: ${message.unread}'); print('Sent date: ${message.sentDate}'); } ``` ### InboxMessage properties Each `InboxMessage` contains: * **id**: Unique identifier for the message * **title**: Message title * **sentDate**: When the message was sent * **expirationDate**: When the message expires (optional) * **unread**: Whether the message is unread * **extras**: Custom key-value pairs associated with the message ## Listen for message updates Subscribe to real-time message updates using streams. This is useful for updating your UI when new messages arrive or existing messages are modified: ```dart StreamSubscription? inboxSubscription; @override void initState() { super.initState(); // Listen for inbox updates inboxSubscription = Airship.messageCenter.onInboxUpdated.listen((event) { setState(() { // Reload messages when the inbox is updated _loadMessages(); }); }); } Future _loadMessages() async { List messages = await Airship.messageCenter.messages; setState(() { _messages = messages; }); } @override void dispose() { inboxSubscription?.cancel(); super.dispose(); } ``` ## Track unread count Monitor the unread message count to display badges or update UI elements: ### Get current unread count ```dart int unreadCount = await Airship.messageCenter.unreadCount; print('Unread messages: $unreadCount'); ``` ### Listen for unread count changes ```dart // This is useful for updating badges in real-time Airship.messageCenter.onInboxUpdated.listen((event) async { int unreadCount = await Airship.messageCenter.unreadCount; // Update your badge UI _updateBadge(unreadCount); }); ``` ### Example: Show unread badge ```dart class InboxButton extends StatefulWidget { @override _InboxButtonState createState() => _InboxButtonState(); } class _InboxButtonState extends State { int _unreadCount = 0; StreamSubscription? _subscription; @override void initState() { super.initState(); _loadUnreadCount(); // Update badge when inbox changes _subscription = Airship.messageCenter.onInboxUpdated.listen((_) { _loadUnreadCount(); }); } Future _loadUnreadCount() async { int count = await Airship.messageCenter.unreadCount; setState(() { _unreadCount = count; }); } @override void dispose() { _subscription?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return IconButton( icon: Badge( label: _unreadCount > 0 ? Text('$_unreadCount') : null, child: Icon(Icons.inbox), ), onPressed: () { Airship.messageCenter.display(); }, ); } } ``` ## Refresh messages Manually refresh the message list from the server. This is useful for implementing pull-to-refresh functionality: ```dart // Refresh messages bool success = await Airship.messageCenter.refreshInbox(); if (success) { print('Messages refreshed successfully'); } else { print('Failed to refresh messages'); } ``` ### Example: Pull-to-refresh ```dart RefreshIndicator( onRefresh: () async { await Airship.messageCenter.refreshInbox(); }, child: ListView.builder( itemCount: messages.length, itemBuilder: (context, index) { return MessageListItem(message: messages[index]); }, ), ) ``` ## Mark messages as read Mark one or more messages as read to update their status: ```dart // Mark a single message as read await Airship.messageCenter.markRead(message.id); // Or using the message object directly await Airship.messageCenter.markRead(message.id); ``` Messages are typically marked as read automatically when viewed in the default Message Center UI. For custom implementations, you should mark messages as read when the user views them: ```dart // When user taps on a message void onMessageTapped(InboxMessage message) { // Mark as read Airship.messageCenter.markRead(message.id); // Navigate to message detail screen Navigator.push( context, MaterialPageRoute( builder: (context) => MessageDetailScreen(message: message), ), ); } ``` ## Delete messages Delete messages from the inbox: ```dart // Delete a single message await Airship.messageCenter.deleteMessage(message.id); ``` ### Example: Swipe to delete ```dart Dismissible( key: Key(message.id), direction: DismissDirection.endToStart, onDismissed: (direction) { Airship.messageCenter.deleteMessage(message.id); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Message deleted')), ); }, background: Container( color: Colors.red, alignment: Alignment.centerRight, padding: EdgeInsets.only(right: 20), child: Icon(Icons.delete, color: Colors.white), ), child: MessageListItem(message: message), ) ``` ## Filter messages by named user By default, Message Center displays all messages sent to the device's channel. If multiple users log into your app on the same device, they'll all see the same messages. To filter messages by named user, you need to: 1. Include a custom key when creating messages with `named_user_id` as the key and the user's actual ID as the value 2. Implement filtering logic in your custom Message Center implementation When creating Message Center messages: * **For the API**: Use the `extra` object in the [Message Center object](https://www.airship.com/docs/developer/rest-api/ua/schemas/push/#messageobject) * **In the dashboard**: See [Add custom keys](https://www.airship.com/docs/guides/messaging/messages/content/app/message-center/#add-custom-keys) in the Message Center content guide Then filter messages in your Flutter code: ```dart Future> getMessagesForCurrentUser() async { // Get the current named user ID String? currentUserId = await Airship.contact.namedUserId; if (currentUserId == null) { return []; // No user logged in } // Get all messages List allMessages = await Airship.messageCenter.messages; // Filter messages that match the current user return allMessages.where((message) { // Check if the message has a named_user_id extra String? messageUserId = message.extras['named_user_id']; return messageUserId == null || messageUserId == currentUserId; }).toList(); } ``` # Embed the Message Center > Build custom Message Center UIs with full control over design, navigation, and functionality using the InboxMessageView widget and Message Center APIs. This guide covers creating fully custom Message Center implementations for Flutter applications, giving you complete control over the design, navigation, and user experience. ## Override Default Display Behavior To use a custom Message Center implementation instead of the default UI, disable auto-launch and listen for display events: ```dart import 'package:flutter/material.dart'; import 'package:airship_flutter/airship_flutter.dart'; class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State { @override void initState() { super.initState(); // Disable the default Message Center UI Airship.messageCenter.setAutoLaunchDefaultMessageCenter(false); // Listen for display events and navigate to custom UI Airship.messageCenter.onDisplay.listen((event) { // Navigate to your custom Message Center screen Navigator.push( context, MaterialPageRoute( builder: (context) => CustomMessageCenterScreen( messageId: event.messageId, // null for full inbox, or specific message ID ), ), ); }); } @override Widget build(BuildContext context) { return MaterialApp( home: HomeScreen(), ); } } ``` > **Note:** When you disable the default Message Center, you're responsible for displaying your own UI when the `onDisplay` event fires. This includes handling both full inbox views and individual message views. ## Why customize Message Center While the default Message Center UI works great for many apps, you might want to customize it for: * **Brand consistency**: Match your app's unique design language and visual style * **Custom navigation**: Integrate Message Center into your app's existing navigation patterns * **Enhanced functionality**: Add search, filtering, categorization, or other custom features * **Multi-user support**: Filter messages by named user when multiple users share a device * **Platform-specific design**: Create different experiences for iOS and Android ## Custom Message Center implementation To build a custom Message Center, disable the default UI and use the Message Center APIs to manage messages programmatically. ### Step 1: Disable the default UI First, disable the default Message Center so you can display your own: ```dart // In your app initialization Airship.messageCenter.setAutoLaunchDefaultMessageCenter(false); ``` ### Step 2: Listen for display events Handle display events to show your custom UI when Message Center is triggered: ```dart Airship.messageCenter.onDisplay.listen((event) { // event.messageId will be null for full inbox, or contain a specific message ID Navigator.push( context, MaterialPageRoute( builder: (context) => CustomMessageCenterScreen( messageId: event.messageId, ), ), ); }); ``` ### Step 3: Build your custom UI Create your custom Message Center screen using the Message Center APIs: ```dart import 'package:flutter/material.dart'; import 'package:airship_flutter/airship_flutter.dart'; import 'dart:async'; class CustomMessageCenterScreen extends StatefulWidget { final String? messageId; const CustomMessageCenterScreen({Key? key, this.messageId}) : super(key: key); @override _CustomMessageCenterScreenState createState() => _CustomMessageCenterScreenState(); } class _CustomMessageCenterScreenState extends State { List _messages = []; int _unreadCount = 0; bool _isLoading = true; StreamSubscription? _inboxSubscription; @override void initState() { super.initState(); _loadMessages(); // Listen for inbox updates _inboxSubscription = Airship.messageCenter.onInboxUpdated.listen((_) { _loadMessages(); }); } Future _loadMessages() async { setState(() { _isLoading = true; }); try { List messages = await Airship.messageCenter.messages; int unreadCount = await Airship.messageCenter.unreadCount; setState(() { _messages = messages; _unreadCount = unreadCount; _isLoading = false; }); // If a specific message was requested, open it if (widget.messageId != null) { InboxMessage? message = _messages.firstWhere( (m) => m.id == widget.messageId, orElse: () => null as InboxMessage, ); if (message != null) { _openMessage(message); } } } catch (e) { setState(() { _isLoading = false; }); print('Error loading messages: $e'); } } Future _refreshMessages() async { await Airship.messageCenter.refreshInbox(); } void _openMessage(InboxMessage message) { // Mark as read Airship.messageCenter.markRead(message.id); // Navigate to message detail Navigator.push( context, MaterialPageRoute( builder: (context) => MessageDetailScreen(message: message), ), ); } Future _deleteMessage(InboxMessage message) async { await Airship.messageCenter.deleteMessage(message.id); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Message deleted')), ); } @override void dispose() { _inboxSubscription?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Messages'), actions: [ if (_unreadCount > 0) Center( child: Padding( padding: EdgeInsets.only(right: 16), child: Chip( label: Text('$_unreadCount unread'), backgroundColor: Theme.of(context).primaryColor, labelStyle: TextStyle(color: Colors.white), ), ), ), ], ), body: _isLoading ? Center(child: CircularProgressIndicator()) : _messages.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.inbox, size: 64, color: Colors.grey), SizedBox(height: 16), Text( 'No messages', style: Theme.of(context).textTheme.headlineSmall, ), ], ), ) : RefreshIndicator( onRefresh: _refreshMessages, child: ListView.builder( itemCount: _messages.length, itemBuilder: (context, index) { InboxMessage message = _messages[index]; return Dismissible( key: Key(message.id), direction: DismissDirection.endToStart, onDismissed: (direction) { _deleteMessage(message); }, background: Container( color: Colors.red, alignment: Alignment.centerRight, padding: EdgeInsets.only(right: 20), child: Icon(Icons.delete, color: Colors.white), ), child: ListTile( leading: CircleAvatar( backgroundColor: message.unread ? Theme.of(context).primaryColor : Colors.grey, child: Icon( message.unread ? Icons.mail : Icons.drafts, color: Colors.white, ), ), title: Text( message.title ?? 'No title', style: TextStyle( fontWeight: message.unread ? FontWeight.bold : FontWeight.normal, ), ), subtitle: Text( _formatDate(message.sentDate), style: TextStyle(fontSize: 12), ), trailing: Icon(Icons.chevron_right), onTap: () => _openMessage(message), ), ); }, ), ), ); } String _formatDate(DateTime date) { DateTime now = DateTime.now(); Duration difference = now.difference(date); if (difference.inDays == 0) { return 'Today ${date.hour}:${date.minute.toString().padLeft(2, '0')}'; } else if (difference.inDays == 1) { return 'Yesterday'; } else if (difference.inDays < 7) { return '${difference.inDays} days ago'; } else { return '${date.month}/${date.day}/${date.year}'; } } } ``` ## Using the InboxMessageView widget The `InboxMessageView` widget displays individual Message Center messages with their HTML content. Use this widget to create custom message detail screens: ```dart import 'package:flutter/material.dart'; import 'package:airship_flutter/airship_flutter.dart'; class MessageDetailScreen extends StatefulWidget { final InboxMessage message; const MessageDetailScreen({Key? key, required this.message}) : super(key: key); @override _MessageDetailScreenState createState() => _MessageDetailScreenState(); } class _MessageDetailScreenState extends State { InboxMessageViewController? _controller; void _onInboxMessageViewCreated(InboxMessageViewController controller) { _controller = controller; controller.loadMessage(widget.message); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.message.title ?? 'Message'), actions: [ IconButton( icon: Icon(Icons.delete), onPressed: () { // Delete and go back Airship.messageCenter.deleteMessage(widget.message.id); Navigator.pop(context); }, ), ], ), body: InboxMessageView( onViewCreated: _onInboxMessageViewCreated, ), ); } } ``` ### InboxMessageView properties The `InboxMessageView` widget provides: * **Automatic HTML rendering**: Displays rich HTML content from Message Center messages * **Link handling**: Automatically handles links within message content * **Action execution**: Processes Airship actions embedded in messages * **Responsive layout**: Adapts to different screen sizes ## Advanced: Message filtering Filter messages based on custom criteria, such as named user or categories: ```dart class FilteredMessageCenterScreen extends StatefulWidget { @override _FilteredMessageCenterScreenState createState() => _FilteredMessageCenterScreenState(); } class _FilteredMessageCenterScreenState extends State { List _filteredMessages = []; String? _currentCategory; Future _loadAndFilterMessages() async { // Get all messages List allMessages = await Airship.messageCenter.messages; // Get current named user String? namedUserId = await Airship.contact.namedUserId; // Filter messages List filtered = allMessages.where((message) { // Filter by named user if set if (namedUserId != null) { String? messageUserId = message.extras['named_user_id']; if (messageUserId != null && messageUserId != namedUserId) { return false; } } // Filter by category if set if (_currentCategory != null) { String? messageCategory = message.extras['category']; if (messageCategory != _currentCategory) { return false; } } return true; }).toList(); setState(() { _filteredMessages = filtered; }); } void _setCategory(String? category) { setState(() { _currentCategory = category; }); _loadAndFilterMessages(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Messages'), actions: [ PopupMenuButton( onSelected: _setCategory, itemBuilder: (context) => [ PopupMenuItem(value: null, child: Text('All')), PopupMenuItem(value: 'promotions', child: Text('Promotions')), PopupMenuItem(value: 'updates', child: Text('Updates')), PopupMenuItem(value: 'alerts', child: Text('Alerts')), ], ), ], ), body: ListView.builder( itemCount: _filteredMessages.length, itemBuilder: (context, index) { return MessageListItem(message: _filteredMessages[index]); }, ), ); } } ``` ## Best practices When implementing custom Message Center: 1. **Always mark messages as read**: When a user views a message, mark it as read to keep the unread count accurate 2. **Handle empty states**: Show helpful messages when the inbox is empty 3. **Implement pull-to-refresh**: Let users manually refresh their messages 4. **Show loading indicators**: Provide feedback while fetching messages 5. **Clean up subscriptions**: Cancel stream subscriptions in `dispose()` to prevent memory leaks 6. **Handle errors gracefully**: Show user-friendly error messages if message loading fails 7. **Test with multiple users**: If implementing named user filtering, thoroughly test the filtering logic