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:
import 'package:flutter/material.dart';
import 'package:airship_flutter/airship_flutter.dart';
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@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(),
);
}
}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:
// 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:
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:
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<CustomMessageCenterScreen> {
List<InboxMessage> _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<void> _loadMessages() async {
setState(() {
_isLoading = true;
});
try {
List<InboxMessage> 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<void> _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<void> _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:
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<MessageDetailScreen> {
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:
class FilteredMessageCenterScreen extends StatefulWidget {
@override
_FilteredMessageCenterScreenState createState() => _FilteredMessageCenterScreenState();
}
class _FilteredMessageCenterScreenState extends State<FilteredMessageCenterScreen> {
List<InboxMessage> _filteredMessages = [];
String? _currentCategory;
Future<void> _loadAndFilterMessages() async {
// Get all messages
List<InboxMessage> allMessages = await Airship.messageCenter.messages;
// Get current named user
String? namedUserId = await Airship.contact.namedUserId;
// Filter messages
List<InboxMessage> 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<String>(
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:
- Always mark messages as read: When a user views a message, mark it as read to keep the unread count accurate
- Handle empty states: Show helpful messages when the inbox is empty
- Implement pull-to-refresh: Let users manually refresh their messages
- Show loading indicators: Provide feedback while fetching messages
- Clean up subscriptions: Cancel stream subscriptions in
dispose()to prevent memory leaks - Handle errors gracefully: Show user-friendly error messages if message loading fails
- Test with multiple users: If implementing named user filtering, thoroughly test the filtering logic
Categories