Flutter AnimatedSize Localization: Dynamic Content, Expandable Sections, and Responsive Text
AnimatedSize automatically animates its size when child dimensions change. Proper localization ensures that expanding content, collapsible sections, and dynamic text adapt smoothly across languages with varying text lengths. This guide covers comprehensive strategies for localizing AnimatedSize widgets in Flutter.
Understanding AnimatedSize Localization
AnimatedSize widgets require localization for:
- Text expansion: Accommodating longer translations smoothly
- Collapsible content: Expanding/collapsing localized sections
- Dynamic messages: Animating status messages of varying lengths
- Form fields: Handling validation messages that differ in length
- Responsive layouts: Adapting to content size changes across locales
- Accessibility: Announcing size changes for screen readers
Basic AnimatedSize with Localized Content
Start with a simple AnimatedSize that handles varying text lengths:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedExpandableMessage extends StatefulWidget {
const LocalizedExpandableMessage({super.key});
@override
State<LocalizedExpandableMessage> createState() => _LocalizedExpandableMessageState();
}
class _LocalizedExpandableMessageState extends State<LocalizedExpandableMessage> {
bool _showFullMessage = false;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(l10n.messageTitle)),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Card(
child: InkWell(
onTap: () => setState(() => _showFullMessage = !_showFullMessage),
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
l10n.importantNotice,
style: Theme.of(context).textTheme.titleMedium,
),
),
AnimatedRotation(
duration: const Duration(milliseconds: 200),
turns: _showFullMessage ? 0.5 : 0,
child: Icon(
Icons.expand_more,
semanticLabel: _showFullMessage
? l10n.collapseAccessibility
: l10n.expandAccessibility,
),
),
],
),
AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
alignment: Alignment.topCenter,
child: Semantics(
liveRegion: true,
child: _showFullMessage
? Padding(
padding: const EdgeInsets.only(top: 12),
child: Text(
l10n.importantNoticeDetails,
style: Theme.of(context).textTheme.bodyMedium,
),
)
: const SizedBox.shrink(),
),
),
],
),
),
),
),
const SizedBox(height: 16),
Text(
l10n.tapToExpandHint,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.outline,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
}
ARB File Structure for AnimatedSize
{
"messageTitle": "Messages",
"@messageTitle": {
"description": "Title for messages screen"
},
"importantNotice": "Important Notice",
"@importantNotice": {
"description": "Header for important notice card"
},
"importantNoticeDetails": "This is important information that you should read carefully. It contains details about recent updates to our service and how they may affect your experience. Please review this information at your earliest convenience.",
"@importantNoticeDetails": {
"description": "Full details of the important notice"
},
"expandAccessibility": "Expand to read more",
"@expandAccessibility": {
"description": "Accessibility label for expand action"
},
"collapseAccessibility": "Collapse details",
"@collapseAccessibility": {
"description": "Accessibility label for collapse action"
},
"tapToExpandHint": "Tap a card to expand or collapse",
"@tapToExpandHint": {
"description": "Hint text explaining interaction"
}
}
Accordion with Multiple Expandable Sections
Create an accordion with localized expandable sections:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedAccordion extends StatefulWidget {
const LocalizedAccordion({super.key});
@override
State<LocalizedAccordion> createState() => _LocalizedAccordionState();
}
class _LocalizedAccordionState extends State<LocalizedAccordion> {
int? _expandedIndex;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final sections = [
_AccordionSection(
title: l10n.faqQuestion1,
content: l10n.faqAnswer1,
icon: Icons.help_outline,
),
_AccordionSection(
title: l10n.faqQuestion2,
content: l10n.faqAnswer2,
icon: Icons.security,
),
_AccordionSection(
title: l10n.faqQuestion3,
content: l10n.faqAnswer3,
icon: Icons.payment,
),
_AccordionSection(
title: l10n.faqQuestion4,
content: l10n.faqAnswer4,
icon: Icons.support_agent,
),
];
return Scaffold(
appBar: AppBar(title: Text(l10n.faqTitle)),
body: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: sections.length,
itemBuilder: (context, index) {
final section = sections[index];
final isExpanded = _expandedIndex == index;
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Card(
clipBehavior: Clip.antiAlias,
child: Semantics(
expanded: isExpanded,
button: true,
label: l10n.faqItemAccessibility(
section.title,
index + 1,
sections.length,
),
child: InkWell(
onTap: () {
setState(() {
_expandedIndex = isExpanded ? null : index;
});
},
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(
section.icon,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 12),
Expanded(
child: Text(
section.title,
style: Theme.of(context).textTheme.titleMedium,
),
),
AnimatedRotation(
duration: const Duration(milliseconds: 200),
turns: isExpanded ? 0.5 : 0,
child: const Icon(Icons.expand_more),
),
],
),
),
AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: isExpanded
? Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Divider(),
const SizedBox(height: 8),
Text(
section.content,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
)
: const SizedBox.shrink(),
),
],
),
),
),
),
);
},
),
);
}
}
class _AccordionSection {
final String title;
final String content;
final IconData icon;
_AccordionSection({
required this.title,
required this.content,
required this.icon,
});
}
Form Validation with Animated Error Messages
Handle form validation messages that vary in length:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedFormValidation extends StatefulWidget {
const LocalizedFormValidation({super.key});
@override
State<LocalizedFormValidation> createState() => _LocalizedFormValidationState();
}
class _LocalizedFormValidationState extends State<LocalizedFormValidation> {
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
String? _usernameError;
String? _passwordError;
List<String> _passwordRequirements = [];
@override
void dispose() {
_usernameController.dispose();
_passwordController.dispose();
super.dispose();
}
void _validateUsername(String value, AppLocalizations l10n) {
setState(() {
if (value.isEmpty) {
_usernameError = l10n.usernameRequired;
} else if (value.length < 3) {
_usernameError = l10n.usernameTooShort(3);
} else if (value.length > 20) {
_usernameError = l10n.usernameTooLong(20);
} else if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) {
_usernameError = l10n.usernameInvalidCharacters;
} else {
_usernameError = null;
}
});
}
void _validatePassword(String value, AppLocalizations l10n) {
final requirements = <String>[];
if (value.length < 8) {
requirements.add(l10n.passwordMinLength(8));
}
if (!value.contains(RegExp(r'[A-Z]'))) {
requirements.add(l10n.passwordNeedsUppercase);
}
if (!value.contains(RegExp(r'[a-z]'))) {
requirements.add(l10n.passwordNeedsLowercase);
}
if (!value.contains(RegExp(r'[0-9]'))) {
requirements.add(l10n.passwordNeedsNumber);
}
if (!value.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'))) {
requirements.add(l10n.passwordNeedsSpecial);
}
setState(() {
_passwordRequirements = requirements;
_passwordError = requirements.isNotEmpty ? l10n.passwordRequirementsNotMet : null;
});
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(l10n.createAccountTitle)),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Username field
TextField(
controller: _usernameController,
decoration: InputDecoration(
labelText: l10n.usernameLabel,
prefixIcon: const Icon(Icons.person),
errorText: _usernameError,
),
onChanged: (value) => _validateUsername(value, l10n),
),
AnimatedSize(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
child: _usernameError != null
? Padding(
padding: const EdgeInsets.only(top: 4),
child: Semantics(
liveRegion: true,
child: Text(
_usernameError!,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 12,
),
),
),
)
: const SizedBox.shrink(),
),
const SizedBox(height: 24),
// Password field
TextField(
controller: _passwordController,
obscureText: true,
decoration: InputDecoration(
labelText: l10n.passwordLabel,
prefixIcon: const Icon(Icons.lock),
errorText: _passwordError,
),
onChanged: (value) => _validatePassword(value, l10n),
),
AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
alignment: Alignment.topLeft,
child: _passwordRequirements.isNotEmpty
? Semantics(
liveRegion: true,
child: Padding(
padding: const EdgeInsets.only(top: 8),
child: Card(
color: Theme.of(context).colorScheme.errorContainer,
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.passwordRequirementsTitle,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onErrorContainer,
),
),
const SizedBox(height: 8),
..._passwordRequirements.map((req) {
return Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Row(
children: [
Icon(
Icons.close,
size: 16,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(width: 8),
Expanded(
child: Text(
req,
style: TextStyle(
color: Theme.of(context).colorScheme.onErrorContainer,
),
),
),
],
),
);
}),
],
),
),
),
),
)
: _passwordController.text.isNotEmpty
? Padding(
padding: const EdgeInsets.only(top: 8),
child: Row(
children: [
Icon(
Icons.check_circle,
color: Colors.green,
size: 16,
),
const SizedBox(width: 8),
Text(
l10n.passwordMeetsRequirements,
style: const TextStyle(color: Colors.green),
),
],
),
)
: const SizedBox.shrink(),
),
const SizedBox(height: 32),
ElevatedButton(
onPressed: _usernameError == null &&
_passwordError == null &&
_usernameController.text.isNotEmpty &&
_passwordController.text.isNotEmpty
? () {}
: null,
child: Text(l10n.createAccountButton),
),
],
),
),
);
}
}
Expandable Description Card
Create cards with expandable descriptions:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedExpandableCard extends StatefulWidget {
final String title;
final String shortDescription;
final String fullDescription;
final String imageUrl;
const LocalizedExpandableCard({
super.key,
required this.title,
required this.shortDescription,
required this.fullDescription,
required this.imageUrl,
});
@override
State<LocalizedExpandableCard> createState() => _LocalizedExpandableCardState();
}
class _LocalizedExpandableCardState extends State<LocalizedExpandableCard> {
bool _isExpanded = false;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
return Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Image header
Container(
height: 150,
color: Theme.of(context).colorScheme.primaryContainer,
child: Center(
child: Icon(
Icons.image,
size: 48,
color: Theme.of(context).colorScheme.primary,
),
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.title,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
alignment: isRtl ? Alignment.topRight : Alignment.topLeft,
child: Semantics(
liveRegion: true,
child: Text(
_isExpanded ? widget.fullDescription : widget.shortDescription,
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
const SizedBox(height: 12),
TextButton(
onPressed: () => setState(() => _isExpanded = !_isExpanded),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(_isExpanded ? l10n.showLess : l10n.showMore),
AnimatedRotation(
duration: const Duration(milliseconds: 200),
turns: _isExpanded ? 0.5 : 0,
child: const Icon(Icons.expand_more, size: 20),
),
],
),
),
],
),
),
],
),
);
}
}
// Usage in a list
class ExpandableCardList extends StatelessWidget {
const ExpandableCardList({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final items = [
_CardData(
title: l10n.productTitle1,
shortDescription: l10n.productShort1,
fullDescription: l10n.productFull1,
),
_CardData(
title: l10n.productTitle2,
shortDescription: l10n.productShort2,
fullDescription: l10n.productFull2,
),
_CardData(
title: l10n.productTitle3,
shortDescription: l10n.productShort3,
fullDescription: l10n.productFull3,
),
];
return Scaffold(
appBar: AppBar(title: Text(l10n.productsTitle)),
body: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: LocalizedExpandableCard(
title: item.title,
shortDescription: item.shortDescription,
fullDescription: item.fullDescription,
imageUrl: '',
),
);
},
),
);
}
}
class _CardData {
final String title;
final String shortDescription;
final String fullDescription;
_CardData({
required this.title,
required this.shortDescription,
required this.fullDescription,
});
}
Dynamic Search Results
Handle search results with varying content sizes:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedSearchResults extends StatefulWidget {
const LocalizedSearchResults({super.key});
@override
State<LocalizedSearchResults> createState() => _LocalizedSearchResultsState();
}
class _LocalizedSearchResultsState extends State<LocalizedSearchResults> {
final _searchController = TextEditingController();
List<String> _results = [];
bool _isSearching = false;
String? _searchMessage;
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _search(String query, AppLocalizations l10n) async {
if (query.isEmpty) {
setState(() {
_results = [];
_searchMessage = null;
});
return;
}
setState(() {
_isSearching = true;
_searchMessage = l10n.searching;
});
await Future.delayed(const Duration(milliseconds: 500));
// Simulate search results
final results = List.generate(
query.length % 5,
(i) => '${l10n.searchResult} ${i + 1}: $query',
);
setState(() {
_isSearching = false;
_results = results;
_searchMessage = results.isEmpty
? l10n.noResultsFound(query)
: l10n.resultsFound(results.length);
});
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(l10n.searchTitle)),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: l10n.searchHint,
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
_search('', l10n);
},
tooltip: l10n.clearSearch,
)
: null,
),
onChanged: (value) => _search(value, l10n),
),
),
// Search status message with animated size
AnimatedSize(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
child: _searchMessage != null
? Semantics(
liveRegion: true,
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
color: Theme.of(context).colorScheme.surfaceVariant,
child: Row(
children: [
if (_isSearching)
const Padding(
padding: EdgeInsets.only(right: 8),
child: SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
Expanded(
child: Text(
_searchMessage!,
style: Theme.of(context).textTheme.bodySmall,
),
),
],
),
),
)
: const SizedBox.shrink(),
),
// Results list
Expanded(
child: AnimatedSize(
duration: const Duration(milliseconds: 300),
child: _results.isEmpty
? Center(
child: Text(
l10n.startSearching,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.outline,
),
),
)
: ListView.builder(
itemCount: _results.length,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(
child: Text('${index + 1}'),
),
title: Text(_results[index]),
);
},
),
),
),
],
),
);
}
}
Animated Info Banner
Create info banners that expand with more details:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
enum BannerType { info, warning, success, error }
class LocalizedAnimatedBanner extends StatefulWidget {
final BannerType type;
final String title;
final String details;
final VoidCallback? onDismiss;
const LocalizedAnimatedBanner({
super.key,
required this.type,
required this.title,
required this.details,
this.onDismiss,
});
@override
State<LocalizedAnimatedBanner> createState() => _LocalizedAnimatedBannerState();
}
class _LocalizedAnimatedBannerState extends State<LocalizedAnimatedBanner> {
bool _isExpanded = false;
bool _isVisible = true;
Color _getBackgroundColor(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return switch (widget.type) {
BannerType.info => scheme.primaryContainer,
BannerType.warning => Colors.orange.shade100,
BannerType.success => Colors.green.shade100,
BannerType.error => scheme.errorContainer,
};
}
Color _getForegroundColor(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return switch (widget.type) {
BannerType.info => scheme.onPrimaryContainer,
BannerType.warning => Colors.orange.shade900,
BannerType.success => Colors.green.shade900,
BannerType.error => scheme.onErrorContainer,
};
}
IconData _getIcon() {
return switch (widget.type) {
BannerType.info => Icons.info,
BannerType.warning => Icons.warning,
BannerType.success => Icons.check_circle,
BannerType.error => Icons.error,
};
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final bgColor = _getBackgroundColor(context);
final fgColor = _getForegroundColor(context);
return AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: _isVisible
? Semantics(
container: true,
label: l10n.bannerAccessibility(widget.title),
child: Container(
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: fgColor.withOpacity(0.3)),
),
child: Column(
children: [
InkWell(
onTap: () => setState(() => _isExpanded = !_isExpanded),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(_getIcon(), color: fgColor),
const SizedBox(width: 12),
Expanded(
child: Text(
widget.title,
style: TextStyle(
color: fgColor,
fontWeight: FontWeight.bold,
),
),
),
if (widget.onDismiss != null)
IconButton(
icon: const Icon(Icons.close, size: 20),
color: fgColor,
onPressed: () {
setState(() => _isVisible = false);
widget.onDismiss?.call();
},
tooltip: l10n.dismissBanner,
)
else
AnimatedRotation(
duration: const Duration(milliseconds: 200),
turns: _isExpanded ? 0.5 : 0,
child: Icon(
Icons.expand_more,
color: fgColor,
),
),
],
),
),
),
AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: _isExpanded
? Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Divider(color: fgColor.withOpacity(0.2)),
const SizedBox(height: 8),
Text(
widget.details,
style: TextStyle(color: fgColor),
),
],
),
)
: const SizedBox.shrink(),
),
],
),
),
)
: const SizedBox.shrink(),
);
}
}
Complete ARB File for AnimatedSize
{
"@@locale": "en",
"messageTitle": "Messages",
"importantNotice": "Important Notice",
"importantNoticeDetails": "This is important information that you should read carefully. It contains details about recent updates to our service and how they may affect your experience.",
"expandAccessibility": "Expand to read more",
"collapseAccessibility": "Collapse details",
"tapToExpandHint": "Tap a card to expand or collapse",
"faqTitle": "Frequently Asked Questions",
"faqQuestion1": "How do I get started?",
"faqAnswer1": "Getting started is easy! Simply create an account, verify your email, and you'll have access to all features. Our onboarding guide will walk you through the basics.",
"faqQuestion2": "Is my data secure?",
"faqAnswer2": "Yes, we take security seriously. All data is encrypted in transit and at rest. We use industry-standard security protocols and undergo regular security audits.",
"faqQuestion3": "What payment methods do you accept?",
"faqAnswer3": "We accept all major credit cards, PayPal, and bank transfers. For enterprise customers, we also offer invoice-based billing.",
"faqQuestion4": "How can I contact support?",
"faqAnswer4": "You can reach our support team via email, live chat, or phone. We typically respond within 24 hours on business days.",
"faqItemAccessibility": "{title}, question {current} of {total}",
"@faqItemAccessibility": {
"placeholders": {
"title": {"type": "String"},
"current": {"type": "int"},
"total": {"type": "int"}
}
},
"createAccountTitle": "Create Account",
"usernameLabel": "Username",
"passwordLabel": "Password",
"usernameRequired": "Username is required",
"usernameTooShort": "Username must be at least {min} characters",
"@usernameTooShort": {
"placeholders": {"min": {"type": "int"}}
},
"usernameTooLong": "Username cannot exceed {max} characters",
"@usernameTooLong": {
"placeholders": {"max": {"type": "int"}}
},
"usernameInvalidCharacters": "Username can only contain letters, numbers, and underscores",
"passwordRequirementsTitle": "Password requirements:",
"passwordMinLength": "At least {length} characters",
"@passwordMinLength": {
"placeholders": {"length": {"type": "int"}}
},
"passwordNeedsUppercase": "One uppercase letter",
"passwordNeedsLowercase": "One lowercase letter",
"passwordNeedsNumber": "One number",
"passwordNeedsSpecial": "One special character",
"passwordRequirementsNotMet": "Password does not meet requirements",
"passwordMeetsRequirements": "Password meets all requirements",
"createAccountButton": "Create Account",
"showMore": "Show more",
"showLess": "Show less",
"productsTitle": "Products",
"productTitle1": "Premium Plan",
"productShort1": "Full access to all features...",
"productFull1": "Full access to all features including priority support, unlimited storage, advanced analytics, and team collaboration tools. Perfect for growing businesses.",
"productTitle2": "Basic Plan",
"productShort2": "Essential features for individuals...",
"productFull2": "Essential features for individuals including core functionality, 5GB storage, basic analytics, and email support. Great for getting started.",
"productTitle3": "Enterprise Plan",
"productShort3": "Custom solutions for large teams...",
"productFull3": "Custom solutions for large teams including dedicated support, unlimited everything, custom integrations, SLA guarantees, and training sessions.",
"searchTitle": "Search",
"searchHint": "Search for items...",
"clearSearch": "Clear search",
"searching": "Searching...",
"searchResult": "Result",
"noResultsFound": "No results found for \"{query}\"",
"@noResultsFound": {
"placeholders": {"query": {"type": "String"}}
},
"resultsFound": "{count} {count, plural, =1{result} other{results}} found",
"@resultsFound": {
"placeholders": {"count": {"type": "int"}}
},
"startSearching": "Start typing to search",
"bannerAccessibility": "Notification: {title}",
"@bannerAccessibility": {
"placeholders": {"title": {"type": "String"}}
},
"dismissBanner": "Dismiss notification"
}
Best Practices Summary
- Set appropriate alignment: Use
Alignment.topLeftorAlignment.topRightbased on text direction - Handle RTL layouts: Ensure content expands in the correct direction for RTL languages
- Provide accessibility: Use liveRegion for dynamic content changes
- Choose smooth curves: Use
easeInOutfor natural expand/collapse animations - Keep durations reasonable: 200-300ms for most size animations
- Clip overflow: Use
Clip.antiAliason parent containers when needed - Test with long text: Verify animations work with verbose translations (German, Finnish)
- Handle empty states: Properly animate to/from
SizedBox.shrink() - Consider performance: Avoid animating complex layouts frequently
- Provide visual feedback: Combine with rotation icons or other indicators
Conclusion
AnimatedSize is essential for creating smooth, professional animations when content changes dynamically. By properly handling text expansion across locales, providing accessibility feedback, and aligning content correctly for RTL languages, you create polished experiences for users worldwide. The patterns shown here—expandable messages, accordions, form validation, and info banners—can be adapted to any Flutter application requiring dynamic size animations.
Remember to test your animations with various locales, especially those with longer text like German or Finnish, to ensure smooth transitions regardless of content length.