Contextless UI

Pub Version License: MIT

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

Demo

Contextless UI 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));

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:

  1. You've called init() with a valid key
  2. The widget tree has been built at least once
  3. 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.

Libraries

contextless_ui