Flutter ListWheelScrollView Localization: Wheel-Style Pickers for Multilingual Apps
ListWheelScrollView is a Flutter widget that displays children on a wheel, creating a 3D barrel-like scrolling effect commonly seen in iOS date pickers. In multilingual applications, ListWheelScrollView is essential for building locale-aware pickers for values like months, cities, and units, displaying translated options in a wheel format, adapting item sizing for different script lengths, and providing accessible wheel selection in the active language.
Understanding ListWheelScrollView in Localization Context
ListWheelScrollView renders children along a cylindrical wheel with perspective effects, where the centered item is the selected value. For multilingual apps, this enables:
- Translated picker options displayed on a scrollable wheel
- Locale-specific value lists (month names, day names, units)
- Adaptive item sizing for translations of varying length
- Accessible selection announcements in the active language
Why ListWheelScrollView Matters for Multilingual Apps
ListWheelScrollView provides:
- Picker UX: Native wheel picker feel for translated option selection
- Perspective effect: 3D cylinder rendering that works with any text direction
- Fixed item extent: Consistent item heights across all translations
- Snap behavior: Selected item snaps to center for clear translated value selection
Basic ListWheelScrollView Implementation
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedListWheelExample extends StatefulWidget {
const LocalizedListWheelExample({super.key});
@override
State<LocalizedListWheelExample> createState() =>
_LocalizedListWheelExampleState();
}
class _LocalizedListWheelExampleState extends State<LocalizedListWheelExample> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final months = [
l10n.january, l10n.february, l10n.march,
l10n.april, l10n.may, l10n.june,
l10n.july, l10n.august, l10n.september,
l10n.october, l10n.november, l10n.december,
];
return Scaffold(
appBar: AppBar(title: Text(l10n.selectMonthTitle)),
body: Column(
children: [
Expanded(
child: ListWheelScrollView(
itemExtent: 50,
diameterRatio: 1.5,
useMagnifier: true,
magnification: 1.2,
onSelectedItemChanged: (index) {
setState(() => _selectedIndex = index);
},
children: months.map((month) {
return Center(
child: Text(
month,
style: Theme.of(context).textTheme.titleMedium,
),
);
}).toList(),
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Text(
l10n.selectedMonthLabel(months[_selectedIndex]),
style: Theme.of(context).textTheme.titleLarge,
),
),
],
),
);
}
}
Advanced ListWheelScrollView Patterns for Localization
Multi-Wheel Date Picker
Multiple synchronized wheels for day, month, and year selection with localized labels.
class LocalizedMultiWheelPicker extends StatefulWidget {
const LocalizedMultiWheelPicker({super.key});
@override
State<LocalizedMultiWheelPicker> createState() =>
_LocalizedMultiWheelPickerState();
}
class _LocalizedMultiWheelPickerState extends State<LocalizedMultiWheelPicker> {
int _selectedDay = 0;
int _selectedMonth = 0;
int _selectedYear = 0;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final months = [
l10n.january, l10n.february, l10n.march,
l10n.april, l10n.may, l10n.june,
l10n.july, l10n.august, l10n.september,
l10n.october, l10n.november, l10n.december,
];
return Scaffold(
appBar: AppBar(title: Text(l10n.selectDateTitle)),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: Column(
children: [
Text(l10n.dayLabel,
style: Theme.of(context).textTheme.labelMedium),
SizedBox(
height: 200,
child: ListWheelScrollView.useDelegate(
itemExtent: 40,
onSelectedItemChanged: (index) {
setState(() => _selectedDay = index);
},
childDelegate: ListWheelChildBuilderDelegate(
builder: (context, index) {
if (index < 0 || index > 30) return null;
return Center(
child: Text('${index + 1}'),
);
},
childCount: 31,
),
),
),
],
),
),
Expanded(
flex: 2,
child: Column(
children: [
Text(l10n.monthLabel,
style: Theme.of(context).textTheme.labelMedium),
SizedBox(
height: 200,
child: ListWheelScrollView(
itemExtent: 40,
onSelectedItemChanged: (index) {
setState(() => _selectedMonth = index);
},
children: months.map((month) {
return Center(child: Text(month));
}).toList(),
),
),
],
),
),
Expanded(
child: Column(
children: [
Text(l10n.yearLabel,
style: Theme.of(context).textTheme.labelMedium),
SizedBox(
height: 200,
child: ListWheelScrollView.useDelegate(
itemExtent: 40,
onSelectedItemChanged: (index) {
setState(() => _selectedYear = index);
},
childDelegate: ListWheelChildBuilderDelegate(
builder: (context, index) {
if (index < 0 || index > 100) return null;
return Center(
child: Text('${2024 + index}'),
);
},
childCount: 101,
),
),
),
],
),
),
],
),
),
Padding(
padding: const EdgeInsets.all(16),
child: FilledButton(
onPressed: () {},
child: Text(l10n.confirmDateButton),
),
),
],
),
);
}
}
Unit Picker with Localized Labels
A wheel picker for selecting measurement units with translated unit names.
class LocalizedUnitPicker extends StatefulWidget {
const LocalizedUnitPicker({super.key});
@override
State<LocalizedUnitPicker> createState() => _LocalizedUnitPickerState();
}
class _LocalizedUnitPickerState extends State<LocalizedUnitPicker> {
int _selectedUnit = 0;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final units = [
(l10n.kilogramsUnit, 'kg'),
(l10n.gramsUnit, 'g'),
(l10n.poundsUnit, 'lb'),
(l10n.ouncesUnit, 'oz'),
(l10n.litersUnit, 'L'),
(l10n.millilitersUnit, 'mL'),
];
return Card(
margin: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
l10n.selectUnitTitle,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
SizedBox(
height: 180,
child: ListWheelScrollView(
itemExtent: 44,
diameterRatio: 1.2,
useMagnifier: true,
magnification: 1.3,
onSelectedItemChanged: (index) {
setState(() => _selectedUnit = index);
},
children: units.map((unit) {
return Center(
child: Text(
'${unit.$1} (${unit.$2})',
style: Theme.of(context).textTheme.bodyLarge,
),
);
}).toList(),
),
),
const Divider(),
Text(
l10n.selectedUnitLabel(units[_selectedUnit].$1),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
);
}
}
Country/City Picker
Wheel picker for locale-specific city or country selection.
class LocalizedCityPicker extends StatefulWidget {
const LocalizedCityPicker({super.key});
@override
State<LocalizedCityPicker> createState() => _LocalizedCityPickerState();
}
class _LocalizedCityPickerState extends State<LocalizedCityPicker> {
int _selectedCity = 0;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final cities = [
l10n.cityNewYork,
l10n.cityLondon,
l10n.cityTokyo,
l10n.cityParis,
l10n.cityDubai,
l10n.cityBerlin,
l10n.citySydney,
];
return Column(
children: [
Text(
l10n.selectCityTitle,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
SizedBox(
height: 200,
child: ListWheelScrollView(
itemExtent: 48,
useMagnifier: true,
magnification: 1.2,
onSelectedItemChanged: (index) {
setState(() => _selectedCity = index);
},
children: cities.map((city) {
return Center(
child: Text(
city,
style: Theme.of(context).textTheme.titleSmall,
),
);
}).toList(),
),
),
],
);
}
}
RTL Support and Bidirectional Layouts
ListWheelScrollView content automatically adapts to RTL. Text within wheel items aligns correctly, and surrounding labels follow the active directionality.
class BidirectionalListWheel extends StatelessWidget {
const BidirectionalListWheel({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Padding(
padding: const EdgeInsetsDirectional.all(16),
child: Column(
children: [
Text(
l10n.selectValueTitle,
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.start,
),
SizedBox(
height: 200,
child: ListWheelScrollView(
itemExtent: 44,
children: List.generate(10, (index) {
return Center(
child: Text(
'${l10n.optionLabel} ${index + 1}',
style: Theme.of(context).textTheme.bodyLarge,
),
);
}),
),
),
],
),
);
}
}
Testing ListWheelScrollView Localization
import 'package:flutter/material.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 MaterialApp(
locale: locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const LocalizedListWheelExample(),
);
}
testWidgets('ListWheelScrollView renders localized items', (tester) async {
await tester.pumpWidget(buildTestWidget());
await tester.pumpAndSettle();
expect(find.byType(ListWheelScrollView), findsOneWidget);
});
testWidgets('ListWheelScrollView works in RTL', (tester) async {
await tester.pumpWidget(buildTestWidget(locale: const Locale('ar')));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
});
}
Best Practices
Use
useMagnifier: trueto enlarge the selected translated item, making it clearly readable regardless of script size.Set
itemExtentto accommodate the tallest translation -- test with verbose languages to ensure no clipping occurs.Use
ListWheelChildBuilderDelegatefor large value ranges (years, numbers) to lazily build only visible items.Provide a translated label above and below the wheel showing the current selection for accessibility and clarity.
Localize month and day names using ARB strings rather than hardcoded English names for wheel pickers.
Test wheel scrolling in RTL to verify text alignment and selection feedback display correctly.
Conclusion
ListWheelScrollView provides a wheel-style picker experience in Flutter, ideal for selecting values from localized lists like months, units, and cities. For multilingual apps, it handles translated options of varying lengths with consistent item extents, magnified selection for readability, and lazy building for large value ranges. By combining wheel pickers with translated labels, multi-wheel layouts, and locale-specific value lists, you can build picker experiences that feel native and intuitive in every supported language.