Flutter LimitedBox Localization: Size Limits for Unconstrained Layouts in Multilingual Apps
LimitedBox limits its size only when it's unconstrained, making it useful for widgets inside scrollable containers where children would otherwise expand infinitely. In multilingual applications, LimitedBox ensures that content has reasonable default sizes when constraints aren't provided by the parent. This guide covers comprehensive strategies for using LimitedBox in Flutter localization.
Understanding LimitedBox in Localization
LimitedBox widgets benefit localization for:
- List items: Default heights for dynamically sized list content
- Scrollable content: Limiting size when parent provides infinite constraints
- Placeholder sizing: Default dimensions for loading states
- Optional content: Reasonable sizes for content that may or may not exist
- Grid items: Default sizes in unconstrained grid layouts
- Expandable sections: Initial sizes before user interaction
Basic LimitedBox with Localized Content
Start with size limits in a scrollable context:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedLimitedBoxDemo extends StatelessWidget {
const LocalizedLimitedBoxDemo({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(l10n.limitedBoxTitle)),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Text(
l10n.unlimitedContextLabel,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(
l10n.unlimitedContextDescription,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 16),
// LimitedBox in a ListView (unconstrained height)
LimitedBox(
maxHeight: 150,
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.limitedCardTitle,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(l10n.limitedCardContent),
],
),
),
),
const SizedBox(height: 16),
// Another LimitedBox with different content
LimitedBox(
maxHeight: 200,
child: Container(
color: Theme.of(context).colorScheme.secondaryContainer,
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.largerCardTitle,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Expanded(
child: SingleChildScrollView(
child: Text(l10n.largerCardContent),
),
),
],
),
),
),
],
),
);
}
}
ARB File Structure for LimitedBox
{
"limitedBoxTitle": "Limited Box Demo",
"@limitedBoxTitle": {
"description": "Title for limited box demo page"
},
"unlimitedContextLabel": "Unconstrained Context",
"unlimitedContextDescription": "LimitedBox limits size only when parent provides no constraints, like inside a ListView.",
"limitedCardTitle": "Limited Height Card",
"limitedCardContent": "This card has a maximum height of 150 pixels when in an unconstrained context.",
"largerCardTitle": "Scrollable Content Card",
"largerCardContent": "This card contains more content that may need scrolling. The LimitedBox ensures it doesn't expand infinitely in a ListView context while still allowing internal scrolling for overflow content."
}
Dynamic List Items with Default Heights
Create list items with reasonable default sizes:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedDynamicList extends StatelessWidget {
const LocalizedDynamicList({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
final items = [
{
'type': 'short',
'title': l10n.shortItemTitle,
'content': l10n.shortItemContent,
},
{
'type': 'medium',
'title': l10n.mediumItemTitle,
'content': l10n.mediumItemContent,
},
{
'type': 'long',
'title': l10n.longItemTitle,
'content': l10n.longItemContent,
},
{
'type': 'image',
'title': l10n.imageItemTitle,
'content': l10n.imageItemContent,
},
];
return Scaffold(
appBar: AppBar(title: Text(l10n.dynamicListTitle)),
body: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
final type = item['type'] as String;
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: _buildListItem(context, l10n, isRtl, item, type),
);
},
),
);
}
Widget _buildListItem(
BuildContext context,
AppLocalizations l10n,
bool isRtl,
Map<String, String> item,
String type,
) {
// Different max heights based on content type
double maxHeight;
switch (type) {
case 'short':
maxHeight = 80;
break;
case 'medium':
maxHeight = 120;
break;
case 'long':
maxHeight = 180;
break;
case 'image':
maxHeight = 250;
break;
default:
maxHeight = 100;
}
return Card(
child: LimitedBox(
maxHeight: maxHeight,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: _getTypeColor(type).withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
type.toUpperCase(),
style: TextStyle(
color: _getTypeColor(type),
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
item['title']!,
style: Theme.of(context).textTheme.titleMedium,
),
),
],
),
const SizedBox(height: 8),
if (type == 'image') ...[
Expanded(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Icon(
Icons.image,
size: 48,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
),
const SizedBox(height: 8),
],
Text(
item['content']!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
maxLines: type == 'short' ? 1 : null,
overflow: type == 'short' ? TextOverflow.ellipsis : null,
),
],
),
),
),
);
}
Color _getTypeColor(String type) {
switch (type) {
case 'short':
return Colors.green;
case 'medium':
return Colors.blue;
case 'long':
return Colors.orange;
case 'image':
return Colors.purple;
default:
return Colors.grey;
}
}
}
Loading Placeholders with Limited Sizes
Create skeleton loaders with reasonable dimensions:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedLoadingPlaceholders extends StatefulWidget {
const LocalizedLoadingPlaceholders({super.key});
@override
State<LocalizedLoadingPlaceholders> createState() =>
_LocalizedLoadingPlaceholdersState();
}
class _LocalizedLoadingPlaceholdersState
extends State<LocalizedLoadingPlaceholders>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
bool _isLoading = true;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
)..repeat();
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.loadingDemoTitle),
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: ListView(
padding: const EdgeInsets.all(16),
children: [
// Header section
_isLoading
? _buildHeaderSkeleton(context)
: _buildHeader(context, l10n),
const SizedBox(height: 24),
// Content cards
...List.generate(3, (index) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: _isLoading
? _buildCardSkeleton(context)
: _buildCard(context, l10n, index),
);
}),
],
),
);
}
Widget _buildHeaderSkeleton(BuildContext context) {
return LimitedBox(
maxHeight: 100,
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildShimmerBox(context, width: 200, height: 28),
const SizedBox(height: 8),
_buildShimmerBox(context, width: 300, height: 16),
const SizedBox(height: 4),
_buildShimmerBox(context, width: 250, height: 16),
],
);
},
),
);
}
Widget _buildHeader(BuildContext context, AppLocalizations l10n) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.welcomeTitle,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
Text(
l10n.welcomeSubtitle,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
);
}
Widget _buildCardSkeleton(BuildContext context) {
return Card(
child: LimitedBox(
maxHeight: 150,
child: Padding(
padding: const EdgeInsets.all(16),
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Row(
children: [
_buildShimmerBox(
context,
width: 80,
height: 80,
borderRadius: 8,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildShimmerBox(context, width: 150, height: 18),
const SizedBox(height: 8),
_buildShimmerBox(context, width: double.infinity, height: 14),
const SizedBox(height: 4),
_buildShimmerBox(context, width: 100, height: 14),
],
),
),
],
);
},
),
),
),
);
}
Widget _buildCard(BuildContext context, AppLocalizations l10n, int index) {
final titles = [l10n.card1Title, l10n.card2Title, l10n.card3Title];
final descriptions = [
l10n.card1Description,
l10n.card2Description,
l10n.card3Description,
];
final colors = [Colors.blue, Colors.green, Colors.orange];
return Card(
child: ListTile(
contentPadding: const EdgeInsets.all(16),
leading: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: colors[index].withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.article,
color: colors[index],
size: 32,
),
),
title: Text(titles[index]),
subtitle: Text(descriptions[index]),
trailing: Icon(
Directionality.of(context) == TextDirection.rtl
? Icons.chevron_left
: Icons.chevron_right,
),
),
);
}
Widget _buildShimmerBox(
BuildContext context, {
required double height,
double? width,
double borderRadius = 4,
}) {
return Container(
width: width,
height: height,
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,
],
),
),
);
}
}
Horizontal Scrolling with Width Limits
Limit widths in horizontal scroll views:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedHorizontalScroll extends StatelessWidget {
const LocalizedHorizontalScroll({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
return Scaffold(
appBar: AppBar(title: Text(l10n.horizontalScrollTitle)),
body: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Categories section
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
l10n.categoriesLabel,
style: Theme.of(context).textTheme.titleLarge,
),
),
const SizedBox(height: 12),
SizedBox(
height: 120,
child: ListView(
scrollDirection: Axis.horizontal,
reverse: isRtl,
padding: const EdgeInsets.symmetric(horizontal: 16),
children: [
_buildCategoryCard(context, l10n.categoryFood, Icons.restaurant, Colors.orange),
_buildCategoryCard(context, l10n.categoryTravel, Icons.flight, Colors.blue),
_buildCategoryCard(context, l10n.categoryShopping, Icons.shopping_bag, Colors.pink),
_buildCategoryCard(context, l10n.categoryEntertainment, Icons.movie, Colors.purple),
_buildCategoryCard(context, l10n.categoryHealth, Icons.favorite, Colors.red),
],
),
),
const SizedBox(height: 24),
// Stories/highlights section
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
l10n.storiesLabel,
style: Theme.of(context).textTheme.titleLarge,
),
),
const SizedBox(height: 12),
SizedBox(
height: 180,
child: ListView(
scrollDirection: Axis.horizontal,
reverse: isRtl,
padding: const EdgeInsets.symmetric(horizontal: 16),
children: List.generate(5, (index) {
return _buildStoryCard(context, l10n, index);
}),
),
),
],
),
),
);
}
Widget _buildCategoryCard(
BuildContext context,
String label,
IconData icon,
Color color,
) {
return Padding(
padding: const EdgeInsets.only(right: 12),
child: LimitedBox(
maxWidth: 100,
child: Column(
children: [
Container(
width: 70,
height: 70,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(icon, color: color, size: 32),
),
const SizedBox(height: 8),
Text(
label,
style: Theme.of(context).textTheme.labelMedium,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
Widget _buildStoryCard(
BuildContext context,
AppLocalizations l10n,
int index,
) {
final colors = [Colors.blue, Colors.green, Colors.orange, Colors.purple, Colors.red];
return Padding(
padding: const EdgeInsets.only(right: 12),
child: LimitedBox(
maxWidth: 140,
child: Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 100,
color: colors[index].withOpacity(0.3),
child: Center(
child: Icon(
Icons.image,
color: colors[index],
size: 40,
),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.storyTitle(index + 1),
style: Theme.of(context).textTheme.titleSmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
l10n.storySubtitle,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
),
);
}
}
Complete ARB File for LimitedBox
{
"@@locale": "en",
"limitedBoxTitle": "Limited Box Demo",
"unlimitedContextLabel": "Unconstrained Context",
"unlimitedContextDescription": "LimitedBox limits size only when parent provides no constraints, like inside a ListView.",
"limitedCardTitle": "Limited Height Card",
"limitedCardContent": "This card has a maximum height of 150 pixels when in an unconstrained context.",
"largerCardTitle": "Scrollable Content Card",
"largerCardContent": "This card contains more content that may need scrolling. The LimitedBox ensures it doesn't expand infinitely in a ListView context while still allowing internal scrolling for overflow content.",
"dynamicListTitle": "Dynamic List",
"shortItemTitle": "Quick Update",
"shortItemContent": "Brief notification message.",
"mediumItemTitle": "News Article",
"mediumItemContent": "This is a medium-length article with more content to display, providing additional context and information.",
"longItemTitle": "Detailed Report",
"longItemContent": "This is a comprehensive report with extensive content. It contains multiple paragraphs of information that may require more space to display properly. The LimitedBox ensures reasonable sizing.",
"imageItemTitle": "Photo Gallery",
"imageItemContent": "Featured image with caption",
"loadingDemoTitle": "Loading States",
"welcomeTitle": "Welcome Back",
"welcomeSubtitle": "Here's what's happening with your account today",
"card1Title": "Recent Activity",
"card1Description": "View your latest transactions and updates",
"card2Title": "Notifications",
"card2Description": "You have 3 unread messages",
"card3Title": "Settings",
"card3Description": "Manage your account preferences",
"horizontalScrollTitle": "Horizontal Scrolling",
"categoriesLabel": "Categories",
"categoryFood": "Food",
"categoryTravel": "Travel",
"categoryShopping": "Shopping",
"categoryEntertainment": "Entertainment",
"categoryHealth": "Health",
"storiesLabel": "Stories",
"storyTitle": "Story {number}",
"@storyTitle": {
"placeholders": {"number": {"type": "int"}}
},
"storySubtitle": "Tap to view"
}
Best Practices Summary
- Use in scrollable contexts: LimitedBox only affects unconstrained children
- Set reasonable defaults: Choose max sizes that work for typical content
- Combine with scrolling: Allow internal scrolling for overflow content
- Consistent sizing: Use similar limits for related content types
- Test with translations: Verify limits work with different text lengths
- Consider content type: Different content types may need different limits
- Loading states: Use for skeleton placeholders with reasonable sizes
- Horizontal scrolling: Limit widths for horizontally scrolling items
- Don't overuse: Only needed when parent provides no constraints
- RTL support: LimitedBox works the same in both directions
Conclusion
LimitedBox is a specialized widget for handling unconstrained layout contexts in Flutter. While it doesn't affect constrained children, it's invaluable for ensuring reasonable sizes in scrollable containers where children would otherwise expand infinitely.
In multilingual applications, LimitedBox helps maintain predictable layouts by providing sensible defaults for content that may vary significantly in length across languages. Use it alongside other constraint widgets for a complete layout strategy.