Flutter CupertinoListTile Localization: iOS List Tiles for Multilingual Apps
CupertinoListTile is a Flutter widget that renders an iOS-style list row with a title, optional subtitle, leading widget, trailing widget, and additional info text. In multilingual applications, CupertinoListTile is essential for displaying translated titles and subtitles that match iOS list row conventions, providing localized additional info text aligned to the trailing side, supporting RTL layout mirroring for leading and trailing widget positions, and building accessible list rows with announcements in the active language.
Understanding CupertinoListTile in Localization Context
CupertinoListTile renders a single row in an iOS-style list with standardized spacing and layout. For multilingual apps, this enables:
- Translated title and subtitle text following iOS typography conventions
- Localized additional info text (status, counts, dates) in the trailing area
- RTL-mirrored layouts with leading/trailing widgets swapped for Arabic and Hebrew
- Accessible row descriptions announced by VoiceOver in the active language
Why CupertinoListTile Matters for Multilingual Apps
CupertinoListTile provides:
- iOS consistency: List rows matching native iOS patterns in every language
- Structured content: Title, subtitle, and additional info with proper translated text hierarchy
- Navigation indicators: CupertinoListTileChevron for drill-down navigation across locales
- Platform feel: iOS users expect Cupertino-style list rows regardless of language
Basic CupertinoListTile Implementation
import 'package:flutter/cupertino.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedCupertinoListTileExample extends StatelessWidget {
const LocalizedCupertinoListTileExample({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(l10n.settingsTitle),
),
child: SafeArea(
child: CupertinoListSection.insetGrouped(
children: [
CupertinoListTile(
title: Text(l10n.wifiLabel),
leading: const Icon(CupertinoIcons.wifi),
additionalInfo: Text(l10n.connectedStatus),
trailing: const CupertinoListTileChevron(),
onTap: () {},
),
CupertinoListTile(
title: Text(l10n.bluetoothLabel),
leading: const Icon(CupertinoIcons.bluetooth),
additionalInfo: Text(l10n.onStatus),
trailing: const CupertinoListTileChevron(),
onTap: () {},
),
CupertinoListTile(
title: Text(l10n.airplaneModeLabel),
leading: const Icon(CupertinoIcons.airplane),
trailing: CupertinoSwitch(
value: false,
onChanged: (bool value) {},
),
),
CupertinoListTile(
title: Text(l10n.notificationsLabel),
leading: const Icon(CupertinoIcons.bell),
trailing: const CupertinoListTileChevron(),
onTap: () {},
),
],
),
),
);
}
}
Advanced CupertinoListTile Patterns for Localization
User Profile List
CupertinoListTile for displaying user profile information with localized labels.
class UserProfileListExample extends StatelessWidget {
const UserProfileListExample({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(l10n.profileTitle),
),
child: SafeArea(
child: ListView(
children: [
CupertinoListSection.insetGrouped(
children: [
CupertinoListTile(
title: Text(
l10n.userName,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
subtitle: Text(l10n.userEmail),
leading: Container(
width: 50,
height: 50,
decoration: const BoxDecoration(
color: CupertinoColors.systemBlue,
shape: BoxShape.circle,
),
child: const Icon(
CupertinoIcons.person_fill,
color: CupertinoColors.white,
),
),
trailing: const CupertinoListTileChevron(),
onTap: () {},
),
],
),
CupertinoListSection.insetGrouped(
header: Text(l10n.accountInfoHeader),
children: [
CupertinoListTile(
title: Text(l10n.memberSinceLabel),
additionalInfo: Text(l10n.memberSinceDate),
),
CupertinoListTile(
title: Text(l10n.subscriptionLabel),
additionalInfo: Text(l10n.premiumPlan),
trailing: const CupertinoListTileChevron(),
onTap: () {},
),
CupertinoListTile(
title: Text(l10n.storageUsedLabel),
additionalInfo: Text(l10n.storageAmount),
trailing: const CupertinoListTileChevron(),
onTap: () {},
),
],
),
CupertinoListSection.insetGrouped(
header: Text(l10n.actionsHeader),
children: [
CupertinoListTile(
title: Text(l10n.editProfileLabel),
leading: const Icon(CupertinoIcons.pencil),
trailing: const CupertinoListTileChevron(),
onTap: () {},
),
CupertinoListTile(
title: Text(l10n.changePasswordLabel),
leading: const Icon(CupertinoIcons.lock),
trailing: const CupertinoListTileChevron(),
onTap: () {},
),
CupertinoListTile(
title: Text(
l10n.signOutLabel,
style: const TextStyle(
color: CupertinoColors.destructiveRed,
),
),
leading: const Icon(
CupertinoIcons.square_arrow_right,
color: CupertinoColors.destructiveRed,
),
onTap: () {},
),
],
),
],
),
),
);
}
}
Notification Settings List
CupertinoListTile for a notification settings screen with localized toggle labels.
class NotificationSettingsExample extends StatefulWidget {
const NotificationSettingsExample({super.key});
@override
State<NotificationSettingsExample> createState() =>
_NotificationSettingsExampleState();
}
class _NotificationSettingsExampleState
extends State<NotificationSettingsExample> {
bool _messages = true;
bool _mentions = true;
bool _likes = false;
bool _comments = true;
bool _follows = false;
bool _promotions = false;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(l10n.notificationSettingsTitle),
),
child: SafeArea(
child: ListView(
children: [
CupertinoListSection.insetGrouped(
header: Text(l10n.socialHeader),
footer: Text(l10n.socialFooter),
children: [
CupertinoListTile(
title: Text(l10n.directMessagesLabel),
subtitle: Text(l10n.directMessagesDescription),
trailing: CupertinoSwitch(
value: _messages,
onChanged: (value) => setState(() => _messages = value),
),
),
CupertinoListTile(
title: Text(l10n.mentionsLabel),
subtitle: Text(l10n.mentionsDescription),
trailing: CupertinoSwitch(
value: _mentions,
onChanged: (value) => setState(() => _mentions = value),
),
),
CupertinoListTile(
title: Text(l10n.likesLabel),
trailing: CupertinoSwitch(
value: _likes,
onChanged: (value) => setState(() => _likes = value),
),
),
CupertinoListTile(
title: Text(l10n.commentsLabel),
trailing: CupertinoSwitch(
value: _comments,
onChanged: (value) => setState(() => _comments = value),
),
),
CupertinoListTile(
title: Text(l10n.newFollowersLabel),
trailing: CupertinoSwitch(
value: _follows,
onChanged: (value) => setState(() => _follows = value),
),
),
],
),
CupertinoListSection.insetGrouped(
header: Text(l10n.marketingHeader),
footer: Text(l10n.marketingFooter),
children: [
CupertinoListTile(
title: Text(l10n.promotionalEmailsLabel),
subtitle: Text(l10n.promotionalEmailsDescription),
trailing: CupertinoSwitch(
value: _promotions,
onChanged: (value) => setState(() => _promotions = value),
),
),
],
),
],
),
),
);
}
}
File Manager List
CupertinoListTile for a file manager with localized file type labels and metadata.
class FileManagerExample extends StatelessWidget {
const FileManagerExample({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(l10n.filesTitle),
trailing: CupertinoButton(
padding: EdgeInsets.zero,
child: Text(l10n.editButton),
onPressed: () {},
),
),
child: SafeArea(
child: ListView(
children: [
CupertinoListSection.insetGrouped(
header: Text(l10n.recentFilesHeader),
children: [
CupertinoListTile(
title: Text(l10n.documentFileName),
subtitle: Text(l10n.fileModifiedDate),
leading: const Icon(
CupertinoIcons.doc_text,
color: CupertinoColors.systemBlue,
),
additionalInfo: Text(l10n.fileSizeKb(245)),
trailing: const CupertinoListTileChevron(),
onTap: () {},
),
CupertinoListTile(
title: Text(l10n.imageFileName),
subtitle: Text(l10n.fileModifiedDate),
leading: const Icon(
CupertinoIcons.photo,
color: CupertinoColors.systemGreen,
),
additionalInfo: Text(l10n.fileSizeMb(3.2)),
trailing: const CupertinoListTileChevron(),
onTap: () {},
),
CupertinoListTile(
title: Text(l10n.videoFileName),
subtitle: Text(l10n.fileModifiedDate),
leading: const Icon(
CupertinoIcons.film,
color: CupertinoColors.systemRed,
),
additionalInfo: Text(l10n.fileSizeMb(128.5)),
trailing: const CupertinoListTileChevron(),
onTap: () {},
),
],
),
CupertinoListSection.insetGrouped(
header: Text(l10n.foldersHeader),
children: [
CupertinoListTile(
title: Text(l10n.documentsFolder),
leading: const Icon(
CupertinoIcons.folder_fill,
color: CupertinoColors.systemBlue,
),
additionalInfo: Text(l10n.itemCount(24)),
trailing: const CupertinoListTileChevron(),
onTap: () {},
),
CupertinoListTile(
title: Text(l10n.downloadsFolder),
leading: const Icon(
CupertinoIcons.folder_fill,
color: CupertinoColors.systemBlue,
),
additionalInfo: Text(l10n.itemCount(8)),
trailing: const CupertinoListTileChevron(),
onTap: () {},
),
],
),
],
),
),
);
}
}
RTL Support and Bidirectional Layouts
CupertinoListTile automatically mirrors its layout for RTL languages. The leading widget moves to the right, trailing widget to the left, and text aligns from right to left. The additionalInfo text also repositions correctly.
class BidirectionalListTileExample extends StatelessWidget {
const BidirectionalListTileExample({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(l10n.settingsTitle),
),
child: SafeArea(
child: CupertinoListSection.insetGrouped(
header: Text(l10n.generalHeader),
children: [
CupertinoListTile(
title: Text(l10n.languageLabel),
leading: const Icon(CupertinoIcons.globe),
additionalInfo: Text(l10n.currentLanguage),
trailing: const CupertinoListTileChevron(),
),
CupertinoListTile(
title: Text(l10n.regionLabel),
subtitle: Text(l10n.regionDescription),
leading: const Icon(CupertinoIcons.map),
trailing: const CupertinoListTileChevron(),
),
],
),
),
);
}
}
Testing CupertinoListTile Localization
import 'package:flutter/cupertino.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void main() {
Widget buildTestWidget({Locale locale = const Locale('en')}) {
return CupertinoApp(
locale: locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const LocalizedCupertinoListTileExample(),
);
}
testWidgets('CupertinoListTile renders with title', (tester) async {
await tester.pumpWidget(buildTestWidget());
await tester.pumpAndSettle();
expect(find.byType(CupertinoListTile), findsWidgets);
});
testWidgets('CupertinoListTile works in RTL', (tester) async {
await tester.pumpWidget(buildTestWidget(locale: const Locale('ar')));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
});
testWidgets('CupertinoListTile renders in Chinese', (tester) async {
await tester.pumpWidget(buildTestWidget(locale: const Locale('zh')));
await tester.pumpAndSettle();
expect(find.byType(CupertinoListTile), findsWidgets);
});
}
Best Practices
Use
additionalInfofor localized status text (Connected, On, 42 items) rather than placing it in the subtitle, to match iOS list row conventions.Add
CupertinoListTileChevronas the trailing widget for tiles that navigate to detail pages, providing a consistent drill-down indicator across languages.Keep titles concise — CupertinoListTile titles should be 1-4 words in all supported languages to prevent text overflow in the list row.
Use subtitles sparingly — Only add translated subtitles when extra context is genuinely needed, as they increase row height.
Style destructive actions — For tiles like Sign Out or Delete Account, apply
CupertinoColors.destructiveRedto both the title text and leading icon for clear visual warning across locales.Test leading widget sizes — Ensure custom leading widgets (avatars, icons) maintain consistent dimensions across all locales so row heights remain uniform.
Conclusion
CupertinoListTile provides iOS-style list rows for Flutter apps with built-in support for translated titles, subtitles, and additional info text. For multilingual apps, it automatically mirrors layouts for RTL languages, positions leading and trailing widgets correctly, and renders text according to the active locale. By using additionalInfo for status text, adding chevrons for navigation, and testing across diverse locales, you can build list interfaces that feel native to iOS users in every supported language.