Flutter FractionallySizedBox Localization: Proportional Sizing for Multilingual Apps
FractionallySizedBox sizes its child to a fraction of the available space, making it perfect for creating responsive layouts that adapt to different screen sizes. In multilingual applications, FractionallySizedBox enables proportional sizing that works consistently across languages with different text lengths and text directions. This guide covers comprehensive strategies for using FractionallySizedBox in Flutter localization.
Understanding FractionallySizedBox in Localization
FractionallySizedBox widgets benefit localization for:
- Responsive dialogs: Modal widths that adapt to screen size
- Progress indicators: Bars that fill proportionally with localized labels
- Split layouts: Panels sized as percentages of available space
- RTL-aware positioning: Fractional alignment that respects text direction
- Adaptive containers: Content areas that scale proportionally
- Loading states: Skeleton screens with proportional placeholders
Basic FractionallySizedBox with Localized Content
Start with a simple fractionally sized container:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedFractionallySizedBoxDemo extends StatelessWidget {
const LocalizedFractionallySizedBoxDemo({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(l10n.fractionallySizedBoxTitle)),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.widthDemoLabel,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
// 80% width container
FractionallySizedBox(
widthFactor: 0.8,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Text(
l10n.eightyPercentWidth,
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
),
),
const SizedBox(height: 12),
// 60% width container
FractionallySizedBox(
widthFactor: 0.6,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Text(
l10n.sixtyPercentWidth,
style: TextStyle(
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
),
),
),
const SizedBox(height: 12),
// 40% width container
FractionallySizedBox(
widthFactor: 0.4,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.tertiaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Text(
l10n.fortyPercentWidth,
style: TextStyle(
color: Theme.of(context).colorScheme.onTertiaryContainer,
),
),
),
),
],
),
),
);
}
}
ARB File Structure for FractionallySizedBox
{
"fractionallySizedBoxTitle": "Fractional Sizing Demo",
"@fractionallySizedBoxTitle": {
"description": "Title for fractional sizing demo page"
},
"widthDemoLabel": "Width Fractions",
"eightyPercentWidth": "80% of parent width",
"sixtyPercentWidth": "60% of parent width",
"fortyPercentWidth": "40% of parent width"
}
Progress Bars with Localized Labels
Create progress indicators with fractional fill:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedProgressBars extends StatefulWidget {
const LocalizedProgressBars({super.key});
@override
State<LocalizedProgressBars> createState() => _LocalizedProgressBarsState();
}
class _LocalizedProgressBarsState extends State<LocalizedProgressBars> {
final Map<String, double> _progressValues = {
'storage': 0.75,
'bandwidth': 0.45,
'tasks': 0.90,
};
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
return Scaffold(
appBar: AppBar(title: Text(l10n.usageStatsTitle)),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.resourceUsageLabel,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 24),
// Storage progress
_buildProgressItem(
context,
l10n,
isRtl,
label: l10n.storageLabel,
value: _progressValues['storage']!,
usedLabel: l10n.storageUsed('7.5 GB', '10 GB'),
color: _getProgressColor(_progressValues['storage']!),
),
const SizedBox(height: 20),
// Bandwidth progress
_buildProgressItem(
context,
l10n,
isRtl,
label: l10n.bandwidthLabel,
value: _progressValues['bandwidth']!,
usedLabel: l10n.bandwidthUsed('45 GB', '100 GB'),
color: _getProgressColor(_progressValues['bandwidth']!),
),
const SizedBox(height: 20),
// Tasks progress
_buildProgressItem(
context,
l10n,
isRtl,
label: l10n.tasksLabel,
value: _progressValues['tasks']!,
usedLabel: l10n.tasksCompleted(90, 100),
color: _getProgressColor(_progressValues['tasks']!),
),
],
),
),
);
}
Color _getProgressColor(double value) {
if (value >= 0.9) return Colors.red;
if (value >= 0.7) return Colors.orange;
return Colors.green;
}
Widget _buildProgressItem(
BuildContext context,
AppLocalizations l10n,
bool isRtl, {
required String label,
required double value,
required String usedLabel,
required Color color,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: Theme.of(context).textTheme.titleMedium,
),
Text(
'${(value * 100).toInt()}%',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: color,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
// Progress bar using FractionallySizedBox
Container(
height: 12,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(6),
),
child: Align(
alignment: isRtl ? Alignment.centerRight : Alignment.centerLeft,
child: FractionallySizedBox(
widthFactor: value,
child: Container(
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(6),
),
),
),
),
),
const SizedBox(height: 4),
Text(
usedLabel,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
);
}
}
Split Panel Layouts
Create responsive split layouts:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedSplitLayout extends StatefulWidget {
const LocalizedSplitLayout({super.key});
@override
State<LocalizedSplitLayout> createState() => _LocalizedSplitLayoutState();
}
class _LocalizedSplitLayoutState extends State<LocalizedSplitLayout> {
double _splitRatio = 0.3; // 30% sidebar, 70% content
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
return Scaffold(
appBar: AppBar(
title: Text(l10n.splitLayoutTitle),
actions: [
IconButton(
icon: const Icon(Icons.settings),
onPressed: () => _showSplitSettings(context, l10n),
tooltip: l10n.adjustSplitTooltip,
),
],
),
body: Row(
children: [
// Sidebar - fractional width
FractionallySizedBox(
widthFactor: isRtl ? (1 - _splitRatio) : _splitRatio,
heightFactor: 1,
child: Container(
color: Theme.of(context).colorScheme.surfaceVariant,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Text(
l10n.sidebarTitle,
style: Theme.of(context).textTheme.titleLarge,
),
),
const Divider(height: 1),
Expanded(
child: ListView(
padding: const EdgeInsets.all(8),
children: [
_buildNavItem(context, l10n.navDashboard, Icons.dashboard, true),
_buildNavItem(context, l10n.navProjects, Icons.folder, false),
_buildNavItem(context, l10n.navTeam, Icons.group, false),
_buildNavItem(context, l10n.navReports, Icons.bar_chart, false),
_buildNavItem(context, l10n.navSettings, Icons.settings, false),
],
),
),
],
),
),
),
// Content - remaining space
Expanded(
child: Container(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.mainContentTitle,
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 8),
Text(
l10n.mainContentDescription,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 24),
Text(
l10n.currentSplitLabel('${(_splitRatio * 100).toInt()}%'),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
),
],
),
);
}
Widget _buildNavItem(
BuildContext context,
String label,
IconData icon,
bool isSelected,
) {
return ListTile(
leading: Icon(
icon,
color: isSelected
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurfaceVariant,
),
title: Text(
label,
style: TextStyle(
color: isSelected
? Theme.of(context).colorScheme.primary
: null,
fontWeight: isSelected ? FontWeight.bold : null,
),
),
selected: isSelected,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
onTap: () {},
);
}
void _showSplitSettings(BuildContext context, AppLocalizations l10n) {
showModalBottomSheet(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setModalState) {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.splitSettingsTitle,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
Text(l10n.sidebarWidthLabel),
Slider(
value: _splitRatio,
min: 0.2,
max: 0.5,
divisions: 6,
label: '${(_splitRatio * 100).toInt()}%',
onChanged: (value) {
setModalState(() => _splitRatio = value);
setState(() {});
},
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.closeButton),
),
],
),
],
),
);
},
),
);
}
}
Responsive Dialog Sizing
Create dialogs that adapt to screen size:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedResponsiveDialogs extends StatelessWidget {
const LocalizedResponsiveDialogs({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(l10n.dialogDemoTitle)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => _showSmallDialog(context, l10n),
child: Text(l10n.showSmallDialogButton),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => _showMediumDialog(context, l10n),
child: Text(l10n.showMediumDialogButton),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => _showLargeDialog(context, l10n),
child: Text(l10n.showLargeDialogButton),
),
],
),
),
);
}
void _showSmallDialog(BuildContext context, AppLocalizations l10n) {
showDialog(
context: context,
builder: (context) => Center(
child: FractionallySizedBox(
widthFactor: 0.6,
child: Material(
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.info_outline,
size: 48,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 16),
Text(
l10n.smallDialogTitle,
style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
l10n.smallDialogMessage,
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.okButton),
),
],
),
),
),
),
),
);
}
void _showMediumDialog(BuildContext context, AppLocalizations l10n) {
showDialog(
context: context,
builder: (context) => Center(
child: FractionallySizedBox(
widthFactor: 0.8,
heightFactor: 0.5,
child: Material(
borderRadius: BorderRadius.circular(16),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(16),
),
),
child: Row(
children: [
Expanded(
child: Text(
l10n.mediumDialogTitle,
style: Theme.of(context).textTheme.titleLarge,
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
],
),
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Text(l10n.mediumDialogContent),
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.cancelButton),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.confirmButton),
),
],
),
),
],
),
),
),
),
);
}
void _showLargeDialog(BuildContext context, AppLocalizations l10n) {
showDialog(
context: context,
builder: (context) => Center(
child: FractionallySizedBox(
widthFactor: 0.9,
heightFactor: 0.85,
child: Material(
borderRadius: BorderRadius.circular(16),
child: Column(
children: [
AppBar(
title: Text(l10n.largeDialogTitle),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(16),
),
),
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
),
Expanded(
child: GridView.count(
crossAxisCount: 2,
padding: const EdgeInsets.all(16),
mainAxisSpacing: 12,
crossAxisSpacing: 12,
children: List.generate(8, (index) {
return Card(
child: Center(
child: Text(l10n.gridItem(index + 1)),
),
);
}),
),
),
],
),
),
),
),
);
}
}
Loading Skeleton with Fractional Placeholders
Create skeleton screens:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedSkeletonLoader extends StatefulWidget {
const LocalizedSkeletonLoader({super.key});
@override
State<LocalizedSkeletonLoader> createState() => _LocalizedSkeletonLoaderState();
}
class _LocalizedSkeletonLoaderState extends State<LocalizedSkeletonLoader>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
bool _isLoading = true;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
)..repeat();
// Simulate loading
Future.delayed(const Duration(seconds: 3), () {
if (mounted) setState(() => _isLoading = false);
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.skeletonDemoTitle),
actions: [
IconButton(
icon: Icon(_isLoading ? Icons.stop : Icons.refresh),
onPressed: () {
setState(() => _isLoading = true);
Future.delayed(const Duration(seconds: 3), () {
if (mounted) setState(() => _isLoading = false);
});
},
),
],
),
body: _isLoading
? _buildSkeleton(context)
: _buildContent(context, l10n),
);
}
Widget _buildSkeleton(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
// Header skeleton
_buildSkeletonItem(widthFactor: 0.6, height: 24),
const SizedBox(height: 8),
_buildSkeletonItem(widthFactor: 0.4, height: 16),
const SizedBox(height: 24),
// Card skeletons
...List.generate(3, (index) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
_buildSkeletonItem(
widthFactor: null,
height: 60,
width: 60,
borderRadius: 30,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSkeletonItem(widthFactor: 0.7, height: 16),
const SizedBox(height: 8),
_buildSkeletonItem(widthFactor: 0.5, height: 12),
const SizedBox(height: 4),
_buildSkeletonItem(widthFactor: 0.3, height: 12),
],
),
),
],
),
),
),
);
}),
],
);
}
Widget _buildSkeletonItem({
required double? widthFactor,
required double height,
double? width,
double borderRadius = 4,
}) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return FractionallySizedBox(
widthFactor: widthFactor,
alignment: Directionality.of(context) == TextDirection.rtl
? Alignment.centerRight
: Alignment.centerLeft,
child: Container(
height: height,
width: width,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(borderRadius),
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
Theme.of(context).colorScheme.surfaceVariant,
Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5),
Theme.of(context).colorScheme.surfaceVariant,
],
stops: [
0,
_controller.value,
1,
],
),
),
),
);
},
);
}
Widget _buildContent(BuildContext context, AppLocalizations l10n) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
Text(
l10n.contentLoadedTitle,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
Text(
l10n.contentLoadedSubtitle,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 24),
...List.generate(3, (index) {
return Card(
margin: const EdgeInsets.only(bottom: 16),
child: ListTile(
leading: CircleAvatar(
child: Text('${index + 1}'),
),
title: Text(l10n.loadedItemTitle(index + 1)),
subtitle: Text(l10n.loadedItemSubtitle),
trailing: const Icon(Icons.chevron_right),
),
);
}),
],
);
}
}
Complete ARB File for FractionallySizedBox
{
"@@locale": "en",
"fractionallySizedBoxTitle": "Fractional Sizing Demo",
"widthDemoLabel": "Width Fractions",
"eightyPercentWidth": "80% of parent width",
"sixtyPercentWidth": "60% of parent width",
"fortyPercentWidth": "40% of parent width",
"usageStatsTitle": "Usage Statistics",
"resourceUsageLabel": "Resource Usage",
"storageLabel": "Storage",
"storageUsed": "{used} of {total} used",
"@storageUsed": {
"placeholders": {
"used": {"type": "String"},
"total": {"type": "String"}
}
},
"bandwidthLabel": "Bandwidth",
"bandwidthUsed": "{used} of {total} used",
"@bandwidthUsed": {
"placeholders": {
"used": {"type": "String"},
"total": {"type": "String"}
}
},
"tasksLabel": "Tasks",
"tasksCompleted": "{completed} of {total} completed",
"@tasksCompleted": {
"placeholders": {
"completed": {"type": "int"},
"total": {"type": "int"}
}
},
"splitLayoutTitle": "Split Layout",
"adjustSplitTooltip": "Adjust split",
"sidebarTitle": "Sidebar",
"navDashboard": "Dashboard",
"navProjects": "Projects",
"navTeam": "Team",
"navReports": "Reports",
"navSettings": "Settings",
"mainContentTitle": "Main Content",
"mainContentDescription": "This area takes up the remaining space after the sidebar.",
"currentSplitLabel": "Sidebar width: {percent}",
"@currentSplitLabel": {
"placeholders": {"percent": {"type": "String"}}
},
"splitSettingsTitle": "Split Settings",
"sidebarWidthLabel": "Sidebar Width",
"closeButton": "Close",
"dialogDemoTitle": "Dialog Demo",
"showSmallDialogButton": "Small Dialog (60%)",
"showMediumDialogButton": "Medium Dialog (80%)",
"showLargeDialogButton": "Large Dialog (90%)",
"smallDialogTitle": "Quick Info",
"smallDialogMessage": "This is a compact dialog that takes 60% of the screen width.",
"okButton": "OK",
"mediumDialogTitle": "Details",
"mediumDialogContent": "This dialog provides more space for content. It's useful for forms, settings, or displaying moderate amounts of information that doesn't require full screen.",
"cancelButton": "Cancel",
"confirmButton": "Confirm",
"largeDialogTitle": "Browse Items",
"gridItem": "Item {number}",
"@gridItem": {
"placeholders": {"number": {"type": "int"}}
},
"skeletonDemoTitle": "Loading Skeleton",
"contentLoadedTitle": "Content Loaded",
"contentLoadedSubtitle": "Your data has been successfully loaded",
"loadedItemTitle": "Item {number}",
"@loadedItemTitle": {
"placeholders": {"number": {"type": "int"}}
},
"loadedItemSubtitle": "Tap to view details"
}
Best Practices Summary
- Use for responsive sizing: Let elements scale with available space
- RTL alignment: Set alignment to respect text direction
- Combine with constraints: Add min/max limits when needed
- Progress indicators: Perfect for creating proportional fill effects
- Skeleton screens: Create loading placeholders that match content layout
- Dialog sizing: Make dialogs responsive to screen size
- Split layouts: Create adjustable panel layouts
- Avoid nesting: Don't nest multiple FractionallySizedBox widgets
- Test on different screens: Verify proportions work across device sizes
- Consider accessibility: Ensure content remains readable at all sizes
Conclusion
FractionallySizedBox is invaluable for creating responsive Flutter layouts that adapt proportionally to available space. In multilingual applications, it ensures that UI elements maintain proper proportions regardless of text length variations across languages.
By combining FractionallySizedBox with RTL-aware alignment and proper constraints, you can create layouts that feel natural and well-proportioned for users of all locales and screen sizes.