Flutter AppBar Localization: Navigation Headers for Multilingual Apps
AppBar is a Flutter Material Design widget that displays a toolbar at the top of the screen. In multilingual applications, AppBar provides a consistent navigation header that adapts to different languages, supporting RTL layouts with properly mirrored leading and trailing actions.
Understanding AppBar in Localization Context
AppBar displays a title, leading icon, and action buttons in a toolbar format. For multilingual apps, this enables:
- Direction-aware icon positioning for RTL languages
- Localized titles that adapt to text length
- Properly mirrored navigation icons
- Theme-aware styling across all locales
Why AppBar Matters for Multilingual Apps
AppBar provides:
- RTL mirroring: Back arrows and icons mirror automatically
- Flexible titles: Titles adapt to translated text lengths
- Action alignment: Actions position correctly in both directions
- Consistent branding: Same visual identity across languages
Basic AppBar Implementation
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedAppBarExample extends StatelessWidget {
const LocalizedAppBarExample({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.pageTitle),
actions: [
IconButton(
icon: const Icon(Icons.search),
tooltip: l10n.searchTooltip,
onPressed: () {},
),
IconButton(
icon: const Icon(Icons.notifications),
tooltip: l10n.notificationsTooltip,
onPressed: () {},
),
PopupMenuButton<String>(
tooltip: l10n.moreOptions,
onSelected: (value) {},
itemBuilder: (context) => [
PopupMenuItem(
value: 'settings',
child: Text(l10n.menuSettings),
),
PopupMenuItem(
value: 'help',
child: Text(l10n.menuHelp),
),
PopupMenuItem(
value: 'about',
child: Text(l10n.menuAbout),
),
],
),
],
),
body: Center(
child: Text(l10n.bodyContent),
),
);
}
}
AppBar Variants
Large AppBar with Flexible Space
class LocalizedLargeAppBar extends StatelessWidget {
const LocalizedLargeAppBar({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar.large(
title: Text(l10n.largeTitle),
actions: [
IconButton(
icon: const Icon(Icons.search),
tooltip: l10n.searchTooltip,
onPressed: () {},
),
],
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return ListTile(
title: Text('${l10n.listItem} ${index + 1}'),
);
},
childCount: 30,
),
),
],
),
);
}
}
Collapsing AppBar with Image
class LocalizedCollapsingAppBar extends StatelessWidget {
const LocalizedCollapsingAppBar({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 200,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: Text(l10n.pageTitle),
background: Stack(
fit: StackFit.expand,
children: [
Image.network(
'https://picsum.photos/800/400',
fit: BoxFit.cover,
),
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withOpacity(0.7),
],
),
),
),
],
),
),
actions: [
IconButton(
icon: const Icon(Icons.share),
tooltip: l10n.shareTooltip,
onPressed: () {},
),
],
),
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverList(
delegate: SliverChildListDelegate([
Text(
l10n.sectionTitle,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
Text(l10n.sectionContent),
]),
),
),
],
),
);
}
}
Search AppBar
Localized Search Bar
class LocalizedSearchAppBar extends StatefulWidget {
const LocalizedSearchAppBar({super.key});
@override
State<LocalizedSearchAppBar> createState() => _LocalizedSearchAppBarState();
}
class _LocalizedSearchAppBarState extends State<LocalizedSearchAppBar> {
bool _isSearching = false;
final _searchController = TextEditingController();
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: _isSearching
? TextField(
controller: _searchController,
autofocus: true,
decoration: InputDecoration(
hintText: l10n.searchHint,
border: InputBorder.none,
),
style: const TextStyle(fontSize: 18),
)
: Text(l10n.appTitle),
actions: [
if (_isSearching)
IconButton(
icon: const Icon(Icons.clear),
tooltip: l10n.clearSearch,
onPressed: () {
_searchController.clear();
},
)
else
IconButton(
icon: const Icon(Icons.search),
tooltip: l10n.searchTooltip,
onPressed: () {
setState(() => _isSearching = true);
},
),
],
leading: _isSearching
? IconButton(
icon: const Icon(Icons.arrow_back),
tooltip: l10n.backTooltip,
onPressed: () {
setState(() {
_isSearching = false;
_searchController.clear();
});
},
)
: null,
),
body: Center(
child: _isSearching
? Text(l10n.searchResults)
: Text(l10n.regularContent),
),
);
}
}
Tabbed AppBar
AppBar with Tabs
class LocalizedTabbedAppBar extends StatelessWidget {
const LocalizedTabbedAppBar({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: Text(l10n.appTitle),
bottom: TabBar(
tabs: [
Tab(
icon: const Icon(Icons.article),
text: l10n.tabArticles,
),
Tab(
icon: const Icon(Icons.photo),
text: l10n.tabPhotos,
),
Tab(
icon: const Icon(Icons.video_library),
text: l10n.tabVideos,
),
],
),
),
body: TabBarView(
children: [
Center(child: Text(l10n.articlesContent)),
Center(child: Text(l10n.photosContent)),
Center(child: Text(l10n.videosContent)),
],
),
),
);
}
}
Contextual AppBar
Selection Mode AppBar
class LocalizedContextualAppBar extends StatefulWidget {
const LocalizedContextualAppBar({super.key});
@override
State<LocalizedContextualAppBar> createState() =>
_LocalizedContextualAppBarState();
}
class _LocalizedContextualAppBarState
extends State<LocalizedContextualAppBar> {
final Set<int> _selectedItems = {};
bool get _isSelectionMode => _selectedItems.isNotEmpty;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
leading: _isSelectionMode
? IconButton(
icon: const Icon(Icons.close),
tooltip: l10n.cancelSelection,
onPressed: () {
setState(() => _selectedItems.clear());
},
)
: null,
title: _isSelectionMode
? Text(l10n.selectedCount(_selectedItems.length))
: Text(l10n.appTitle),
backgroundColor: _isSelectionMode
? Theme.of(context).colorScheme.primaryContainer
: null,
actions: _isSelectionMode
? [
IconButton(
icon: const Icon(Icons.share),
tooltip: l10n.shareSelected,
onPressed: () {},
),
IconButton(
icon: const Icon(Icons.delete),
tooltip: l10n.deleteSelected,
onPressed: () {
setState(() => _selectedItems.clear());
},
),
]
: [
IconButton(
icon: const Icon(Icons.search),
tooltip: l10n.searchTooltip,
onPressed: () {},
),
],
),
body: ListView.builder(
itemCount: 20,
itemBuilder: (context, index) {
final isSelected = _selectedItems.contains(index);
return ListTile(
leading: _isSelectionMode
? Checkbox(
value: isSelected,
onChanged: (value) {
setState(() {
if (value == true) {
_selectedItems.add(index);
} else {
_selectedItems.remove(index);
}
});
},
)
: CircleAvatar(child: Text('${index + 1}')),
title: Text('${l10n.itemTitle} ${index + 1}'),
selected: isSelected,
onTap: () {
if (_isSelectionMode) {
setState(() {
if (isSelected) {
_selectedItems.remove(index);
} else {
_selectedItems.add(index);
}
});
}
},
onLongPress: () {
setState(() => _selectedItems.add(index));
},
);
},
),
);
}
}
Custom AppBar Styling
Themed AppBar
class LocalizedThemedAppBar extends StatelessWidget {
const LocalizedThemedAppBar({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.appTitle),
centerTitle: true,
elevation: 0,
backgroundColor: Theme.of(context).colorScheme.surface,
foregroundColor: Theme.of(context).colorScheme.onSurface,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(16),
),
),
actions: [
Container(
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
child: IconButton.filledTonal(
icon: const Icon(Icons.notifications_outlined),
tooltip: l10n.notificationsTooltip,
onPressed: () {},
),
),
Container(
margin: const EdgeInsets.only(right: 8, top: 8, bottom: 8),
child: IconButton.filledTonal(
icon: const Icon(Icons.person_outline),
tooltip: l10n.profileTooltip,
onPressed: () {},
),
),
],
),
body: Center(
child: Text(l10n.bodyContent),
),
);
}
}
Transparent AppBar with Gradient
class LocalizedTransparentAppBar extends StatelessWidget {
const LocalizedTransparentAppBar({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
title: Text(l10n.appTitle),
flexibleSpace: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.7),
Colors.transparent,
],
),
),
),
),
body: Container(
decoration: const BoxDecoration(
image: DecorationImage(
image: NetworkImage('https://picsum.photos/800/1200'),
fit: BoxFit.cover,
),
),
child: SafeArea(
child: Center(
child: Text(
l10n.overlayContent,
style: const TextStyle(
color: Colors.white,
fontSize: 24,
),
),
),
),
),
);
}
}
ARB File Structure
English (app_en.arb)
{
"@@locale": "en",
"appTitle": "My App",
"pageTitle": "Page Title",
"searchTooltip": "Search",
"notificationsTooltip": "Notifications",
"moreOptions": "More options",
"menuSettings": "Settings",
"menuHelp": "Help",
"menuAbout": "About",
"bodyContent": "Main content area",
"largeTitle": "Large Title",
"listItem": "Item",
"shareTooltip": "Share",
"sectionTitle": "Section Title",
"sectionContent": "This is the content of the section.",
"searchHint": "Search...",
"clearSearch": "Clear",
"backTooltip": "Back",
"searchResults": "Search results will appear here",
"regularContent": "Regular content",
"tabArticles": "Articles",
"tabPhotos": "Photos",
"tabVideos": "Videos",
"articlesContent": "Articles content",
"photosContent": "Photos content",
"videosContent": "Videos content",
"cancelSelection": "Cancel selection",
"selectedCount": "{count} selected",
"@selectedCount": {
"placeholders": {
"count": {"type": "int"}
}
},
"shareSelected": "Share selected",
"deleteSelected": "Delete selected",
"itemTitle": "Item",
"profileTooltip": "Profile",
"overlayContent": "Overlay Content"
}
German (app_de.arb)
{
"@@locale": "de",
"appTitle": "Meine App",
"pageTitle": "Seitentitel",
"searchTooltip": "Suchen",
"notificationsTooltip": "Benachrichtigungen",
"moreOptions": "Weitere Optionen",
"menuSettings": "Einstellungen",
"menuHelp": "Hilfe",
"menuAbout": "Über",
"bodyContent": "Hauptinhaltsbereich",
"largeTitle": "Großer Titel",
"listItem": "Element",
"shareTooltip": "Teilen",
"sectionTitle": "Abschnittstitel",
"sectionContent": "Dies ist der Inhalt des Abschnitts.",
"searchHint": "Suchen...",
"clearSearch": "Löschen",
"backTooltip": "Zurück",
"searchResults": "Suchergebnisse erscheinen hier",
"regularContent": "Regulärer Inhalt",
"tabArticles": "Artikel",
"tabPhotos": "Fotos",
"tabVideos": "Videos",
"articlesContent": "Artikelinhalt",
"photosContent": "Fotoinhalt",
"videosContent": "Videoinhalt",
"cancelSelection": "Auswahl abbrechen",
"selectedCount": "{count} ausgewählt",
"shareSelected": "Ausgewählte teilen",
"deleteSelected": "Ausgewählte löschen",
"itemTitle": "Element",
"profileTooltip": "Profil",
"overlayContent": "Overlay-Inhalt"
}
Arabic (app_ar.arb)
{
"@@locale": "ar",
"appTitle": "تطبيقي",
"pageTitle": "عنوان الصفحة",
"searchTooltip": "بحث",
"notificationsTooltip": "الإشعارات",
"moreOptions": "المزيد من الخيارات",
"menuSettings": "الإعدادات",
"menuHelp": "المساعدة",
"menuAbout": "حول",
"bodyContent": "منطقة المحتوى الرئيسية",
"largeTitle": "عنوان كبير",
"listItem": "عنصر",
"shareTooltip": "مشاركة",
"sectionTitle": "عنوان القسم",
"sectionContent": "هذا هو محتوى القسم.",
"searchHint": "بحث...",
"clearSearch": "مسح",
"backTooltip": "رجوع",
"searchResults": "ستظهر نتائج البحث هنا",
"regularContent": "المحتوى العادي",
"tabArticles": "المقالات",
"tabPhotos": "الصور",
"tabVideos": "الفيديوهات",
"articlesContent": "محتوى المقالات",
"photosContent": "محتوى الصور",
"videosContent": "محتوى الفيديوهات",
"cancelSelection": "إلغاء التحديد",
"selectedCount": "{count} محدد",
"shareSelected": "مشاركة المحدد",
"deleteSelected": "حذف المحدد",
"itemTitle": "عنصر",
"profileTooltip": "الملف الشخصي",
"overlayContent": "محتوى التراكب"
}
Best Practices Summary
Do's
- Let AppBar handle RTL icon mirroring automatically
- Use tooltips for all icon buttons
- Localize menu items in PopupMenuButton
- Test title overflow with long translations
- Use SliverAppBar for scrollable headers
Don'ts
- Don't hardcode back arrow icons - use automaticallyImplyLeading
- Don't forget to localize search hints and placeholders
- Don't use fixed widths for title widgets
- Don't skip contextual app bars for selection modes
Conclusion
AppBar is essential for creating consistent navigation headers in multilingual Flutter applications. With automatic RTL support and flexible title and action positioning, AppBar ensures your navigation works correctly across all languages. Use AppBar variants like SliverAppBar and custom styling to create engaging, localized header experiences.