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(),
    );
  }
}
 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:

// 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:

  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