Flutter StreamBuilder Localization: Real-Time Data, Live Updates, and Status Messages
StreamBuilder enables reactive UI updates for real-time data in Flutter apps. Proper localization ensures connection status messages, live updates, and data presentations work seamlessly across languages. This guide covers comprehensive strategies for localizing StreamBuilder widgets in Flutter.
Understanding StreamBuilder Localization
StreamBuilder widgets require localization for:
- Connection states: Connecting, active, done, and waiting messages
- Live status indicators: Online/offline, typing indicators, presence states
- Real-time counters: Live viewer counts, unread badges, notification counts
- Stream error messages: Connection lost, reconnecting, timeout errors
- Data timestamps: "Last updated", "Live", "2 minutes ago"
- Accessibility announcements: Screen reader updates for live content
Basic StreamBuilder with Localized Status
Start with a simple StreamBuilder that handles connection states:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedStockTicker extends StatefulWidget {
final String symbol;
const LocalizedStockTicker({super.key, required this.symbol});
@override
State<LocalizedStockTicker> createState() => _LocalizedStockTickerState();
}
class _LocalizedStockTickerState extends State<LocalizedStockTicker> {
late Stream<StockPrice> _priceStream;
@override
void initState() {
super.initState();
_priceStream = _createPriceStream();
}
Stream<StockPrice> _createPriceStream() async* {
while (true) {
await Future.delayed(const Duration(seconds: 2));
yield StockPrice(
symbol: widget.symbol,
price: 150.00 + (DateTime.now().millisecond % 100) / 10,
change: (DateTime.now().millisecond % 50 - 25) / 10,
timestamp: DateTime.now(),
);
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: StreamBuilder<StockPrice>(
stream: _priceStream,
builder: (context, snapshot) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(l10n, snapshot),
const SizedBox(height: 16),
_buildContent(l10n, snapshot),
],
);
},
),
),
);
}
Widget _buildHeader(AppLocalizations l10n, AsyncSnapshot<StockPrice> snapshot) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.symbol,
style: Theme.of(context).textTheme.headlineSmall,
),
_buildConnectionStatus(l10n, snapshot.connectionState),
],
);
}
Widget _buildConnectionStatus(AppLocalizations l10n, ConnectionState state) {
final (color, text, icon) = switch (state) {
ConnectionState.none => (Colors.grey, l10n.disconnected, Icons.cloud_off),
ConnectionState.waiting => (Colors.orange, l10n.connecting, Icons.sync),
ConnectionState.active => (Colors.green, l10n.live, Icons.circle),
ConnectionState.done => (Colors.grey, l10n.streamEnded, Icons.stop_circle),
};
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 12, color: color),
const SizedBox(width: 4),
Text(
text,
style: TextStyle(color: color, fontSize: 12),
),
],
);
}
Widget _buildContent(AppLocalizations l10n, AsyncSnapshot<StockPrice> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: Column(
children: [
const CircularProgressIndicator(),
const SizedBox(height: 8),
Text(l10n.loadingPrice),
],
),
);
}
if (snapshot.hasError) {
return Center(
child: Column(
children: [
Icon(
Icons.error_outline,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(height: 8),
Text(l10n.priceUnavailable),
],
),
);
}
if (!snapshot.hasData) {
return Center(child: Text(l10n.waitingForData));
}
final price = snapshot.data!;
final isPositive = price.change >= 0;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.priceValue(price.price.toStringAsFixed(2)),
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 8),
Row(
children: [
Icon(
isPositive ? Icons.arrow_upward : Icons.arrow_downward,
color: isPositive ? Colors.green : Colors.red,
size: 16,
),
Text(
l10n.priceChange(
isPositive ? '+' : '',
price.change.toStringAsFixed(2),
),
style: TextStyle(
color: isPositive ? Colors.green : Colors.red,
),
),
],
),
const SizedBox(height: 8),
Text(
l10n.lastUpdated(_formatTimestamp(l10n, price.timestamp)),
style: Theme.of(context).textTheme.bodySmall,
),
],
);
}
String _formatTimestamp(AppLocalizations l10n, DateTime timestamp) {
final now = DateTime.now();
final difference = now.difference(timestamp);
if (difference.inSeconds < 5) {
return l10n.justNow;
} else if (difference.inSeconds < 60) {
return l10n.secondsAgo(difference.inSeconds);
} else if (difference.inMinutes < 60) {
return l10n.minutesAgo(difference.inMinutes);
}
return l10n.hoursAgo(difference.inHours);
}
}
class StockPrice {
final String symbol;
final double price;
final double change;
final DateTime timestamp;
StockPrice({
required this.symbol,
required this.price,
required this.change,
required this.timestamp,
});
}
ARB File for Real-Time Status
{
"disconnected": "Disconnected",
"@disconnected": {
"description": "Stream disconnected status"
},
"connecting": "Connecting...",
"@connecting": {
"description": "Stream connecting status"
},
"live": "Live",
"@live": {
"description": "Stream is live and active"
},
"streamEnded": "Ended",
"@streamEnded": {
"description": "Stream has ended"
},
"loadingPrice": "Loading price...",
"@loadingPrice": {
"description": "Loading price data"
},
"priceUnavailable": "Price unavailable",
"@priceUnavailable": {
"description": "Price data not available"
},
"waitingForData": "Waiting for data...",
"@waitingForData": {
"description": "Waiting for stream data"
},
"priceValue": "${price}",
"@priceValue": {
"description": "Formatted price with currency",
"placeholders": {
"price": {"type": "String"}
}
},
"priceChange": "{sign}{amount}",
"@priceChange": {
"description": "Price change with sign",
"placeholders": {
"sign": {"type": "String"},
"amount": {"type": "String"}
}
},
"lastUpdated": "Last updated: {time}",
"@lastUpdated": {
"description": "Last update timestamp",
"placeholders": {
"time": {"type": "String"}
}
},
"justNow": "just now",
"@justNow": {
"description": "Time indicator for very recent"
},
"secondsAgo": "{seconds} {seconds, plural, =1{second} other{seconds}} ago",
"@secondsAgo": {
"description": "Seconds ago indicator",
"placeholders": {
"seconds": {"type": "int"}
}
},
"minutesAgo": "{minutes} {minutes, plural, =1{minute} other{minutes}} ago",
"@minutesAgo": {
"description": "Minutes ago indicator",
"placeholders": {
"minutes": {"type": "int"}
}
},
"hoursAgo": "{hours} {hours, plural, =1{hour} other{hours}} ago",
"@hoursAgo": {
"description": "Hours ago indicator",
"placeholders": {
"hours": {"type": "int"}
}
}
}
Chat Application with Localized Status
Build a chat interface with typing indicators and online status:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedChatScreen extends StatefulWidget {
final String chatRoomId;
const LocalizedChatScreen({super.key, required this.chatRoomId});
@override
State<LocalizedChatScreen> createState() => _LocalizedChatScreenState();
}
class _LocalizedChatScreenState extends State<LocalizedChatScreen> {
late Stream<ChatRoom> _chatRoomStream;
late Stream<List<ChatMessage>> _messagesStream;
final TextEditingController _messageController = TextEditingController();
@override
void initState() {
super.initState();
_chatRoomStream = _createChatRoomStream();
_messagesStream = _createMessagesStream();
}
Stream<ChatRoom> _createChatRoomStream() async* {
// Simulate real-time chat room status
while (true) {
await Future.delayed(const Duration(seconds: 3));
yield ChatRoom(
id: widget.chatRoomId,
name: 'Team Chat',
onlineCount: 5 + (DateTime.now().second % 3),
typingUsers: DateTime.now().second % 5 == 0 ? ['Alice'] : [],
lastActivity: DateTime.now(),
);
}
}
Stream<List<ChatMessage>> _createMessagesStream() async* {
// Simulate incoming messages
final messages = <ChatMessage>[];
while (true) {
await Future.delayed(const Duration(seconds: 5));
messages.add(ChatMessage(
id: DateTime.now().millisecondsSinceEpoch.toString(),
sender: 'Alice',
content: 'Hello! This is message #${messages.length + 1}',
timestamp: DateTime.now(),
isRead: false,
));
yield List.from(messages);
}
}
@override
void dispose() {
_messageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: StreamBuilder<ChatRoom>(
stream: _chatRoomStream,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Text(l10n.chatTitle);
}
final room = snapshot.data!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(room.name),
Text(
l10n.onlineCount(room.onlineCount),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.white70,
),
),
],
);
},
),
),
body: Column(
children: [
// Typing indicator
StreamBuilder<ChatRoom>(
stream: _chatRoomStream,
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data!.typingUsers.isEmpty) {
return const SizedBox.shrink();
}
return _buildTypingIndicator(l10n, snapshot.data!.typingUsers);
},
),
// Messages list
Expanded(
child: StreamBuilder<List<ChatMessage>>(
stream: _messagesStream,
builder: (context, snapshot) {
return _buildMessagesList(l10n, snapshot);
},
),
),
// Message input
_buildMessageInput(l10n),
],
),
);
}
Widget _buildTypingIndicator(AppLocalizations l10n, List<String> typingUsers) {
final text = switch (typingUsers.length) {
1 => l10n.userTyping(typingUsers.first),
2 => l10n.twoUsersTyping(typingUsers[0], typingUsers[1]),
_ => l10n.multipleUsersTyping(typingUsers.length),
};
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
color: Theme.of(context).colorScheme.surfaceVariant,
child: Row(
children: [
_buildTypingDots(),
const SizedBox(width: 8),
Text(
text,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
);
}
Widget _buildTypingDots() {
return Row(
children: List.generate(3, (index) {
return TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: 1),
duration: Duration(milliseconds: 300 + (index * 150)),
builder: (context, value, child) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 2),
width: 6,
height: 6,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey.withOpacity(0.3 + (value * 0.7)),
),
);
},
);
}),
);
}
Widget _buildMessagesList(
AppLocalizations l10n,
AsyncSnapshot<List<ChatMessage>> snapshot,
) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(l10n.loadingMessages),
],
),
);
}
if (snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 48,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(height: 16),
Text(l10n.errorLoadingMessages),
const SizedBox(height: 8),
TextButton(
onPressed: () {
// Reconnect logic
},
child: Text(l10n.reconnect),
),
],
),
);
}
final messages = snapshot.data ?? [];
if (messages.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.chat_bubble_outline,
size: 64,
color: Theme.of(context).colorScheme.outline,
),
const SizedBox(height: 16),
Text(l10n.noMessages),
const SizedBox(height: 8),
Text(
l10n.startConversation,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
);
}
return ListView.builder(
reverse: true,
itemCount: messages.length,
itemBuilder: (context, index) {
final message = messages[messages.length - 1 - index];
return _buildMessageBubble(l10n, message);
},
);
}
Widget _buildMessageBubble(AppLocalizations l10n, ChatMessage message) {
final isMe = message.sender == 'Me';
return Align(
alignment: isMe ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
padding: const EdgeInsets.all(12),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.75,
),
decoration: BoxDecoration(
color: isMe
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isMe)
Text(
message.sender,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
message.content,
style: TextStyle(
color: isMe ? Colors.white : null,
),
),
const SizedBox(height: 4),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_formatMessageTime(l10n, message.timestamp),
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: isMe ? Colors.white70 : Colors.grey,
),
),
if (isMe) ...[
const SizedBox(width: 4),
Icon(
message.isRead ? Icons.done_all : Icons.done,
size: 14,
color: message.isRead ? Colors.blue : Colors.white70,
),
],
],
),
],
),
),
);
}
String _formatMessageTime(AppLocalizations l10n, DateTime timestamp) {
final now = DateTime.now();
final difference = now.difference(timestamp);
if (difference.inMinutes < 1) {
return l10n.justNow;
} else if (difference.inHours < 1) {
return l10n.minutesAgo(difference.inMinutes);
} else if (difference.inDays < 1) {
return l10n.hoursAgo(difference.inHours);
} else if (difference.inDays == 1) {
return l10n.yesterday;
}
return l10n.daysAgo(difference.inDays);
}
Widget _buildMessageInput(AppLocalizations l10n) {
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border(
top: BorderSide(
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
),
),
),
child: Row(
children: [
Expanded(
child: TextField(
controller: _messageController,
decoration: InputDecoration(
hintText: l10n.typeMessage,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
),
textInputAction: TextInputAction.send,
onSubmitted: (_) => _sendMessage(),
),
),
const SizedBox(width: 8),
IconButton(
onPressed: _sendMessage,
icon: const Icon(Icons.send),
tooltip: l10n.sendMessage,
),
],
),
);
}
void _sendMessage() {
if (_messageController.text.trim().isEmpty) return;
// Send message logic
_messageController.clear();
}
}
class ChatRoom {
final String id;
final String name;
final int onlineCount;
final List<String> typingUsers;
final DateTime lastActivity;
ChatRoom({
required this.id,
required this.name,
required this.onlineCount,
required this.typingUsers,
required this.lastActivity,
});
}
class ChatMessage {
final String id;
final String sender;
final String content;
final DateTime timestamp;
final bool isRead;
ChatMessage({
required this.id,
required this.sender,
required this.content,
required this.timestamp,
required this.isRead,
});
}
Live Counter with Localized Numbers
Display real-time counters with proper number formatting:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:intl/intl.dart';
class LocalizedLiveCounter extends StatefulWidget {
final Stream<int> viewerCountStream;
final Stream<int> likesStream;
final Stream<int> commentsStream;
const LocalizedLiveCounter({
super.key,
required this.viewerCountStream,
required this.likesStream,
required this.commentsStream,
});
@override
State<LocalizedLiveCounter> createState() => _LocalizedLiveCounterState();
}
class _LocalizedLiveCounterState extends State<LocalizedLiveCounter> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final locale = Localizations.localeOf(context);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Live viewers
StreamBuilder<int>(
stream: widget.viewerCountStream,
builder: (context, snapshot) {
return _buildCounter(
l10n: l10n,
locale: locale,
icon: Icons.visibility,
count: snapshot.data,
labelBuilder: (count) => l10n.viewersCount(count),
isLive: true,
);
},
),
// Likes
StreamBuilder<int>(
stream: widget.likesStream,
builder: (context, snapshot) {
return _buildCounter(
l10n: l10n,
locale: locale,
icon: Icons.favorite,
count: snapshot.data,
labelBuilder: (count) => l10n.likesCount(count),
color: Colors.red,
);
},
),
// Comments
StreamBuilder<int>(
stream: widget.commentsStream,
builder: (context, snapshot) {
return _buildCounter(
l10n: l10n,
locale: locale,
icon: Icons.comment,
count: snapshot.data,
labelBuilder: (count) => l10n.commentsCount(count),
);
},
),
],
);
}
Widget _buildCounter({
required AppLocalizations l10n,
required Locale locale,
required IconData icon,
required int? count,
required String Function(int) labelBuilder,
bool isLive = false,
Color? color,
}) {
final numberFormat = NumberFormat.compact(locale: locale.toString());
return Semantics(
liveRegion: isLive,
label: count != null ? labelBuilder(count) : l10n.loading,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isLive)
Container(
width: 8,
height: 8,
margin: const EdgeInsets.only(right: 4),
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.red,
),
),
Icon(icon, size: 20, color: color),
],
),
const SizedBox(height: 4),
if (count == null)
const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
else
Text(
_formatCount(numberFormat, count),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
_getCountLabel(l10n, count ?? 0, isLive),
style: Theme.of(context).textTheme.bodySmall,
),
],
),
);
}
String _formatCount(NumberFormat formatter, int count) {
if (count >= 1000000) {
return formatter.format(count);
} else if (count >= 1000) {
return formatter.format(count);
}
return count.toString();
}
String _getCountLabel(AppLocalizations l10n, int count, bool isLive) {
if (isLive) {
return l10n.watching;
}
return '';
}
}
Connection Status Manager
Manage and display connection status with reconnection:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
enum ConnectionStatus {
connected,
connecting,
reconnecting,
disconnected,
error,
}
class LocalizedConnectionStatus extends StatefulWidget {
final Stream<ConnectionStatus> connectionStream;
final VoidCallback onReconnect;
const LocalizedConnectionStatus({
super.key,
required this.connectionStream,
required this.onReconnect,
});
@override
State<LocalizedConnectionStatus> createState() =>
_LocalizedConnectionStatusState();
}
class _LocalizedConnectionStatusState extends State<LocalizedConnectionStatus>
with SingleTickerProviderStateMixin {
late AnimationController _reconnectAnimController;
@override
void initState() {
super.initState();
_reconnectAnimController = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
);
}
@override
void dispose() {
_reconnectAnimController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return StreamBuilder<ConnectionStatus>(
stream: widget.connectionStream,
builder: (context, snapshot) {
final status = snapshot.data ?? ConnectionStatus.connecting;
// Manage animation
if (status == ConnectionStatus.reconnecting) {
_reconnectAnimController.repeat();
} else {
_reconnectAnimController.stop();
}
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
color: _getStatusColor(status).withOpacity(0.1),
child: Row(
children: [
_buildStatusIcon(status),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
_getStatusTitle(l10n, status),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
color: _getStatusColor(status),
),
),
Text(
_getStatusDescription(l10n, status),
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
if (status == ConnectionStatus.disconnected ||
status == ConnectionStatus.error)
TextButton(
onPressed: widget.onReconnect,
child: Text(l10n.reconnect),
),
],
),
);
},
);
}
Widget _buildStatusIcon(ConnectionStatus status) {
return switch (status) {
ConnectionStatus.connected => const Icon(
Icons.cloud_done,
color: Colors.green,
),
ConnectionStatus.connecting => const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
),
ConnectionStatus.reconnecting => RotationTransition(
turns: _reconnectAnimController,
child: const Icon(Icons.sync, color: Colors.orange),
),
ConnectionStatus.disconnected => const Icon(
Icons.cloud_off,
color: Colors.grey,
),
ConnectionStatus.error => const Icon(
Icons.error_outline,
color: Colors.red,
),
};
}
Color _getStatusColor(ConnectionStatus status) {
return switch (status) {
ConnectionStatus.connected => Colors.green,
ConnectionStatus.connecting => Colors.blue,
ConnectionStatus.reconnecting => Colors.orange,
ConnectionStatus.disconnected => Colors.grey,
ConnectionStatus.error => Colors.red,
};
}
String _getStatusTitle(AppLocalizations l10n, ConnectionStatus status) {
return switch (status) {
ConnectionStatus.connected => l10n.connectionConnected,
ConnectionStatus.connecting => l10n.connectionConnecting,
ConnectionStatus.reconnecting => l10n.connectionReconnecting,
ConnectionStatus.disconnected => l10n.connectionDisconnected,
ConnectionStatus.error => l10n.connectionError,
};
}
String _getStatusDescription(AppLocalizations l10n, ConnectionStatus status) {
return switch (status) {
ConnectionStatus.connected => l10n.connectionConnectedDesc,
ConnectionStatus.connecting => l10n.connectionConnectingDesc,
ConnectionStatus.reconnecting => l10n.connectionReconnectingDesc,
ConnectionStatus.disconnected => l10n.connectionDisconnectedDesc,
ConnectionStatus.error => l10n.connectionErrorDesc,
};
}
}
Notification Stream with Localized Messages
Handle real-time notifications with localized content:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedNotificationStream extends StatefulWidget {
final Stream<AppNotification> notificationStream;
const LocalizedNotificationStream({
super.key,
required this.notificationStream,
});
@override
State<LocalizedNotificationStream> createState() =>
_LocalizedNotificationStreamState();
}
class _LocalizedNotificationStreamState
extends State<LocalizedNotificationStream> {
final List<AppNotification> _notifications = [];
@override
void initState() {
super.initState();
widget.notificationStream.listen((notification) {
setState(() {
_notifications.insert(0, notification);
});
// Show snackbar for new notifications
_showNotificationSnackbar(notification);
});
}
void _showNotificationSnackbar(AppNotification notification) {
final l10n = AppLocalizations.of(context)!;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(_getLocalizedNotificationText(l10n, notification)),
action: SnackBarAction(
label: l10n.view,
onPressed: () {
// Navigate to notification
},
),
duration: const Duration(seconds: 3),
),
);
}
String _getLocalizedNotificationText(
AppLocalizations l10n,
AppNotification notification,
) {
return switch (notification.type) {
NotificationType.like => l10n.notificationLike(notification.actor),
NotificationType.comment => l10n.notificationComment(notification.actor),
NotificationType.follow => l10n.notificationFollow(notification.actor),
NotificationType.mention => l10n.notificationMention(notification.actor),
NotificationType.share => l10n.notificationShare(notification.actor),
};
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.notificationsTitle),
actions: [
StreamBuilder<int>(
stream: _getUnreadCountStream(),
builder: (context, snapshot) {
final count = snapshot.data ?? 0;
if (count == 0) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.only(right: 16),
child: Center(
child: Badge(
label: Text(count.toString()),
child: Text(l10n.unreadCount(count)),
),
),
);
},
),
],
),
body: _notifications.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.notifications_none,
size: 64,
color: Theme.of(context).colorScheme.outline,
),
const SizedBox(height: 16),
Text(l10n.noNotifications),
],
),
)
: ListView.builder(
itemCount: _notifications.length,
itemBuilder: (context, index) {
return _buildNotificationTile(l10n, _notifications[index]);
},
),
);
}
Stream<int> _getUnreadCountStream() async* {
while (true) {
await Future.delayed(const Duration(seconds: 1));
yield _notifications.where((n) => !n.isRead).length;
}
}
Widget _buildNotificationTile(
AppLocalizations l10n,
AppNotification notification,
) {
return ListTile(
leading: CircleAvatar(
child: Icon(_getNotificationIcon(notification.type)),
),
title: Text(_getLocalizedNotificationText(l10n, notification)),
subtitle: Text(_formatNotificationTime(l10n, notification.timestamp)),
trailing: notification.isRead
? null
: Container(
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.primary,
),
),
tileColor: notification.isRead
? null
: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3),
onTap: () {
setState(() {
notification.isRead = true;
});
},
);
}
IconData _getNotificationIcon(NotificationType type) {
return switch (type) {
NotificationType.like => Icons.favorite,
NotificationType.comment => Icons.comment,
NotificationType.follow => Icons.person_add,
NotificationType.mention => Icons.alternate_email,
NotificationType.share => Icons.share,
};
}
String _formatNotificationTime(AppLocalizations l10n, DateTime timestamp) {
final difference = DateTime.now().difference(timestamp);
if (difference.inMinutes < 1) {
return l10n.justNow;
} else if (difference.inHours < 1) {
return l10n.minutesAgo(difference.inMinutes);
} else if (difference.inDays < 1) {
return l10n.hoursAgo(difference.inHours);
}
return l10n.daysAgo(difference.inDays);
}
}
enum NotificationType { like, comment, follow, mention, share }
class AppNotification {
final String id;
final NotificationType type;
final String actor;
final DateTime timestamp;
bool isRead;
AppNotification({
required this.id,
required this.type,
required this.actor,
required this.timestamp,
this.isRead = false,
});
}
Complete ARB File for StreamBuilder
{
"@@locale": "en",
"chatTitle": "Chat",
"@chatTitle": {
"description": "Chat screen title"
},
"onlineCount": "{count} online",
"@onlineCount": {
"description": "Number of online users",
"placeholders": {
"count": {"type": "int"}
}
},
"userTyping": "{user} is typing...",
"@userTyping": {
"description": "Single user typing indicator",
"placeholders": {
"user": {"type": "String"}
}
},
"twoUsersTyping": "{user1} and {user2} are typing...",
"@twoUsersTyping": {
"description": "Two users typing indicator",
"placeholders": {
"user1": {"type": "String"},
"user2": {"type": "String"}
}
},
"multipleUsersTyping": "{count} people are typing...",
"@multipleUsersTyping": {
"description": "Multiple users typing indicator",
"placeholders": {
"count": {"type": "int"}
}
},
"loadingMessages": "Loading messages...",
"@loadingMessages": {
"description": "Loading messages indicator"
},
"errorLoadingMessages": "Failed to load messages",
"@errorLoadingMessages": {
"description": "Error loading messages"
},
"noMessages": "No messages yet",
"@noMessages": {
"description": "Empty chat state"
},
"startConversation": "Start the conversation!",
"@startConversation": {
"description": "Empty chat prompt"
},
"typeMessage": "Type a message...",
"@typeMessage": {
"description": "Message input hint"
},
"sendMessage": "Send message",
"@sendMessage": {
"description": "Send button tooltip"
},
"yesterday": "Yesterday",
"@yesterday": {
"description": "Yesterday time indicator"
},
"daysAgo": "{days} {days, plural, =1{day} other{days}} ago",
"@daysAgo": {
"description": "Days ago indicator",
"placeholders": {
"days": {"type": "int"}
}
},
"viewersCount": "{count} {count, plural, =1{viewer} other{viewers}}",
"@viewersCount": {
"description": "Live viewer count",
"placeholders": {
"count": {"type": "int"}
}
},
"likesCount": "{count} {count, plural, =1{like} other{likes}}",
"@likesCount": {
"description": "Likes count",
"placeholders": {
"count": {"type": "int"}
}
},
"commentsCount": "{count} {count, plural, =1{comment} other{comments}}",
"@commentsCount": {
"description": "Comments count",
"placeholders": {
"count": {"type": "int"}
}
},
"watching": "watching",
"@watching": {
"description": "Watching label for live viewers"
},
"connectionConnected": "Connected",
"@connectionConnected": {
"description": "Connected status title"
},
"connectionConnecting": "Connecting",
"@connectionConnecting": {
"description": "Connecting status title"
},
"connectionReconnecting": "Reconnecting",
"@connectionReconnecting": {
"description": "Reconnecting status title"
},
"connectionDisconnected": "Disconnected",
"@connectionDisconnected": {
"description": "Disconnected status title"
},
"connectionError": "Connection Error",
"@connectionError": {
"description": "Connection error status title"
},
"connectionConnectedDesc": "You're connected and receiving updates",
"@connectionConnectedDesc": {
"description": "Connected status description"
},
"connectionConnectingDesc": "Establishing connection...",
"@connectionConnectingDesc": {
"description": "Connecting status description"
},
"connectionReconnectingDesc": "Connection lost. Attempting to reconnect...",
"@connectionReconnectingDesc": {
"description": "Reconnecting status description"
},
"connectionDisconnectedDesc": "You're offline. Tap to reconnect.",
"@connectionDisconnectedDesc": {
"description": "Disconnected status description"
},
"connectionErrorDesc": "Unable to connect. Please try again.",
"@connectionErrorDesc": {
"description": "Connection error description"
},
"reconnect": "Reconnect",
"@reconnect": {
"description": "Reconnect button text"
},
"notificationsTitle": "Notifications",
"@notificationsTitle": {
"description": "Notifications screen title"
},
"noNotifications": "No notifications",
"@noNotifications": {
"description": "Empty notifications state"
},
"unreadCount": "{count} unread",
"@unreadCount": {
"description": "Unread notifications count",
"placeholders": {
"count": {"type": "int"}
}
},
"view": "View",
"@view": {
"description": "View action button"
},
"notificationLike": "{user} liked your post",
"@notificationLike": {
"description": "Like notification",
"placeholders": {
"user": {"type": "String"}
}
},
"notificationComment": "{user} commented on your post",
"@notificationComment": {
"description": "Comment notification",
"placeholders": {
"user": {"type": "String"}
}
},
"notificationFollow": "{user} started following you",
"@notificationFollow": {
"description": "Follow notification",
"placeholders": {
"user": {"type": "String"}
}
},
"notificationMention": "{user} mentioned you",
"@notificationMention": {
"description": "Mention notification",
"placeholders": {
"user": {"type": "String"}
}
},
"notificationShare": "{user} shared your post",
"@notificationShare": {
"description": "Share notification",
"placeholders": {
"user": {"type": "String"}
}
},
"loading": "Loading...",
"@loading": {
"description": "Generic loading message"
}
}
Best Practices Summary
- Handle all connection states: None, waiting, active, and done
- Show real-time status indicators: Use colors and icons for connection states
- Localize time-relative strings: "Just now", "5 minutes ago" with proper pluralization
- Use compact number formatting: Display large numbers appropriately per locale
- Announce live updates: Use
Semantics.liveRegionfor accessibility - Provide reconnection options: Let users manually retry failed connections
- Cache stream data: Show last known data while reconnecting
- Localize typing indicators: Handle singular, dual, and plural forms
- Format timestamps per locale: Use
intlpackage for date/time formatting - Test with slow connections: Ensure graceful handling of delays and disconnections
Conclusion
Proper StreamBuilder localization ensures your real-time features provide excellent user experiences across all languages. By handling connection states, typing indicators, live counters, and notifications with localized content, you create apps that feel responsive and native to users worldwide. Remember to test your streams with various network conditions and verify that accessibility announcements work correctly for live updates.
The patterns shown here—connection status managers, live counters, chat interfaces, and notification streams—can be adapted to any Flutter application requiring real-time localization support.