Flutter FractionallySizedBox Localization: Proportional Sizing for Global Apps
FractionallySizedBox sizes its child to a fraction of the total available space. In multilingual applications, this widget enables responsive layouts that adapt gracefully to varying content lengths while maintaining proportional relationships.
Understanding FractionallySizedBox in Localization Context
FractionallySizedBox sizes its child based on a fraction of available space rather than fixed dimensions. For multilingual apps, this creates powerful capabilities:
- Content areas can grow proportionally with screen size
- Text containers adapt to different language lengths
- RTL layouts maintain proper proportional spacing
- Responsive designs work across all locales
Why FractionallySizedBox Matters for Multilingual Apps
Proportional sizing ensures:
- Flexible layouts: Containers adapt to available space
- Consistent proportions: Visual balance across screen sizes
- Language accommodation: More verbose languages have room to breathe
- Responsive design: Layouts scale proportionally
Basic FractionallySizedBox Implementation
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedFractionallySizedExample extends StatelessWidget {
const LocalizedFractionallySizedExample({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
body: Center(
child: FractionallySizedBox(
widthFactor: 0.8, // 80% of parent width
child: Card(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
l10n.welcomeTitle,
style: Theme.of(context).textTheme.headlineMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Text(
l10n.welcomeDescription,
textAlign: TextAlign.center,
),
],
),
),
),
),
),
);
}
}
Language-Aware Width Factors
Adaptive Width for Text Content
class AdaptiveWidthContainer extends StatelessWidget {
final Widget child;
final double baseWidthFactor;
const AdaptiveWidthContainer({
super.key,
required this.child,
this.baseWidthFactor = 0.8,
});
@override
Widget build(BuildContext context) {
final locale = Localizations.localeOf(context);
final adjustedWidth = _getAdjustedWidthFactor(locale);
return FractionallySizedBox(
widthFactor: adjustedWidth,
child: child,
);
}
double _getAdjustedWidthFactor(Locale locale) {
switch (locale.languageCode) {
case 'de': // German - typically longer
case 'ru': // Russian
case 'fi': // Finnish
case 'nl': // Dutch
return (baseWidthFactor + 0.1).clamp(0.0, 1.0);
case 'ja': // Japanese - more compact
case 'zh': // Chinese
case 'ko': // Korean
return (baseWidthFactor - 0.05).clamp(0.0, 1.0);
default:
return baseWidthFactor;
}
}
}
// Usage
class LocalizedContentExample extends StatelessWidget {
const LocalizedContentExample({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return AdaptiveWidthContainer(
baseWidthFactor: 0.75,
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(l10n.longDescription),
),
),
);
}
}
Modal and Dialog Layouts
Localized Dialog with Fractional Width
class LocalizedFractionalDialog extends StatelessWidget {
final String title;
final String content;
final List<Widget> actions;
const LocalizedFractionalDialog({
super.key,
required this.title,
required this.content,
required this.actions,
});
@override
Widget build(BuildContext context) {
return Dialog(
child: FractionallySizedBox(
widthFactor: 0.85, // 85% of screen width
child: Container(
constraints: const BoxConstraints(maxWidth: 400),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 16),
Text(
content,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: actions,
),
],
),
),
),
),
);
}
static Future<T?> show<T>(
BuildContext context, {
required String title,
required String content,
required List<Widget> actions,
}) {
return showDialog<T>(
context: context,
builder: (context) => LocalizedFractionalDialog(
title: title,
content: content,
actions: actions,
),
);
}
}
Responsive Bottom Sheet
class LocalizedBottomSheet extends StatelessWidget {
final String title;
final Widget content;
const LocalizedBottomSheet({
super.key,
required this.title,
required this.content,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Drag handle
FractionallySizedBox(
widthFactor: 0.1,
child: Container(
margin: const EdgeInsets.symmetric(vertical: 12),
height: 4,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.outlineVariant,
borderRadius: BorderRadius.circular(2),
),
),
),
// Title
FractionallySizedBox(
widthFactor: 0.9,
child: Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Text(
title,
style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center,
),
),
),
// Content
FractionallySizedBox(
widthFactor: 0.9,
child: content,
),
const SizedBox(height: 24),
],
),
);
}
}
Form Layouts with Fractional Sizing
Responsive Form Container
class LocalizedFormContainer extends StatelessWidget {
const LocalizedFormContainer({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Center(
child: FractionallySizedBox(
widthFactor: 0.9,
child: Container(
constraints: const BoxConstraints(maxWidth: 500),
child: Card(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
l10n.loginTitle,
style: Theme.of(context).textTheme.headlineMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
l10n.loginSubtitle,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.outline,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
_LocalizedTextField(
label: l10n.emailLabel,
hint: l10n.emailHint,
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 16),
_LocalizedTextField(
label: l10n.passwordLabel,
hint: l10n.passwordHint,
obscureText: true,
),
const SizedBox(height: 24),
FilledButton(
onPressed: () {},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Text(l10n.loginButton),
),
),
],
),
),
),
),
),
);
}
}
class _LocalizedTextField extends StatelessWidget {
final String label;
final String hint;
final TextInputType? keyboardType;
final bool obscureText;
const _LocalizedTextField({
required this.label,
required this.hint,
this.keyboardType,
this.obscureText = false,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsetsDirectional.only(start: 4, bottom: 8),
child: Text(
label,
style: Theme.of(context).textTheme.labelLarge,
),
),
TextField(
keyboardType: keyboardType,
obscureText: obscureText,
decoration: InputDecoration(
hintText: hint,
border: const OutlineInputBorder(),
),
),
],
);
}
}
Progress and Loading Indicators
Localized Progress Bar
class LocalizedProgressBar extends StatelessWidget {
final double progress;
final String label;
const LocalizedProgressBar({
super.key,
required this.progress,
required this.label,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: Theme.of(context).textTheme.bodyMedium,
),
Text(
'${(progress * 100).toInt()}%',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
Container(
height: 8,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(4),
),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: FractionallySizedBox(
widthFactor: progress.clamp(0.0, 1.0),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(4),
),
),
),
),
),
],
);
}
}
// Usage
class DownloadProgressExample extends StatelessWidget {
const DownloadProgressExample({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
LocalizedProgressBar(
progress: 0.65,
label: l10n.downloadProgress,
),
const SizedBox(height: 24),
LocalizedProgressBar(
progress: 0.30,
label: l10n.uploadProgress,
),
],
),
);
}
}
Split View Layouts
Localized Split Panel
class LocalizedSplitPanel extends StatelessWidget {
final Widget leftPanel;
final Widget rightPanel;
final double leftFactor;
const LocalizedSplitPanel({
super.key,
required this.leftPanel,
required this.rightPanel,
this.leftFactor = 0.3,
});
@override
Widget build(BuildContext context) {
final isRtl = Directionality.of(context) == TextDirection.rtl;
return Row(
children: [
FractionallySizedBox(
widthFactor: leftFactor,
child: isRtl ? rightPanel : leftPanel,
),
Expanded(
child: isRtl ? leftPanel : rightPanel,
),
],
);
}
}
// Adaptive split panel that adjusts to language
class AdaptiveSplitPanel extends StatelessWidget {
final Widget navigationPanel;
final Widget contentPanel;
const AdaptiveSplitPanel({
super.key,
required this.navigationPanel,
required this.contentPanel,
});
@override
Widget build(BuildContext context) {
final locale = Localizations.localeOf(context);
final navWidth = _getNavigationWidth(locale);
return Row(
children: [
SizedBox(
width: navWidth,
child: navigationPanel,
),
Expanded(child: contentPanel),
],
);
}
double _getNavigationWidth(Locale locale) {
// Adjust navigation width for verbose languages
switch (locale.languageCode) {
case 'de':
case 'ru':
case 'fi':
return 280; // Wider for longer menu items
case 'ja':
case 'zh':
return 200; // Narrower for compact text
default:
return 240;
}
}
}
Onboarding Screens
Localized Onboarding Page
class LocalizedOnboardingPage extends StatelessWidget {
final String imageAsset;
final String title;
final String description;
const LocalizedOnboardingPage({
super.key,
required this.imageAsset,
required this.title,
required this.description,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
Expanded(
flex: 3,
child: FractionallySizedBox(
widthFactor: 0.8,
child: Image.asset(
imageAsset,
fit: BoxFit.contain,
),
),
),
Expanded(
flex: 2,
child: FractionallySizedBox(
widthFactor: 0.9,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
title,
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Text(
description,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.outline,
),
textAlign: TextAlign.center,
),
],
),
),
),
],
),
);
}
}
ARB File Structure
English (app_en.arb)
{
"@@locale": "en",
"welcomeTitle": "Welcome",
"@welcomeTitle": {
"description": "Welcome screen title"
},
"welcomeDescription": "Discover amazing features and start your journey with our app today.",
"@welcomeDescription": {
"description": "Welcome screen description"
},
"longDescription": "This comprehensive guide will help you understand all the features and capabilities available to you as a user of our application.",
"@longDescription": {
"description": "Long content description"
},
"loginTitle": "Welcome Back",
"loginSubtitle": "Sign in to continue",
"emailLabel": "Email",
"emailHint": "Enter your email",
"passwordLabel": "Password",
"passwordHint": "Enter your password",
"loginButton": "Sign In",
"downloadProgress": "Downloading...",
"uploadProgress": "Uploading..."
}
German (app_de.arb)
{
"@@locale": "de",
"welcomeTitle": "Willkommen",
"welcomeDescription": "Entdecken Sie erstaunliche Funktionen und beginnen Sie noch heute Ihre Reise mit unserer App.",
"longDescription": "Dieser umfassende Leitfaden hilft Ihnen, alle Funktionen und Möglichkeiten zu verstehen, die Ihnen als Benutzer unserer Anwendung zur Verfügung stehen.",
"loginTitle": "Willkommen zurück",
"loginSubtitle": "Melden Sie sich an, um fortzufahren",
"emailLabel": "E-Mail-Adresse",
"emailHint": "Geben Sie Ihre E-Mail-Adresse ein",
"passwordLabel": "Passwort",
"passwordHint": "Geben Sie Ihr Passwort ein",
"loginButton": "Anmelden",
"downloadProgress": "Wird heruntergeladen...",
"uploadProgress": "Wird hochgeladen..."
}
Arabic (app_ar.arb)
{
"@@locale": "ar",
"welcomeTitle": "مرحباً",
"welcomeDescription": "اكتشف ميزات مذهلة وابدأ رحلتك مع تطبيقنا اليوم.",
"longDescription": "سيساعدك هذا الدليل الشامل على فهم جميع الميزات والإمكانيات المتاحة لك كمستخدم لتطبيقنا.",
"loginTitle": "مرحباً بعودتك",
"loginSubtitle": "سجّل الدخول للمتابعة",
"emailLabel": "البريد الإلكتروني",
"emailHint": "أدخل بريدك الإلكتروني",
"passwordLabel": "كلمة المرور",
"passwordHint": "أدخل كلمة المرور",
"loginButton": "تسجيل الدخول",
"downloadProgress": "جارٍ التنزيل...",
"uploadProgress": "جارٍ الرفع..."
}
Best Practices Summary
Do's
- Use FractionallySizedBox for responsive layouts that need proportional sizing
- Combine with constraints (maxWidth, maxHeight) for reasonable limits
- Adjust width factors for languages with different text lengths
- Test progress indicators in RTL mode for proper direction
- Use for modal widths to ensure consistent presentation
Don'ts
- Don't use without constraints when content might overflow
- Don't ignore text overflow possibilities
- Don't assume fixed content across all languages
- Don't forget RTL testing for progress bars and split views
Accessibility Considerations
class AccessibleFractionalContent extends StatelessWidget {
final Widget child;
final String semanticLabel;
const AccessibleFractionalContent({
super.key,
required this.child,
required this.semanticLabel,
});
@override
Widget build(BuildContext context) {
return Semantics(
label: semanticLabel,
child: FractionallySizedBox(
widthFactor: 0.9,
child: child,
),
);
}
}
Conclusion
FractionallySizedBox is invaluable for creating responsive, proportionally-sized layouts in multilingual Flutter applications. By adapting width factors based on language characteristics and combining with proper constraints, you can build flexible interfaces that gracefully accommodate content variations across all supported locales.