Contextless UI
A Flutter package for displaying UI components without requiring BuildContext. Show dialogs, snackbars, toasts, and bottom sheets from anywhere in your code.
Demo

Features
- No BuildContext required - display UI from anywhere (services, controllers, etc.)
- Unified API for dialogs, snackbars, toasts, and bottom sheets
- Manage components by handle, ID, or tag
- Async support with return values
- Custom styling with decoration models
- Built-in transitions and animations
- Event streams for analytics
- Cross-platform support
Installation
dependencies:
contextless_ui: ^0.1.0
Quick Start
Initialize
Add ContextlessObserver to your MaterialApp:
import 'package:contextless_ui/contextless_ui.dart';
class MyApp extends StatelessWidget {
final navigatorKey = GlobalKey<NavigatorState>();
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: navigatorKey,
navigatorObservers: [ContextlessObserver()],
home: const MyHomePage(),
);
}
}
Unified API
All UI components can be accessed through the unified ContextlessUi class:
// Instead of using separate classes:
ContextlessDialogs.show(...)
ContextlessSnackbars.show(...)
ContextlessToasts.show(...)
ContextlessBottomSheets.show(...)
// Use the unified API:
ContextlessUi.showDialog(...)
ContextlessUi.showSnackbar(...)
ContextlessUi.showToast(...)
ContextlessUi.showBottomSheet(...)
Usage Examples
Dialogs
void showLoadingDialog() {
final handle = ContextlessUi.showDialog(
const Dialog(
child: Padding(
padding: EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Loading...'),
],
),
),
),
);
Future.delayed(const Duration(seconds: 3), () {
ContextlessUi.closeDialog(handle);
});
}
Snackbars
void showNotification() {
ContextlessUi.showSnackbar(
const Text('File uploaded successfully!'),
action: TextButton(
onPressed: () => openFile(),
child: const Text('View'),
),
decoration: const SnackbarDecoration(
backgroundColor: Colors.green,
),
);
}
Toasts
void showToast() {
ContextlessUi.showToast(
const Text('Operation completed'),
iconLeft: const Icon(Icons.check_circle, color: Colors.white),
decoration: const ToastDecoration(
backgroundColor: Colors.black87,
borderRadius: BorderRadius.all(Radius.circular(8)),
),
);
}
Bottom Sheets
void showSettings() {
ContextlessUi.showBottomSheet(
Container(
padding: const EdgeInsets.all(16),
child: const Text('Settings'),
),
decoration: const BottomSheetDecoration(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
),
);
}
Async Results
Future<String?> pickColor() async {
return await ContextlessUi.showDialogAsync<String>(
Dialog(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
onPressed: () => ContextlessUi.closeAllDialogs('red'),
child: const Text('Red'),
),
ElevatedButton(
onPressed: () => ContextlessUi.closeAllDialogs('blue'),
child: const Text('Blue'),
),
],
),
),
);
}
API Reference
Dialog API
// Show a dialog
DialogHandle show(
Widget dialog, {
String? id,
String? tag,
bool barrierDismissible = true,
DialogDecoration? decoration,
});
// Async dialog with return value
Future<T?> showAsync<T>(Widget dialog, {...});
// Close methods
ContextlessDialogs.close(handle);
ContextlessDialogs.closeById(id);
ContextlessDialogs.closeByTag(tag);
ContextlessDialogs.closeAll();
Snackbar API
// Show a snackbar
SnackbarHandle show(
Widget content, {
Widget? action,
Widget? iconLeft,
Widget? iconRight,
String? id,
String? tag,
Duration duration,
SnackbarDecoration? decoration,
});
// Decoration model
SnackbarDecoration({
Color? backgroundColor,
EdgeInsetsGeometry? margin,
EdgeInsetsGeometry? padding,
double? elevation,
ShapeBorder? shape,
SnackBarBehavior behavior,
// ... more properties
});
Toast API
// Show a toast
ToastHandle show(
Widget content, {
Widget? iconLeft,
Widget? iconRight,
String? id,
String? tag,
Duration duration,
Alignment alignment,
ToastDecoration? decoration,
});
// Decoration model
ToastDecoration({
Color? backgroundColor,
EdgeInsetsGeometry? padding,
BorderRadius? borderRadius,
double? elevation,
});
Bottom Sheet API
// Show a bottom sheet
BottomSheetHandle show(
Widget content, {
String? id,
String? tag,
bool isDismissible,
bool enableDrag,
BottomSheetDecoration? decoration,
});
// Decoration model
BottomSheetDecoration({
Color? backgroundColor,
double? elevation,
ShapeBorder? shape,
BoxConstraints? constraints,
});
Advanced Features
Tag-based Management
// Group components with tags
ContextlessUi.showDialog(dialog, tag: 'loading');
ContextlessUi.showToast(toast, tag: 'loading');
// Close all components with the same tag
ContextlessUi.closeDialogsByTag('loading');
Event Streams
// Listen to events
ContextlessDialogs.events.listen((event) {
print('Dialog ${event.handle.id} ${event.type}');
});
Custom Transitions
// Built-in transitions
DialogTransitions.fade
DialogTransitions.slideFromBottom
DialogTransitions.scale
// Use in decoration
ContextlessUi.showDialog(
dialog,
decoration: DialogDecoration(
transitionsBuilder: DialogTransitions.fade,
),
);
Mixed Component Usage
void showMixedComponents() {
// Show multiple component types together
final snackbar = ContextlessUi.showSnackbar(
const Text('Background task running'),
tag: 'background',
);
final dialog = ContextlessUi.showDialog(
const ProcessingDialog(),
tag: 'background',
);
final toast = ContextlessUi.showToast(
const Text('Starting process...'),
tag: 'background',
);
// Close all background components later
Timer(const Duration(seconds: 5), () {
ContextlessUi.closeSnackbarsByTag('background');
ContextlessUi.closeDialogsByTag('background');
ContextlessUi.closeToastsByTag('background');
});
}
Best Practices
1. Initialize Early
Always initialize before runApp():
void main() {
WidgetsFlutterBinding.ensureInitialized();
final navigatorKey = GlobalKey<NavigatorState>();
ContextlessUi.init(navigatorKey: navigatorKey);
runApp(MyApp(navigatorKey: navigatorKey));
}
2. Use Tags for Organization
Group related components for easier management:
// Progress components
ContextlessUi.showSnackbar(const Text('Step 1'), tag: 'wizard');
ContextlessUi.showToast(const Text('Step 2'), tag: 'wizard');
// Error components
ContextlessUi.showSnackbar(const Text('Error'), tag: 'error');
// Close all wizard components when done
ContextlessUi.closeSnackbarsByTag('wizard');
ContextlessUi.closeToastsByTag('wizard');
3. Handle Async Results Properly
Future<void> showConfirmationDialog() async {
final confirmed = await ContextlessUi.showDialogAsync<bool>(
const ConfirmationDialog(),
);
if (confirmed == true) {
// User confirmed
await performAction();
}
// Handle null (dismissed) vs false (cancelled)
}
4. Use Consistent Styling Patterns
// Instead of manually creating styled snackbars
ContextlessUi.showSnackbar(
const Text('Success!'),
decoration: const SnackbarDecoration(
backgroundColor: Colors.green,
),
);
// For common patterns, create helper functions
void showSuccessMessage(String message) {
ContextlessUi.showSnackbar(
Text(message),
iconLeft: const Icon(Icons.check_circle, color: Colors.white),
decoration: const SnackbarDecoration(
backgroundColor: Colors.green,
),
);
}
5. Listen to Events for Analytics
void initializeAnalytics() {
ContextlessUi.events.listen((event) {
analytics.track('ui_${event.type.name}', {
'component_type': event.handle.type.name,
'component_id': event.handle.id,
'component_tag': event.handle.tag,
'result': event.result,
});
});
}
6. Cleanup on App Disposal
class MyApp extends StatefulWidget {
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void dispose() {
ContextlessUi.dispose();
```dart
// Wrong
runApp(const MyApp());
ContextlessUi.init(navigatorKey: key); // Too late!
// Correct
ContextlessUi.init(navigatorKey: key);
runApp(MyApp(navigatorKey: key));
Navigator Key Not Working
Ensure the same key is passed to both init() and MaterialApp:
final navigatorKey = GlobalKey<NavigatorState>(); // Create once
ContextlessUi.init(navigatorKey: navigatorKey); // Use same key
runApp(MaterialApp(navigatorKey: navigatorKey, ...)); // Use same key
Components Not Appearing
Check that:
- You've called
init()with a valid key - The widget tree has been built at least once
- You're not calling from an isolate without proper context
Memory Leaks
Always dispose the system when your app shuts down:
@override
void dispose() {
ContextlessUi.dispose(); // This closes all components and cleans up
super.dispose();
}
Component Types
| Component | Use Case | Key Features |
|---|---|---|
| Dialog | Modal interactions | Barrier, transitions, blocking |
| Snackbar | Status updates | Material Design, actions, auto-dismiss |
| Toast | Simple notifications | Lightweight, flexible positioning |
| Bottom Sheet | Options, forms | Material Design, drag support |
Platform Support
- Android - Full support with Material Design
- iOS - Full support with Cupertino styling
- Web - Full support with responsive design
- Desktop - Windows, macOS, Linux support
- Embedded - Flutter embedded platforms
Examples
Check out the /example folder for a complete working example showcasing:
- All component types in action
- Async dialogs with results
- Tag-based component management
- Custom styling and transitions
- Service layer integration
- Builder patterns
- Event stream usage
License
MIT License - see the LICENSE file for details.
Contributing
Contributions are welcome. Fork the repository, create a feature branch, add tests, and submit a pull request.