π ToastKit
A smart, rule-driven toast and notification system for Flutter.
ToastKit goes beyond simple toasts β it provides a headless + UI hybrid notification engine with rule-based triggering, a plugin architecture, queue management, and 12+ ready-made toast variants. No BuildContext required.
β¨ Features
- No BuildContext required β show toasts from anywhere (services, blocs, repositories)
- Rule-based triggering β deduplicate, set error thresholds, limit max triggers per channel
- Plugin architecture β hook into lifecycle events for logging, analytics, haptics, and more
- 12+ built-in variants β Minimal, Material, iOS, Glassmorphism, Gradient, Compact, and more
- Custom UI builders β full control over toast rendering with your own widgets
- Queue management β FIFO, LIFO, priority modes with max-visible limits
- Channel system β group toasts by category (auth, network, payment) with per-channel policies
- Stateful toasts β loading β success/error transitions with
ToastController - 12 animation types β fade, slide, scale, bounce, elastic, spring, shake, blur, glow
- Gesture support β swipe dismiss, tap, hover pause, drag
- Persistence β save and restore critical toasts across app restarts
- Accessibility β semantics, screen reader support, keyboard avoidance
π¦ Installation
Add ToastKit to your pubspec.yaml:
dependencies:
toast_kit:
git:
url: https://github.com/yasersabri580-oss/toast-kit.git
Then run:
flutter pub get
π Quick Start
1. Initialize
import 'package:toast_kit/toast_kit.dart';
final navigatorKey = GlobalKey<NavigatorState>();
MaterialApp(
navigatorKey: navigatorKey,
home: const MyApp(),
);
// Initialize once after the first frame
WidgetsBinding.instance.addPostFrameCallback((_) {
ToastKit.init(navigatorKey: navigatorKey);
});
2. Show Toasts β Anywhere
ToastKit.success('File saved successfully!');
ToastKit.error('Connection lost');
ToastKit.warning('Battery below 20%');
ToastKit.info('New version available');
3. Stateful Loading β Result
final ctrl = ToastKit.showLoading('Uploading fileβ¦');
try {
await uploadFile();
ctrl.success('Upload complete!');
} catch (e) {
ctrl.error('Upload failed');
}
π Core Concepts
Toasts
A toast is a ToastEvent β the fundamental unit in ToastKit. Every toast has a type (success, error, warning, info, loading, custom), optional message, icon, position, animation, priority, and more.
// Using convenience factories
ToastKit.show(ToastEvent.success(message: 'Done!'));
ToastKit.show(ToastEvent.error(message: 'Oops', variant: ToastVariant.gradient));
// Using the full constructor for complete control
ToastKit.show(ToastEvent(
type: ToastType.info,
message: 'Custom event',
position: ToastPosition.bottom,
animation: ToastAnimationType.slideFromBottom,
priority: ToastPriority.high,
deduplicationKey: 'unique-info',
));
Rules
Rules let you define smart behavior based on toast activity β error thresholds, deduplication windows, trigger limits, and windowed rate detection per channel.
Config-Based Rules (Simple)
// Trigger after 5 errors on the "payment" channel,
// with a 30-second deduplication window and a maximum of 1 trigger.
ToastKit.configureRule(
'payment',
RuleConfig(
errorThreshold: 5, // Fire when errorCount >= 5
deduplicateWindow: Duration(seconds: 30), // Cooldown between triggers
maxTriggers: 1, // Fire at most once
),
);
Custom Rules (Full Control)
// Suggest password reset after 3 login failures (fire once)
ToastKit.addRule(ToastRule(
id: 'suggest-reset',
channel: 'auth',
maxTriggers: 1,
condition: (stats, event) => stats.errorCount >= 3,
action: (context) {
ToastKit.show(ToastEvent.info(
message: 'Forgot your password?',
variant: ToastVariant.action,
actions: [
ToastAction(
label: 'Reset Password',
onPressed: () => ToastKit.success('Reset email sent!'),
),
],
channel: 'auth',
));
},
));
Windowed Rate Detection
// Detect error bursts: 5+ errors within 30 seconds
ToastKit.addRule(ToastRule(
id: 'error-burst',
channel: 'network',
deduplicateWindow: Duration(seconds: 60),
condition: (stats, event) {
return stats.errorsInWindow(const Duration(seconds: 30)) >= 5;
},
action: (context) {
ToastKit.warning('Unstable connection detected');
},
));
Combined Stat Conditions
// Fire only when BOTH errors AND warnings are high
ToastKit.addRule(ToastRule(
id: 'sync-degraded',
channel: 'sync',
maxTriggers: 1,
condition: (stats, event) =>
stats.errorCount >= 2 &&
stats.warningCount >= 2 &&
stats.totalCount >= 6,
action: (context) {
ToastKit.info(
'Sync degraded: ${context.stats.errorCount} errors, '
'${context.stats.warningCount} warnings',
);
},
));
Dynamic Rule Management
// Add, remove, and re-register rules at runtime
ToastKit.addRule(ToastRule(id: 'guard', channel: 'session', ...));
// Remove when no longer needed (e.g., after re-authentication)
ToastKit.removeRule('guard');
// Reset stats but keep rules
ToastKit.ruleEngine.resetStats();
// Clear everything (rules + stats + trigger counts)
ToastKit.ruleEngine.clear();
Plugins
Plugins observe the toast lifecycle without blocking the core pipeline. Plugin errors are always caught β they never crash ToastKit.
class LoggerPlugin extends ToastPlugin {
@override
String get name => 'logger';
@override
void onToastShown(ToastEvent event) {
print('[TOAST] Shown: ${event.type.name} β ${event.message}');
}
@override
void onToastDismissed(ToastEvent event, DismissReason? reason) {
print('[TOAST] Dismissed: ${event.id} (${reason?.name ?? "auto"})');
}
}
// Register at init or later
ToastKit.init(navigatorKey: key, plugins: [LoggerPlugin()]);
// or
ToastKit.registerPlugin(LoggerPlugin());
Builders
Custom builders give you full control over toast UI. The builder receives a BuildContext and a ToastController for state management.
ToastKit.custom(builder: (context, controller) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.deepPurple,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
const Icon(Icons.rocket_launch, color: Colors.white),
const SizedBox(width: 12),
Text(
'Custom toast!',
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
const Spacer(),
IconButton(
icon: const Icon(Icons.close, color: Colors.white70),
onPressed: controller.dismiss,
),
],
),
);
});
π― Usage
Basic Toasts
ToastKit.success('Profile updated!');
ToastKit.error('Failed to load data');
ToastKit.warning('Disk space running low');
ToastKit.info('Syncing your dataβ¦');
Toast Variants
// Minimal style
ToastKit.show(ToastEvent.success(message: 'Saved', variant: ToastVariant.minimal));
// Glassmorphism (frosted glass)
ToastKit.show(ToastEvent.info(message: 'New message', variant: ToastVariant.glassmorphism));
// Gradient background
ToastKit.show(ToastEvent.error(message: 'Error!', variant: ToastVariant.gradient));
// Compact pill
ToastKit.show(ToastEvent.success(message: 'OK', variant: ToastVariant.compact));
// Full-width banner
ToastKit.show(ToastEvent.warning(message: 'Maintenance window', variant: ToastVariant.fullWidth));
// Action buttons
ToastKit.show(ToastEvent.error(
message: 'Send failed',
variant: ToastVariant.action,
actions: [
ToastAction(label: 'Retry', onPressed: () => retrySend()),
ToastAction(label: 'Cancel', onPressed: () {}),
],
));
Rule Configuration
Rules are configured per channel. A channel is a logical grouping for toasts (e.g., "auth", "network", "payment"). Both config-based and custom rules work together on the same channel.
// Register channels
ToastKit.registerChannel(ToastChannel.auth);
ToastKit.registerChannel(ToastChannel.payment);
// Config-based rule: after 10 errors on "payment", trigger once
ToastKit.configureRule(
'payment',
RuleConfig(
errorThreshold: 10, // Fire when errorCount >= 10
deduplicateWindow: Duration(seconds: 60), // 60s cooldown
maxTriggers: 1, // Fire at most once total
),
);
// Custom rule on the same channel for user-facing actions
ToastKit.addRule(ToastRule(
id: 'payment-help',
channel: 'payment',
maxTriggers: 1,
condition: (stats, event) => stats.errorCount >= 5,
action: (context) {
ToastKit.show(ToastEvent.info(
message: 'Need help? Contact support.',
variant: ToastVariant.action,
actions: [
ToastAction(
label: 'Contact Support',
onPressed: () => openSupportChat(),
),
],
channel: 'payment',
));
},
));
// Send errors on that channel β rules evaluate automatically
ToastKit.error('Payment declined', channel: 'payment');
Rule Properties Reference
| Property | Default | Description |
|---|---|---|
RuleConfig.errorThreshold |
5 |
Fire when errorCount >= threshold |
RuleConfig.deduplicateWindow |
30s |
Cooldown between triggers |
RuleConfig.maxTriggers |
0 (unlimited) |
Total trigger limit |
ToastRule.maxTriggers |
0 (unlimited) |
Total trigger limit |
ToastRule.deduplicateWindow |
null |
Cooldown between triggers |
Available ToastStats Fields
| Field | Description |
|---|---|
totalCount |
All events regardless of type |
errorCount |
Error events only |
warningCount |
Warning events only |
successCount |
Success events only |
infoCount |
Info events only |
dismissedCount |
Toasts dismissed by user |
droppedCount |
Toasts dropped (channel full, dedup) |
errorsInWindow(Duration) |
Errors within a sliding time window |
Using Keys with Rules
Deduplication keys prevent the same toast from appearing multiple times:
// These two calls produce only one visible toast
ToastKit.show(ToastEvent.error(
message: 'Network unavailable',
deduplicationKey: 'network-error',
channel: 'network',
));
ToastKit.show(ToastEvent.error(
message: 'Network unavailable',
deduplicationKey: 'network-error',
channel: 'network',
));
Plugin Implementation and Registration
class AnalyticsPlugin extends ToastPlugin {
@override
String get name => 'analytics';
@override
void onToastShown(ToastEvent event) {
// Forward to your analytics service
analytics.track('toast_shown', {
'type': event.type.name,
'message': event.message,
'channel': event.channel,
});
}
@override
void onRuleTriggered(String ruleId, String channel) {
analytics.track('toast_rule_triggered', {
'rule_id': ruleId,
'channel': channel,
});
}
}
// Register at init
ToastKit.init(
navigatorKey: navigatorKey,
plugins: [AnalyticsPlugin()],
);
// Or register later
ToastKit.registerPlugin(AnalyticsPlugin());
Custom UI Builder
ToastKit.show(ToastEvent.custom(
builder: (context, controller) {
return Card(
color: Colors.black87,
child: ListTile(
leading: const CircularProgressIndicator(color: Colors.white),
title: ValueListenableBuilder<String>(
valueListenable: controller.messageNotifier,
builder: (_, msg, __) =>
Text(msg, style: const TextStyle(color: Colors.white)),
),
trailing: IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: controller.dismiss,
),
),
);
},
duration: const Duration(seconds: 5),
position: ToastPosition.bottom,
));
Queue Handling
ToastKit.init(
navigatorKey: navigatorKey,
config: const ToastConfig(
maxVisibleToasts: 3, // Show up to 3 toasts at once
enableQueue: true, // Queue extras instead of dropping
queueMode: QueueMode.fifo, // First-in, first-out
),
routerConfig: const RouterConfig(
enableDeduplication: true,
deduplicationWindow: Duration(seconds: 2),
replacementStrategy: ReplacementStrategy.dropNew,
),
);
// Rapid-fire: only 3 visible, rest queued
for (var i = 1; i <= 10; i++) {
ToastKit.info('Notification #$i');
}
Global Configuration
ToastKit.init(
navigatorKey: navigatorKey,
config: const ToastConfig(
defaultPosition: ToastPosition.top,
defaultDuration: Duration(seconds: 4),
maxVisibleToasts: 3,
enableQueue: true,
queueMode: QueueMode.fifo,
defaultAnimation: ToastAnimationType.slideFromTop,
safeAreaEnabled: true,
keyboardAvoidance: true,
density: ToastDensity.comfortable,
toastSpacing: 8.0,
),
routerConfig: const RouterConfig(
enableDeduplication: true,
deduplicationWindow: Duration(seconds: 2),
enableThrottling: false,
replacementStrategy: ReplacementStrategy.dropNew,
),
channels: [
ToastChannel.auth,
ToastChannel.network,
ToastChannel.payment,
],
plugins: [LoggerPlugin(), AnalyticsPlugin()],
);
π₯ Advanced Usage
Preventing Toast Spam
Use deduplication and throttling to prevent users from seeing the same toast repeatedly:
ToastKit.init(
navigatorKey: navigatorKey,
routerConfig: const RouterConfig(
enableDeduplication: true,
deduplicationWindow: Duration(seconds: 5),
enableThrottling: true,
),
);
// Even if called 100 times, only one toast appears within the 5-second window
void onNetworkError() {
ToastKit.show(ToastEvent.error(
message: 'No internet connection',
deduplicationKey: 'no-internet',
));
}
API Error Handling
Future<void> fetchUserProfile() async {
final ctrl = ToastKit.showLoading('Loading profileβ¦');
try {
final user = await api.getProfile();
ctrl.success('Welcome back, ${user.name}!');
} on NotFoundException {
ctrl.error('Profile not found');
} on TimeoutException {
ctrl.error('Request timed out β please try again');
} catch (e) {
ctrl.error('Something went wrong');
ToastKit.error('Error: $e', channel: 'network');
}
}
Form Validation
void onSubmitForm(String email, String password) {
final errors = <String>[];
if (email.isEmpty || !email.contains('@')) {
errors.add('Please enter a valid email');
}
if (password.length < 8) {
errors.add('Password must be at least 8 characters');
}
if (errors.isNotEmpty) {
for (final error in errors) {
ToastKit.warning(error, channel: 'form');
}
return;
}
// Proceed with submission
submitForm(email, password);
}
Login Attempt Limiting
// Register auth channel and configure escalating rules
ToastKit.registerChannel(ToastChannel.auth);
// Config-based rule: fire analytics callback after 3 errors
ToastKit.configureRule(
'auth',
RuleConfig(
errorThreshold: 3,
deduplicateWindow: Duration(seconds: 60),
maxTriggers: 1,
),
);
// Custom rule: suggest password reset after 3 failures
ToastKit.addRule(ToastRule(
id: 'suggest-reset',
channel: 'auth',
maxTriggers: 1,
condition: (stats, event) =>
stats.errorCount >= 3 && stats.errorCount < 5,
action: (context) {
ToastKit.show(ToastEvent.info(
message: 'Forgot your password?',
variant: ToastVariant.action,
deduplicationKey: 'suggest-reset',
actions: [
ToastAction(
label: 'Reset Password',
onPressed: () => ToastKit.success('Reset email sent!'),
),
],
channel: 'auth',
));
},
));
// Custom rule: lock account after 5 failures
ToastKit.addRule(ToastRule(
id: 'login-lockout',
channel: 'auth',
maxTriggers: 1,
condition: (stats, event) => stats.errorCount >= 5,
action: (context) {
setState(() => _isLocked = true);
ToastKit.show(ToastEvent.error(
message: 'Account locked for 30 seconds.',
persistent: true,
dismissible: false,
deduplicationKey: 'login-lockout',
channel: 'auth',
));
},
));
Future<void> attemptLogin(String email, String password) async {
final ctrl = ToastKit.showLoading('Signing inβ¦');
try {
await authService.login(email, password);
ctrl.success('Welcome back!');
} catch (e) {
ctrl.error('Invalid credentials');
// Error recorded on auth channel β rules evaluate automatically
ToastKit.error('Login failed', channel: 'auth');
}
}
Payment Failure Scenario
ToastKit.registerChannel(ToastChannel.payment);
// Step 1: Warning after 2 failures
ToastKit.addRule(ToastRule(
id: 'payment-warn',
channel: 'payment',
maxTriggers: 1,
condition: (stats, event) =>
stats.errorCount >= 2 && stats.errorCount < 4,
action: (context) {
ToastKit.warning(
'Multiple payment failures. Check your card details.',
channel: 'payment',
);
},
));
// Step 2: Block and offer recovery after 4 failures
ToastKit.addRule(ToastRule(
id: 'payment-block',
channel: 'payment',
maxTriggers: 1,
condition: (stats, event) => stats.errorCount >= 4,
action: (context) {
ToastKit.show(ToastEvent.error(
message: 'Payment processing suspended.',
persistent: true,
variant: ToastVariant.action,
deduplicationKey: 'payment-block-toast',
actions: [
ToastAction(
label: 'Switch Card',
onPressed: () => switchPaymentMethod(),
),
ToastAction(
label: 'Use PayPal',
onPressed: () => redirectToPayPal(),
),
ToastAction(
label: 'Contact Support',
onPressed: () => openSupportChat(),
),
],
channel: 'payment',
));
},
));
Future<void> processPayment(double amount) async {
final ctrl = ToastKit.showLoading('Processing paymentβ¦');
try {
await paymentService.charge(amount);
ctrl.success('Payment of \$${amount.toStringAsFixed(2)} successful!');
} on PaymentDeclinedException {
ctrl.error('Card declined β please try another card');
ToastKit.error('Payment declined', channel: 'payment');
} on InsufficientFundsException {
ctrl.error('Insufficient funds');
ToastKit.error('Insufficient funds', channel: 'payment');
} catch (e) {
ctrl.error('Payment failed');
ToastKit.error('Payment error', channel: 'payment');
}
}
Network Retry Messaging
ToastKit.registerChannel(ToastChannel.network);
// Config rule for general error tracking
ToastKit.configureRule(
'network',
RuleConfig(
errorThreshold: 3,
deduplicateWindow: Duration(seconds: 10),
maxTriggers: 2,
),
);
// Custom rule: detect error bursts using errorsInWindow()
ToastKit.addRule(ToastRule(
id: 'network-burst',
channel: 'network',
deduplicateWindow: Duration(seconds: 30),
condition: (stats, event) {
return stats.errorsInWindow(const Duration(seconds: 15)) >= 3;
},
action: (context) {
ToastKit.show(ToastEvent.error(
message: 'Connection unstable. Check your network.',
persistent: true,
variant: ToastVariant.action,
deduplicationKey: 'network-unstable',
actions: [
ToastAction(
label: 'Retry',
onPressed: () {
ToastKit.dismissAll();
ToastKit.info('Retryingβ¦');
},
),
],
channel: 'network',
));
},
));
Future<T> fetchWithRetry<T>(
Future<T> Function() request, {
int maxRetries = 3,
}) async {
for (var attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await request();
} catch (e) {
ToastKit.error(
'Retry $attempt/$maxRetries failed',
channel: 'network',
);
if (attempt == maxRetries) {
ToastKit.show(ToastEvent.error(
message: 'All retries exhausted. Check your connection.',
variant: ToastVariant.action,
actions: [
ToastAction(
label: 'Retry',
onPressed: () => fetchWithRetry(request),
),
],
channel: 'network',
));
rethrow;
}
await Future.delayed(Duration(seconds: attempt * 2));
}
}
throw StateError('Unreachable');
}
π Plugin System
Plugin Interface
All plugins extend ToastPlugin and override lifecycle hooks:
abstract class ToastPlugin {
String get name;
void onToastShown(ToastEvent event) {}
void onToastQueued(ToastEvent event) {}
void onToastDismissed(ToastEvent event, DismissReason? reason) {}
void onToastDropped(ToastEvent event, String reason) {}
void onToastReplaced(ToastEvent newEvent, String replacedId) {}
void onToastAction(ToastEvent event, String actionLabel) {}
void onChannelRegistered(String channelId) {}
void onRuleTriggered(String ruleId, String channel) {}
void onTelemetryEvent(ToastTelemetryEvent telemetryEvent) {}
void onAttach() {}
void onDetach() {}
}
LoggerPlugin
class LoggerPlugin extends ToastPlugin {
@override
String get name => 'logger';
@override
void onAttach() => print('[ToastKit:Logger] Plugin attached');
@override
void onDetach() => print('[ToastKit:Logger] Plugin detached');
@override
void onToastShown(ToastEvent event) {
print('[ToastKit:Logger] SHOWN β ${event.type.name}: "${event.message}"'
'${event.channel != null ? " [${event.channel}]" : ""}');
}
@override
void onToastQueued(ToastEvent event) {
print('[ToastKit:Logger] QUEUED β ${event.id}');
}
@override
void onToastDismissed(ToastEvent event, DismissReason? reason) {
print('[ToastKit:Logger] DISMISSED β ${event.id} '
'(${reason?.name ?? "auto"})');
}
@override
void onToastDropped(ToastEvent event, String reason) {
print('[ToastKit:Logger] DROPPED β ${event.id}: $reason');
}
@override
void onRuleTriggered(String ruleId, String channel) {
print('[ToastKit:Logger] RULE TRIGGERED β $ruleId on "$channel"');
}
}
AnalyticsPlugin
class AnalyticsPlugin extends ToastPlugin {
@override
String get name => 'analytics';
@override
void onToastShown(ToastEvent event) {
_trackEvent('toast_shown', {
'type': event.type.name,
'message': event.message ?? '',
'channel': event.channel ?? 'default',
'variant': event.variant?.name ?? 'default',
});
}
@override
void onToastDismissed(ToastEvent event, DismissReason? reason) {
_trackEvent('toast_dismissed', {
'toast_id': event.id,
'dismiss_reason': reason?.name ?? 'auto',
});
}
@override
void onRuleTriggered(String ruleId, String channel) {
_trackEvent('toast_rule_triggered', {
'rule_id': ruleId,
'channel': channel,
});
}
void _trackEvent(String name, Map<String, String> params) {
// Replace with your analytics SDK
print('[Analytics] $name: $params');
}
}
HapticsPlugin
import 'package:flutter/services.dart';
class HapticsPlugin extends ToastPlugin {
@override
String get name => 'haptics';
@override
void onToastShown(ToastEvent event) {
switch (event.type) {
case ToastType.error:
HapticFeedback.heavyImpact();
break;
case ToastType.warning:
HapticFeedback.mediumImpact();
break;
case ToastType.success:
HapticFeedback.lightImpact();
break;
default:
HapticFeedback.selectionClick();
break;
}
}
}
Registering Plugins
// At init time
ToastKit.init(
navigatorKey: navigatorKey,
plugins: [
LoggerPlugin(),
AnalyticsPlugin(),
HapticsPlugin(),
],
);
// Or after init
ToastKit.registerPlugin(LoggerPlugin());
// Remove a plugin
ToastKit.unregisterPlugin('logger');
Event flow: ToastKit.show() β router β queue β overlay engine. At each stage, PluginHub notifies all registered plugins via onToastQueued, onToastShown, onToastDismissed, etc.
π API Overview
| Method | Description |
|---|---|
ToastKit.init(...) |
Initialize the SDK (required once) |
ToastKit.show(event) |
Show a ToastEvent |
ToastKit.success(msg) |
Show a success toast |
ToastKit.error(msg) |
Show an error toast |
ToastKit.warning(msg) |
Show a warning toast |
ToastKit.info(msg) |
Show an info toast |
ToastKit.showLoading(msg) |
Show loading toast, returns ToastController |
ToastKit.loading(msg) |
Show loading toast (fire-and-forget) |
ToastKit.custom(builder: ...) |
Show toast with custom builder |
ToastKit.configureRule(ch, config) |
Set a config-based rule for a channel |
ToastKit.addRule(rule) |
Add a custom ToastRule |
ToastKit.removeRule(id) |
Remove a custom rule by ID |
ToastKit.ruleEngine.resetStats() |
Reset all stats but keep rules |
ToastKit.ruleEngine.clear() |
Remove all rules, stats, and trigger counts |
ToastKit.registerChannel(ch) |
Register a ToastChannel |
ToastKit.registerPlugin(plugin) |
Register a ToastPlugin |
ToastKit.unregisterPlugin(name) |
Remove a plugin |
ToastKit.dismiss(id) |
Dismiss a specific toast |
ToastKit.dismissAll() |
Dismiss all visible toasts |
ToastKit.dispose() |
Release all resources |
ToastKit.channel(name) |
Get a fluent ChannelHandle |
ToastKit.eventStream |
Broadcast stream of all toast events |
ToastKit.restorePersistedToasts() |
Restore saved persistent toasts |
π± Example App
See the example/ directory for a complete demo app with real-world scenarios.
import 'package:flutter/material.dart';
import 'package:toast_kit/toast_kit.dart';
void main() => runApp(const MyApp());
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final _navKey = GlobalKey<NavigatorState>();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
ToastKit.init(
navigatorKey: _navKey,
config: const ToastConfig(
defaultPosition: ToastPosition.top,
maxVisibleToasts: 3,
),
channels: [ToastChannel.auth, ToastChannel.payment],
);
ToastKit.configureRule('auth', RuleConfig(
errorThreshold: 3,
deduplicateWindow: Duration(seconds: 60),
maxTriggers: 1,
));
});
}
@override
void dispose() {
ToastKit.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: _navKey,
home: Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () => ToastKit.success('Hello, ToastKit!'),
child: const Text('Show Toast'),
),
),
),
);
}
}
π Folder Structure
toast_kit/
βββ lib/
β βββ toast_kit.dart # Barrel export
β βββ src/
β βββ core/ # Config + SDK singleton
β βββ events/ # ToastEvent + EventBus
β βββ queue/ # FIFO/LIFO/priority queue
β βββ router/ # Dedup, throttle, replacement
β βββ overlay/ # OverlayEntry lifecycle
β βββ channels/ # Channel definitions + policies
β βββ rules/ # Rule engine + config + stats
β βββ plugins/ # Plugin base class + hub
β βββ persistence/ # Save/restore critical toasts
β βββ animation/ # 12 animations + factory
β βββ gestures/ # Swipe/tap/hover
β βββ theme/ # Design tokens + provider
β βββ layout/ # Position calculator
β βββ stacking/ # Smart group stacking
β βββ accessibility/ # Screen reader support
β βββ analytics/ # Telemetry events
β βββ debug/ # Debug overlay
β βββ variants/ # 12+ toast variants
βββ example/
β βββ lib/
β β βββ main.dart # Full demo app
β β βββ scenarios/
β β βββ api_error.dart # API failure handling
β β βββ form_validation.dart # Form validation errors
β β βββ login_rules.dart # Login retry limits
β β βββ payment_failure.dart # Payment failure with rules
β β βββ network_retry.dart # Network retry messaging
β β βββ custom_ui.dart # Custom toast builder
β βββ pubspec.yaml
βββ test/
βββ pubspec.yaml
βββ README.md
βββ VIDEO_TUTORIAL_PLAN.md
βββ analysis_options.yaml
π§ Troubleshooting
Toast not showing
- Ensure
ToastKit.init()is called after the first frame:WidgetsBinding.instance.addPostFrameCallback((_) { ToastKit.init(navigatorKey: navigatorKey); }); - Verify
navigatorKeyis the same instance passed toMaterialApp.
Toasts getting dropped
- Check if deduplication is enabled and the
deduplicationKeyis the same. - Check if the channel's
maxVisiblelimit is reached. - Verify
enableQueue: trueinToastConfigto queue overflow toasts.
Rules not triggering
- Ensure the channel is registered with
ToastKit.registerChannel(...). - Verify toast events include the correct
channelparameter. - Check
RuleConfig.errorThresholdβ the rule only triggers after this many errors. - Check
maxTriggersβ a value of1means it triggers only once.
Plugin not receiving events
- Confirm the plugin is registered via
ToastKit.init(plugins: [...])orToastKit.registerPlugin(...). - Plugin names must be unique β a second plugin with the same name replaces the first.
π€ Contributing
Contributions are welcome! Please follow these steps:
- Fork the repository
- Create a feature branch:
git checkout -b feature/my-feature - Make your changes and add tests
- Run tests:
flutter test - Commit:
git commit -m "feat: add my feature" - Push and open a pull request
Please ensure:
- All existing tests pass
- New features include test coverage
- Code follows the existing style (run
flutter analyze)
π License
MIT Β© 2026 ToastKit Contributors
Libraries
- toast_kit
- ToastKit SDK β A production-grade Flutter notification framework.