import 'dart:async'; import 'dart:math' as math; import 'package:flutter/material.dart'; enum ToastType { success, error, info } class _ToastItem { const _ToastItem({ required this.id, required this.message, required this.type, }); final String id; final String message; final ToastType type; } class ToastService { static const Duration _displayDuration = Duration(milliseconds: 3000); static final ValueNotifier> _toasts = ValueNotifier>(<_ToastItem>[]); static void success(String message) { show(message, type: ToastType.success); } static void error(String message) { show(message, type: ToastType.error); } static void info(String message) { show(message, type: ToastType.info); } static void show(String message, {ToastType type = ToastType.success}) { final trimmed = message.trim(); if (trimmed.isEmpty) { return; } final item = _ToastItem( id: '${DateTime.now().microsecondsSinceEpoch}-${_toasts.value.length}', message: trimmed, type: type, ); _toasts.value = [..._toasts.value, item]; unawaited( Future.delayed(_displayDuration, () { _remove(item.id); }), ); } static void _remove(String id) { final next = _toasts.value.where((toast) => toast.id != id).toList(); if (next.length == _toasts.value.length) { return; } _toasts.value = next; } } class ToastViewport extends StatelessWidget { const ToastViewport({super.key}); @override Widget build(BuildContext context) { return IgnorePointer( ignoring: true, child: SafeArea( child: ValueListenableBuilder>( valueListenable: ToastService._toasts, builder: (context, toasts, _) { if (toasts.isEmpty) { return const SizedBox.shrink(); } final media = MediaQuery.of(context); final width = math.min(320.0, media.size.width - 32); return Align( alignment: Alignment.bottomRight, child: Padding( padding: const EdgeInsets.only(right: 16, bottom: 16), child: SizedBox( width: width > 0 ? width : 320, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ for (final toast in toasts) Padding( padding: const EdgeInsets.only(top: 8), child: _ToastCard(item: toast), ), ], ), ), ), ); }, ), ), ); } } class _ToastCard extends StatefulWidget { const _ToastCard({required this.item}); final _ToastItem item; @override State<_ToastCard> createState() => _ToastCardState(); } class _ToastCardState extends State<_ToastCard> { bool _visible = false; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) { return; } setState(() { _visible = true; }); }); } @override Widget build(BuildContext context) { final scheme = _toastColorScheme(widget.item.type); final icon = _toastIcon(widget.item.type); return AnimatedSlide( duration: const Duration(milliseconds: 300), curve: Curves.easeOutCubic, offset: _visible ? Offset.zero : const Offset(1, 0), child: AnimatedOpacity( duration: const Duration(milliseconds: 220), opacity: _visible ? 1 : 0, child: DecoratedBox( decoration: BoxDecoration( color: scheme.background, borderRadius: BorderRadius.circular(8), border: Border.all(color: scheme.border), boxShadow: const [ BoxShadow( color: Color(0x26000000), blurRadius: 16, offset: Offset(0, 6), ), ], ), child: Padding( padding: const EdgeInsets.all(16), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon(icon, size: 20, color: scheme.foreground), const SizedBox(width: 12), Expanded( child: Text( widget.item.message, style: TextStyle( color: scheme.foreground, fontSize: 14, fontWeight: FontWeight.w400, height: 1.2, decoration: TextDecoration.none, ), ), ), ], ), ), ), ), ); } _ToastColorScheme _toastColorScheme(ToastType type) { switch (type) { case ToastType.success: return const _ToastColorScheme( background: Color(0xFFECFDF5), border: Color(0xFFA7F3D0), foreground: Color(0xFF065F46), ); case ToastType.error: return const _ToastColorScheme( background: Color(0xFFFFF1F2), border: Color(0xFFFDA4AF), foreground: Color(0xFF9F1239), ); case ToastType.info: return const _ToastColorScheme( background: Color(0xFFEFF6FF), border: Color(0xFFBFDBFE), foreground: Color(0xFF1E40AF), ); } } IconData _toastIcon(ToastType type) { switch (type) { case ToastType.success: return Icons.check_circle_outline; case ToastType.error: return Icons.error_outline; case ToastType.info: return Icons.info_outline; } } } class _ToastColorScheme { const _ToastColorScheme({ required this.background, required this.border, required this.foreground, }); final Color background; final Color border; final Color foreground; }