Flutter Material Widget Localization: Foundation for Multilingual Material Design
Material is a Flutter widget that provides Material Design visual properties like elevation and ink effects. In multilingual applications, Material serves as the foundation for touch feedback and visual styling that works consistently across all languages and text directions.
Understanding Material in Localization Context
Material creates a piece of material with elevation, shadows, and ink splash effects. For multilingual apps, this enables:
- Consistent visual foundation across all locales
- Direction-aware ink effects for RTL layouts
- Themed appearance that respects locale preferences
- Accessible visual feedback for all users
Why Material Matters for Multilingual Apps
Material provides:
- Universal visual language: Material effects work without translation
- Theme integration: Respects app-wide localized themes
- Ink support: Enables InkWell and InkResponse effects
- Elevation system: Consistent depth across all languages
Basic Material Implementation
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedMaterialExample extends StatelessWidget {
const LocalizedMaterialExample({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Material(
elevation: 4,
borderRadius: BorderRadius.circular(12),
color: Theme.of(context).colorScheme.surface,
child: InkWell(
onTap: () {},
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
l10n.materialTitle,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(
l10n.materialDescription,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
),
);
}
}
Material Types
Different Material Types
class LocalizedMaterialTypes extends StatelessWidget {
const LocalizedMaterialTypes({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Column(
children: [
Material(
type: MaterialType.card,
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(l10n.cardMaterial),
),
),
const SizedBox(height: 16),
Material(
type: MaterialType.canvas,
color: Theme.of(context).colorScheme.surfaceVariant,
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(l10n.canvasMaterial),
),
),
const SizedBox(height: 16),
Material(
type: MaterialType.circle,
elevation: 4,
color: Theme.of(context).colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(24),
child: Icon(
Icons.check,
color: Theme.of(context).colorScheme.onPrimary,
),
),
),
const SizedBox(height: 16),
Material(
type: MaterialType.transparency,
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(l10n.transparentMaterial),
),
),
],
);
}
}
Custom Buttons with Material
Material Button Base
class LocalizedMaterialButton extends StatelessWidget {
final String label;
final IconData? icon;
final VoidCallback? onPressed;
final Color? backgroundColor;
final Color? foregroundColor;
const LocalizedMaterialButton({
super.key,
required this.label,
this.icon,
this.onPressed,
this.backgroundColor,
this.foregroundColor,
});
@override
Widget build(BuildContext context) {
final bgColor = backgroundColor ?? Theme.of(context).colorScheme.primary;
final fgColor = foregroundColor ?? Theme.of(context).colorScheme.onPrimary;
return Material(
color: bgColor,
borderRadius: BorderRadius.circular(8),
elevation: 2,
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(icon, color: fgColor, size: 20),
const SizedBox(width: 8),
],
Text(
label,
style: TextStyle(
color: fgColor,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
);
}
}
class LocalizedButtonRow extends StatelessWidget {
const LocalizedButtonRow({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Wrap(
spacing: 12,
runSpacing: 12,
children: [
LocalizedMaterialButton(
label: l10n.primaryButton,
icon: Icons.check,
onPressed: () {},
),
LocalizedMaterialButton(
label: l10n.secondaryButton,
icon: Icons.close,
backgroundColor: Theme.of(context).colorScheme.secondary,
foregroundColor: Theme.of(context).colorScheme.onSecondary,
onPressed: () {},
),
LocalizedMaterialButton(
label: l10n.tertiaryButton,
backgroundColor: Theme.of(context).colorScheme.tertiary,
foregroundColor: Theme.of(context).colorScheme.onTertiary,
onPressed: () {},
),
],
);
}
}
Material with Shapes
Shaped Material Containers
class LocalizedShapedMaterial extends StatelessWidget {
final Widget child;
final ShapeBorder shape;
final double elevation;
final Color? color;
const LocalizedShapedMaterial({
super.key,
required this.child,
required this.shape,
this.elevation = 2,
this.color,
});
@override
Widget build(BuildContext context) {
return Material(
shape: shape,
elevation: elevation,
color: color ?? Theme.of(context).colorScheme.surface,
child: child,
);
}
}
class LocalizedShapeShowcase extends StatelessWidget {
const LocalizedShapeShowcase({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Column(
children: [
LocalizedShapedMaterial(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(l10n.roundedShape),
),
),
const SizedBox(height: 16),
LocalizedShapedMaterial(
shape: const StadiumBorder(),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
child: Text(l10n.stadiumShape),
),
),
const SizedBox(height: 16),
LocalizedShapedMaterial(
shape: BeveledRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(l10n.beveledShape),
),
),
const SizedBox(height: 16),
LocalizedShapedMaterial(
shape: const CircleBorder(),
child: Padding(
padding: const EdgeInsets.all(24),
child: Icon(Icons.star),
),
),
],
);
}
}
Material Banners
Localized Banner Component
class LocalizedMaterialBanner extends StatelessWidget {
final String message;
final List<Widget> actions;
final IconData? icon;
final Color? backgroundColor;
const LocalizedMaterialBanner({
super.key,
required this.message,
required this.actions,
this.icon,
this.backgroundColor,
});
@override
Widget build(BuildContext context) {
return Material(
color: backgroundColor ?? Theme.of(context).colorScheme.surfaceVariant,
elevation: 1,
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (icon != null) ...[
Icon(icon),
const SizedBox(width: 16),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
message,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: actions,
),
],
),
),
],
),
),
);
}
}
class LocalizedBannerDemo extends StatelessWidget {
const LocalizedBannerDemo({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Column(
children: [
LocalizedMaterialBanner(
icon: Icons.info_outline,
message: l10n.infoBannerMessage,
actions: [
TextButton(
onPressed: () {},
child: Text(l10n.dismissButton),
),
TextButton(
onPressed: () {},
child: Text(l10n.learnMoreButton),
),
],
),
const SizedBox(height: 16),
LocalizedMaterialBanner(
icon: Icons.warning_amber,
message: l10n.warningBannerMessage,
backgroundColor: Colors.amber.shade100,
actions: [
TextButton(
onPressed: () {},
child: Text(l10n.fixNowButton),
),
],
),
],
);
}
}
Material Elevation States
Interactive Elevation Changes
class LocalizedElevatedContainer extends StatefulWidget {
final Widget child;
final VoidCallback? onTap;
const LocalizedElevatedContainer({
super.key,
required this.child,
this.onTap,
});
@override
State<LocalizedElevatedContainer> createState() =>
_LocalizedElevatedContainerState();
}
class _LocalizedElevatedContainerState
extends State<LocalizedElevatedContainer> {
bool _isPressed = false;
bool _isHovered = false;
double get _elevation {
if (_isPressed) return 1;
if (_isHovered) return 6;
return 3;
}
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: GestureDetector(
onTapDown: (_) => setState(() => _isPressed = true),
onTapUp: (_) => setState(() => _isPressed = false),
onTapCancel: () => setState(() => _isPressed = false),
onTap: widget.onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
child: Material(
elevation: _elevation,
borderRadius: BorderRadius.circular(12),
color: Theme.of(context).colorScheme.surface,
child: widget.child,
),
),
),
);
}
}
class LocalizedInteractiveCards extends StatelessWidget {
const LocalizedInteractiveCards({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Column(
children: [
LocalizedElevatedContainer(
onTap: () {},
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
const Icon(Icons.folder),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.projectFolder,
style: Theme.of(context).textTheme.titleMedium,
),
Text(
l10n.projectFolderDesc,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
const Icon(Icons.chevron_right),
],
),
),
),
const SizedBox(height: 16),
LocalizedElevatedContainer(
onTap: () {},
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
const Icon(Icons.settings),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.settingsFolder,
style: Theme.of(context).textTheme.titleMedium,
),
Text(
l10n.settingsFolderDesc,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
const Icon(Icons.chevron_right),
],
),
),
),
],
);
}
}
ARB File Structure
English (app_en.arb)
{
"@@locale": "en",
"materialTitle": "Material Container",
"materialDescription": "This is a Material widget with elevation and ink effects.",
"cardMaterial": "Card Material Type",
"canvasMaterial": "Canvas Material Type",
"transparentMaterial": "Transparent Material Type",
"primaryButton": "Primary",
"secondaryButton": "Secondary",
"tertiaryButton": "Tertiary",
"roundedShape": "Rounded Rectangle",
"stadiumShape": "Stadium Shape",
"beveledShape": "Beveled Rectangle",
"infoBannerMessage": "Your account settings have been updated successfully.",
"warningBannerMessage": "Your subscription expires in 3 days.",
"dismissButton": "Dismiss",
"learnMoreButton": "Learn More",
"fixNowButton": "Renew Now",
"projectFolder": "Projects",
"projectFolderDesc": "View all your projects",
"settingsFolder": "Settings",
"settingsFolderDesc": "Configure app preferences"
}
German (app_de.arb)
{
"@@locale": "de",
"materialTitle": "Material-Container",
"materialDescription": "Dies ist ein Material-Widget mit Höhe und Tinteneffekten.",
"cardMaterial": "Karten-Materialtyp",
"canvasMaterial": "Leinwand-Materialtyp",
"transparentMaterial": "Transparenter Materialtyp",
"primaryButton": "Primär",
"secondaryButton": "Sekundär",
"tertiaryButton": "Tertiär",
"roundedShape": "Abgerundetes Rechteck",
"stadiumShape": "Stadionform",
"beveledShape": "Abgeschrägtes Rechteck",
"infoBannerMessage": "Ihre Kontoeinstellungen wurden erfolgreich aktualisiert.",
"warningBannerMessage": "Ihr Abonnement läuft in 3 Tagen ab.",
"dismissButton": "Schließen",
"learnMoreButton": "Mehr erfahren",
"fixNowButton": "Jetzt erneuern",
"projectFolder": "Projekte",
"projectFolderDesc": "Alle Projekte anzeigen",
"settingsFolder": "Einstellungen",
"settingsFolderDesc": "App-Einstellungen konfigurieren"
}
Arabic (app_ar.arb)
{
"@@locale": "ar",
"materialTitle": "حاوية Material",
"materialDescription": "هذا عنصر Material مع ارتفاع وتأثيرات الحبر.",
"cardMaterial": "نوع مادة البطاقة",
"canvasMaterial": "نوع مادة اللوحة",
"transparentMaterial": "نوع مادة شفافة",
"primaryButton": "أساسي",
"secondaryButton": "ثانوي",
"tertiaryButton": "ثالثي",
"roundedShape": "مستطيل مستدير",
"stadiumShape": "شكل ملعب",
"beveledShape": "مستطيل مشطوف",
"infoBannerMessage": "تم تحديث إعدادات حسابك بنجاح.",
"warningBannerMessage": "تنتهي صلاحية اشتراكك خلال 3 أيام.",
"dismissButton": "إغلاق",
"learnMoreButton": "اعرف المزيد",
"fixNowButton": "تجديد الآن",
"projectFolder": "المشاريع",
"projectFolderDesc": "عرض جميع مشاريعك",
"settingsFolder": "الإعدادات",
"settingsFolderDesc": "تكوين تفضيلات التطبيق"
}
Best Practices Summary
Do's
- Use Material as foundation for custom touchable widgets
- Set appropriate elevation for visual hierarchy
- Match borderRadius with InkWell for proper ripple clipping
- Use semantic colors from Theme for consistency
- Test elevation states on different devices
Don'ts
- Don't use Material without purpose - it adds to the widget tree
- Don't forget ink effects need Material ancestor
- Don't over-elevate - follow Material Design guidelines
- Don't mix Material types inconsistently
Conclusion
Material is the foundation for creating Material Design-compliant UI elements in multilingual Flutter applications. By providing elevation, shadows, and ink support, Material enables consistent visual feedback that works across all languages. Use Material strategically as the base for custom interactive widgets that need touch feedback and visual depth.