Flutter AbsorbPointer Localization: Blocking Touch Events in Multilingual Apps
AbsorbPointer is a Flutter widget that absorbs pointer events, preventing them from reaching widgets below. In multilingual applications, AbsorbPointer creates interaction boundaries that communicate through visual cues rather than text, ensuring consistent behavior across all languages.
Understanding AbsorbPointer in Localization Context
AbsorbPointer intercepts and absorbs all pointer events in its area, stopping them from reaching underlying widgets. For multilingual apps, this enables:
- Modal overlays that block background interactions
- Loading screens with consistent blocking behavior
- Premium content gates that work across locales
- Controlled interaction zones for tutorials
Why AbsorbPointer Matters for Multilingual Apps
AbsorbPointer provides:
- Complete event blocking: Prevents all touches from passing through
- Visual feedback patterns: Combine with overlays for clear boundaries
- Consistent UX: Same blocking behavior in all languages
- Modal interactions: Create focused interaction areas
Basic AbsorbPointer Implementation
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedAbsorbPointerExample extends StatefulWidget {
const LocalizedAbsorbPointerExample({super.key});
@override
State<LocalizedAbsorbPointerExample> createState() =>
_LocalizedAbsorbPointerExampleState();
}
class _LocalizedAbsorbPointerExampleState
extends State<LocalizedAbsorbPointerExample> {
bool _showBlocker = false;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Stack(
children: [
Column(
children: [
FilledButton(
onPressed: () {
setState(() => _showBlocker = true);
},
child: Text(l10n.showBlockerButton),
),
const SizedBox(height: 16),
Text(l10n.interactiveContent),
],
),
if (_showBlocker)
Positioned.fill(
child: AbsorbPointer(
child: GestureDetector(
onTap: () {
setState(() => _showBlocker = false);
},
child: Container(
color: Colors.black54,
child: Center(
child: Card(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(l10n.blockerMessage),
),
),
),
),
),
),
),
],
);
}
}
Modal Dialog Patterns
Custom Modal with Background Block
class LocalizedCustomModal extends StatelessWidget {
final Widget child;
final VoidCallback onDismiss;
final bool dismissible;
const LocalizedCustomModal({
super.key,
required this.child,
required this.onDismiss,
this.dismissible = true,
});
@override
Widget build(BuildContext context) {
return Stack(
children: [
AbsorbPointer(
absorbing: !dismissible,
child: GestureDetector(
onTap: dismissible ? onDismiss : null,
child: Container(
color: Colors.black.withOpacity(0.5),
),
),
),
Center(child: child),
],
);
}
}
class LocalizedConfirmationDialog extends StatelessWidget {
final VoidCallback onConfirm;
final VoidCallback onCancel;
const LocalizedConfirmationDialog({
super.key,
required this.onConfirm,
required this.onCancel,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return LocalizedCustomModal(
onDismiss: onCancel,
child: Card(
margin: const EdgeInsets.all(32),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
l10n.confirmationTitle,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
Text(l10n.confirmationMessage),
const SizedBox(height: 24),
Row(
mainAxisSize: MainAxisSize.min,
children: [
OutlinedButton(
onPressed: onCancel,
child: Text(l10n.cancelButton),
),
const SizedBox(width: 16),
FilledButton(
onPressed: onConfirm,
child: Text(l10n.confirmButton),
),
],
),
],
),
),
),
);
}
}
Non-Dismissible Loading Modal
class LocalizedLoadingModal extends StatelessWidget {
final String? message;
const LocalizedLoadingModal({
super.key,
this.message,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return AbsorbPointer(
child: Container(
color: Colors.black54,
child: Center(
child: Card(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 24),
Text(message ?? l10n.loadingMessage),
],
),
),
),
),
),
);
}
}
class ProcessingScreen extends StatefulWidget {
const ProcessingScreen({super.key});
@override
State<ProcessingScreen> createState() => _ProcessingScreenState();
}
class _ProcessingScreenState extends State<ProcessingScreen> {
bool _isProcessing = false;
Future<void> _process() async {
setState(() => _isProcessing = true);
await Future.delayed(const Duration(seconds: 3));
setState(() => _isProcessing = false);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Stack(
children: [
Scaffold(
appBar: AppBar(title: Text(l10n.processScreenTitle)),
body: Center(
child: FilledButton(
onPressed: _process,
child: Text(l10n.startProcessButton),
),
),
),
if (_isProcessing)
LocalizedLoadingModal(
message: l10n.processingMessage,
),
],
);
}
}
Premium Content Gates
Content Lock Overlay
class LocalizedContentLock extends StatelessWidget {
final Widget child;
final bool isLocked;
const LocalizedContentLock({
super.key,
required this.child,
required this.isLocked,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Stack(
children: [
child,
if (isLocked)
Positioned.fill(
child: AbsorbPointer(
child: Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.7),
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.lock,
size: 48,
color: Colors.white,
),
const SizedBox(height: 16),
Text(
l10n.premiumContentTitle,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
l10n.premiumContentMessage,
style: const TextStyle(color: Colors.white70),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
FilledButton(
onPressed: () {
// Navigate to upgrade
},
child: Text(l10n.upgradeButton),
),
],
),
),
),
),
],
);
}
}
class LocalizedPremiumArticle extends StatelessWidget {
final bool isPremiumUser;
const LocalizedPremiumArticle({
super.key,
this.isPremiumUser = false,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return LocalizedContentLock(
isLocked: !isPremiumUser,
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.articleTitle,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(l10n.articlePreview),
const SizedBox(height: 16),
Text(l10n.articleFullContent),
],
),
),
),
);
}
}
Partial Screen Blocking
Block Specific Sections
class LocalizedSectionBlocker extends StatelessWidget {
final Widget child;
final bool isBlocked;
final String? blockedMessage;
const LocalizedSectionBlocker({
super.key,
required this.child,
required this.isBlocked,
this.blockedMessage,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Stack(
children: [
AnimatedOpacity(
opacity: isBlocked ? 0.3 : 1.0,
duration: const Duration(milliseconds: 200),
child: child,
),
if (isBlocked)
Positioned.fill(
child: AbsorbPointer(
child: Center(
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
),
],
),
child: Text(
blockedMessage ?? l10n.sectionUnavailable,
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
),
),
],
);
}
}
class LocalizedSettingsScreen extends StatelessWidget {
final bool isOnline;
const LocalizedSettingsScreen({
super.key,
this.isOnline = true,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return ListView(
children: [
ListTile(
leading: const Icon(Icons.person),
title: Text(l10n.profileSettings),
onTap: () {},
),
LocalizedSectionBlocker(
isBlocked: !isOnline,
blockedMessage: l10n.requiresConnection,
child: Column(
children: [
ListTile(
leading: const Icon(Icons.cloud_sync),
title: Text(l10n.syncSettings),
onTap: () {},
),
ListTile(
leading: const Icon(Icons.cloud_download),
title: Text(l10n.downloadSettings),
onTap: () {},
),
],
),
),
ListTile(
leading: const Icon(Icons.info),
title: Text(l10n.aboutSettings),
onTap: () {},
),
],
);
}
}
Tutorial Overlays
Guided Tour with AbsorbPointer
class LocalizedGuidedTour extends StatefulWidget {
final Widget child;
const LocalizedGuidedTour({
super.key,
required this.child,
});
@override
State<LocalizedGuidedTour> createState() => _LocalizedGuidedTourState();
}
class _LocalizedGuidedTourState extends State<LocalizedGuidedTour> {
int _currentStep = 0;
bool _showTour = true;
final List<Rect> _highlights = [
const Rect.fromLTWH(16, 100, 200, 60),
const Rect.fromLTWH(16, 180, 200, 60),
const Rect.fromLTWH(16, 260, 200, 60),
];
void _nextStep() {
if (_currentStep < _highlights.length - 1) {
setState(() => _currentStep++);
} else {
setState(() => _showTour = false);
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Stack(
children: [
widget.child,
if (_showTour)
Positioned.fill(
child: AbsorbPointer(
child: GestureDetector(
onTap: _nextStep,
child: CustomPaint(
painter: HighlightPainter(
highlight: _highlights[_currentStep],
),
child: Container(),
),
),
),
),
if (_showTour)
Positioned(
top: _highlights[_currentStep].bottom + 16,
left: 16,
right: 16,
child: AbsorbPointer(
absorbing: false,
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
_getTourTitle(l10n, _currentStep),
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(_getTourDescription(l10n, _currentStep)),
const SizedBox(height: 16),
Align(
alignment: AlignmentDirectional.centerEnd,
child: FilledButton(
onPressed: _nextStep,
child: Text(
_currentStep < _highlights.length - 1
? l10n.nextButton
: l10n.finishButton,
),
),
),
],
),
),
),
),
),
],
);
}
String _getTourTitle(AppLocalizations l10n, int step) {
switch (step) {
case 0:
return l10n.tourStep1Title;
case 1:
return l10n.tourStep2Title;
case 2:
return l10n.tourStep3Title;
default:
return '';
}
}
String _getTourDescription(AppLocalizations l10n, int step) {
switch (step) {
case 0:
return l10n.tourStep1Desc;
case 1:
return l10n.tourStep2Desc;
case 2:
return l10n.tourStep3Desc;
default:
return '';
}
}
}
class HighlightPainter extends CustomPainter {
final Rect highlight;
HighlightPainter({required this.highlight});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = Colors.black54;
canvas.drawPath(
Path.combine(
PathOperation.difference,
Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height)),
Path()
..addRRect(
RRect.fromRectAndRadius(highlight, const Radius.circular(8)),
),
),
paint,
);
}
@override
bool shouldRepaint(covariant HighlightPainter oldDelegate) {
return highlight != oldDelegate.highlight;
}
}
ARB File Structure
English (app_en.arb)
{
"@@locale": "en",
"showBlockerButton": "Show Blocker",
"interactiveContent": "This content is interactive",
"blockerMessage": "Tap to dismiss",
"confirmationTitle": "Confirm Action",
"confirmationMessage": "Are you sure you want to proceed?",
"cancelButton": "Cancel",
"confirmButton": "Confirm",
"loadingMessage": "Loading...",
"processScreenTitle": "Process",
"startProcessButton": "Start Process",
"processingMessage": "Processing your request...",
"premiumContentTitle": "Premium Content",
"premiumContentMessage": "Upgrade to access this content",
"upgradeButton": "Upgrade Now",
"articleTitle": "Exclusive Article",
"articlePreview": "This is a preview of the article...",
"articleFullContent": "This is the full premium content that is only available to subscribers.",
"sectionUnavailable": "This section is currently unavailable",
"requiresConnection": "Requires internet connection",
"profileSettings": "Profile",
"syncSettings": "Sync Settings",
"downloadSettings": "Downloads",
"aboutSettings": "About",
"nextButton": "Next",
"finishButton": "Finish",
"tourStep1Title": "Welcome",
"tourStep1Desc": "This is the first feature of our app.",
"tourStep2Title": "Navigation",
"tourStep2Desc": "Use this to navigate between sections.",
"tourStep3Title": "Settings",
"tourStep3Desc": "Customize your experience here."
}
German (app_de.arb)
{
"@@locale": "de",
"showBlockerButton": "Blocker anzeigen",
"interactiveContent": "Dieser Inhalt ist interaktiv",
"blockerMessage": "Zum Schließen tippen",
"confirmationTitle": "Aktion bestätigen",
"confirmationMessage": "Sind Sie sicher, dass Sie fortfahren möchten?",
"cancelButton": "Abbrechen",
"confirmButton": "Bestätigen",
"loadingMessage": "Laden...",
"processScreenTitle": "Verarbeitung",
"startProcessButton": "Verarbeitung starten",
"processingMessage": "Ihre Anfrage wird verarbeitet...",
"premiumContentTitle": "Premium-Inhalt",
"premiumContentMessage": "Upgraden Sie, um auf diesen Inhalt zuzugreifen",
"upgradeButton": "Jetzt upgraden",
"articleTitle": "Exklusiver Artikel",
"articlePreview": "Dies ist eine Vorschau des Artikels...",
"articleFullContent": "Dies ist der vollständige Premium-Inhalt, der nur für Abonnenten verfügbar ist.",
"sectionUnavailable": "Dieser Bereich ist derzeit nicht verfügbar",
"requiresConnection": "Internetverbindung erforderlich",
"profileSettings": "Profil",
"syncSettings": "Sync-Einstellungen",
"downloadSettings": "Downloads",
"aboutSettings": "Über",
"nextButton": "Weiter",
"finishButton": "Fertig",
"tourStep1Title": "Willkommen",
"tourStep1Desc": "Dies ist die erste Funktion unserer App.",
"tourStep2Title": "Navigation",
"tourStep2Desc": "Navigieren Sie zwischen den Bereichen.",
"tourStep3Title": "Einstellungen",
"tourStep3Desc": "Passen Sie Ihr Erlebnis hier an."
}
Arabic (app_ar.arb)
{
"@@locale": "ar",
"showBlockerButton": "إظهار الحاجز",
"interactiveContent": "هذا المحتوى تفاعلي",
"blockerMessage": "انقر للإغلاق",
"confirmationTitle": "تأكيد الإجراء",
"confirmationMessage": "هل أنت متأكد أنك تريد المتابعة؟",
"cancelButton": "إلغاء",
"confirmButton": "تأكيد",
"loadingMessage": "جاري التحميل...",
"processScreenTitle": "المعالجة",
"startProcessButton": "بدء المعالجة",
"processingMessage": "جاري معالجة طلبك...",
"premiumContentTitle": "محتوى مميز",
"premiumContentMessage": "قم بالترقية للوصول إلى هذا المحتوى",
"upgradeButton": "ترقية الآن",
"articleTitle": "مقال حصري",
"articlePreview": "هذه معاينة للمقال...",
"articleFullContent": "هذا هو المحتوى المميز الكامل المتاح فقط للمشتركين.",
"sectionUnavailable": "هذا القسم غير متاح حالياً",
"requiresConnection": "يتطلب اتصال بالإنترنت",
"profileSettings": "الملف الشخصي",
"syncSettings": "إعدادات المزامنة",
"downloadSettings": "التنزيلات",
"aboutSettings": "حول",
"nextButton": "التالي",
"finishButton": "إنهاء",
"tourStep1Title": "مرحباً",
"tourStep1Desc": "هذه هي الميزة الأولى في تطبيقنا.",
"tourStep2Title": "التنقل",
"tourStep2Desc": "استخدم هذا للتنقل بين الأقسام.",
"tourStep3Title": "الإعدادات",
"tourStep3Desc": "خصص تجربتك هنا."
}
Best Practices Summary
Do's
- Use AbsorbPointer for modal overlays that should block all background interactions
- Combine with visual overlays to clearly indicate blocked areas
- Provide dismiss mechanisms for user-initiated blocks
- Test on touch devices to verify proper event handling
- Use for loading states that shouldn't be interrupted
Don'ts
- Don't use without visual feedback - users need to see why they can't interact
- Don't block entire screens indefinitely without progress indication
- Don't confuse with IgnorePointer - AbsorbPointer stops events, IgnorePointer passes them through
- Don't use for simple disabled buttons - use the button's built-in disabled state
Conclusion
AbsorbPointer is essential for creating controlled interaction zones in multilingual Flutter applications. By blocking touch events completely, you can create modal overlays, loading screens, and premium content gates that work consistently across all languages. Combine AbsorbPointer with visual feedback to create clear, intuitive interaction boundaries that users understand regardless of their language.