Last active
January 19, 2024 08:02
-
-
Save CoderNamedHendrick/5d5fa70a42594caf10765d9a4077171f to your computer and use it in GitHub Desktop.
A simple toast messaging service
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
import 'package:notification_poc/toast_notification.dart'; | |
void main() { | |
runApp(const MyApp()); | |
} | |
class MyApp extends StatefulWidget { | |
const MyApp({super.key}); | |
@override | |
State<MyApp> createState() => _MyAppState(); | |
} | |
class _MyAppState extends State<MyApp> { | |
// This widget is the root of your application. | |
@override | |
Widget build(BuildContext context) { | |
return ToastNotification.builder( | |
builder: (_) => MaterialApp( | |
title: 'Flutter Demo', | |
theme: ThemeData( | |
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), | |
useMaterial3: true, | |
), | |
home: const MyHomePage(title: 'Flutter Demo Home Page'), | |
), | |
); | |
} | |
} | |
class MyHomePage extends StatefulWidget { | |
const MyHomePage({super.key, required this.title}); | |
final String title; | |
@override | |
State<MyHomePage> createState() => _MyHomePageState(); | |
} | |
class _MyHomePageState extends State<MyHomePage> { | |
int _counter = 0; | |
void _incrementCounter() { | |
setState(() { | |
_counter++; | |
}); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
backgroundColor: Theme.of(context).colorScheme.inversePrimary, | |
title: Text(widget.title), | |
), | |
body: Center( | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: <Widget>[ | |
TextButton( | |
onPressed: () { | |
final toast1 = Container( | |
height: 200 + MediaQuery.viewPaddingOf(context).bottom, | |
width: double.infinity, | |
margin: | |
const EdgeInsets.symmetric(horizontal: 16, vertical: 16), | |
decoration: const BoxDecoration( | |
color: Colors.blue, | |
boxShadow: [ | |
BoxShadow( | |
color: Colors.black12, | |
blurRadius: 10, | |
offset: Offset(0, 5), | |
), | |
BoxShadow( | |
color: Colors.black12, | |
blurRadius: 10, | |
offset: Offset(-1, 5), | |
), | |
], | |
), | |
padding: const EdgeInsets.all(16), | |
child: Row( | |
children: [ | |
const Text('An example notification toast'), | |
IconButton( | |
onPressed: () { | |
ToastNotification.of(context).hideNotification(); | |
}, | |
icon: const Icon(Icons.close), | |
), | |
], | |
), | |
); | |
ToastNotification.of(context).showNotification( | |
toast1, | |
direction: ToastDirection.bottom, | |
entryDuration: const Duration(milliseconds: 500), | |
dismissDuration: const Duration(seconds: 4), | |
); | |
}, | |
child: const Text('Show Toast'), | |
), | |
const SizedBox(height: 10), | |
TextButton( | |
onPressed: () { | |
Navigator.of(context).push( | |
MaterialPageRoute(builder: (context) => const NextPage()), | |
); | |
}, | |
child: const Text('Go to next page'), | |
), | |
const Text( | |
'You have pushed the button this many times:', | |
), | |
Text( | |
'$_counter', | |
style: Theme.of(context).textTheme.headlineMedium, | |
), | |
], | |
), | |
), | |
floatingActionButton: FloatingActionButton( | |
onPressed: _incrementCounter, | |
tooltip: 'Increment', | |
child: const Icon(Icons.add), | |
), | |
); | |
} | |
} | |
class NextPage extends StatelessWidget { | |
const NextPage({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: const Text('Flutter Demo Second Page'), | |
), | |
body: Column( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: [ | |
TextButton( | |
onPressed: () { | |
final toast1 = Container( | |
height: 200 + MediaQuery.viewPaddingOf(context).bottom, | |
width: double.infinity, | |
margin: | |
const EdgeInsets.symmetric(horizontal: 16, vertical: 16), | |
decoration: const BoxDecoration( | |
color: Colors.blue, | |
boxShadow: [ | |
BoxShadow( | |
color: Colors.black12, | |
blurRadius: 10, | |
offset: Offset(0, 5), | |
), | |
BoxShadow( | |
color: Colors.black12, | |
blurRadius: 10, | |
offset: Offset(-1, 5), | |
), | |
], | |
), | |
padding: const EdgeInsets.all(16), | |
child: Row( | |
children: [ | |
const Text('An example notification toast'), | |
IconButton( | |
onPressed: () { | |
ToastNotification.of(context).hideNotification(); | |
}, | |
icon: const Icon(Icons.close), | |
), | |
], | |
), | |
); | |
ToastNotification.of(context).showNotification( | |
toast1, | |
direction: ToastDirection.bottom, | |
entryDuration: const Duration(milliseconds: 500), | |
dismissDuration: const Duration(seconds: 4), | |
); | |
ToastNotification.of(context).showNotification( | |
toast1, | |
direction: ToastDirection.top, | |
entryDuration: const Duration(milliseconds: 500), | |
isDismissible: false, | |
); | |
}, | |
child: const Text('Show Toast'), | |
), | |
], | |
), | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:collection'; | |
import 'package:flutter/material.dart'; | |
const _kEntryDuration = Duration(milliseconds: 300); | |
const Curve _snackBarFadeInCurve = Interval(0, 0.5); | |
enum ToastDirection { | |
top, | |
bottom, | |
} | |
typedef ToastNotificationBuilder = Widget Function( | |
ToastNotificationController controller); | |
/// This is used to provider a context for ToastNotification | |
/// to show toast notifications | |
/// This is done so ToastNotificationState can be accessed via the context | |
class ToastNotification extends StatefulWidget { | |
const ToastNotification({super.key, required this.child}); | |
final Widget child; | |
/// Use static builder to wrap your app with ToastNotification | |
/// Use this widget before Material/Cupertino/Widget app to ensure overlay | |
/// .showNotification can be called anywhere in the app without wrapping pages | |
/// where it is needed with ToastNotification. | |
static Widget builder({required WidgetBuilder builder}) { | |
return Directionality( | |
textDirection: TextDirection.ltr, | |
child: Overlay( | |
initialEntries: [ | |
OverlayEntry( | |
builder: (context) => ToastNotification( | |
child: builder(context), | |
), | |
), | |
], | |
), | |
); | |
} | |
static ToastNotificationState of(BuildContext context) { | |
final _ToastNotificationScope scope = | |
context.dependOnInheritedWidgetOfExactType<_ToastNotificationScope>()!; | |
return scope._toastState; | |
} | |
static ToastNotificationState? maybeOf(BuildContext context) { | |
final _ToastNotificationScope? scope = | |
context.dependOnInheritedWidgetOfExactType<_ToastNotificationScope>(); | |
return scope?._toastState; | |
} | |
@override | |
State<ToastNotification> createState() => ToastNotificationState(); | |
} | |
class ToastNotificationState extends State<ToastNotification> { | |
final Queue<ToastNotificationController> _entries = | |
Queue<ToastNotificationController>(); | |
/// displays widget child as notification entry | |
ToastNotificationController? showNotification( | |
Widget child, { | |
Duration entryDuration = _kEntryDuration, | |
ToastDirection direction = ToastDirection.bottom, | |
Duration dismissDuration = const Duration(seconds: 4), | |
bool isDismissible = true, | |
}) { | |
final index = _entries.length; | |
child = _AnimatedToast( | |
duration: entryDuration, | |
onControllerInitialised: (controller) => _setControllerForEntry( | |
controller, | |
index, | |
), | |
direction: direction, | |
child: child, | |
); | |
final entry = _showOverlay(child, entryDuration); | |
return _addOverlayEntry(entry, dismissDuration, isDismissible); | |
} | |
OverlayEntry _showOverlay(Widget child, Duration duration) { | |
return OverlayEntry( | |
builder: (context) { | |
return Material( | |
type: MaterialType.transparency, | |
child: Stack( | |
children: [ | |
child, | |
Positioned.fill( | |
child: Listener( | |
behavior: HitTestBehavior.translucent, | |
onPointerDown: _removeFirstOverlay, | |
onPointerSignal: _removeFirstOverlay, | |
onPointerCancel: _removeFirstOverlay, | |
onPointerMove: _removeFirstOverlay, | |
onPointerUp: _removeFirstOverlay, | |
), | |
), | |
], | |
), | |
); | |
}, | |
); | |
} | |
ToastNotificationController _addOverlayEntry(OverlayEntry entry, | |
[Duration? dismissDuration, bool isDismissible = true]) { | |
ToastNotificationController controller = | |
ToastNotificationController(entry, null); | |
_entries.add(controller); | |
Overlay.of(context, debugRequiredFor: widget).insert(entry); | |
if (isDismissible) { | |
Future.delayed(dismissDuration ?? const Duration(seconds: 3), () { | |
_removeOverlayEntry(controller); | |
}); | |
} | |
return controller; | |
} | |
void hideNotification() { | |
_removeFirstOverlay(); | |
} | |
void _removeOverlayEntry(ToastNotificationController controller) { | |
if (controller.isDismissed || !controller.isCompleted) return; | |
controller.controller?.reverse().whenComplete(controller.dispose); | |
_entries.remove(controller); | |
} | |
void _removeFirstOverlay([_]) { | |
if (_entries.isEmpty) return; | |
if (!_entries.first.isCompleted) return; | |
_removeOverlayEntry(_entries.first); | |
} | |
void _setControllerForEntry(AnimationController animController, int index) { | |
if (_entries.isEmpty) return; | |
final controller = _entries.elementAt(index); | |
controller.controller = animController; | |
} | |
@override | |
void dispose() { | |
for (var controller in _entries) { | |
controller.dispose(); | |
} | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return _ToastNotificationScope( | |
toastState: this, | |
child: widget.child, | |
); | |
} | |
} | |
class _AnimatedToast extends StatefulWidget { | |
const _AnimatedToast({ | |
required this.child, | |
this.onControllerInitialised, | |
required this.duration, | |
this.direction = ToastDirection.bottom, | |
}); | |
final Widget child; | |
final void Function(AnimationController controller)? onControllerInitialised; | |
final Duration duration; | |
final ToastDirection direction; | |
@override | |
State<_AnimatedToast> createState() => _AnimatedToastState(); | |
} | |
class _AnimatedToastState extends State<_AnimatedToast> | |
with SingleTickerProviderStateMixin { | |
late AnimationController controller; | |
late Animation<double> _fadeInAnimation; | |
late AlignmentDirectional alignment; | |
@override | |
void initState() { | |
super.initState(); | |
controller = AnimationController(vsync: this, duration: widget.duration); | |
_fadeInAnimation = | |
CurvedAnimation(parent: controller, curve: _snackBarFadeInCurve); | |
widget.onControllerInitialised?.call(controller); | |
alignment = switch (widget.direction) { | |
ToastDirection.top => AlignmentDirectional.topCenter, | |
ToastDirection.bottom => AlignmentDirectional.bottomCenter, | |
}; | |
controller.forward(); | |
} | |
@override | |
void dispose() { | |
controller.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return AnimatedBuilder( | |
animation: controller, | |
builder: (context, child) { | |
return Align( | |
alignment: alignment, | |
child: SizedBox( | |
height: MediaQuery.sizeOf(context).height * controller.value, | |
child: FadeTransition( | |
opacity: _fadeInAnimation, | |
child: Align( | |
alignment: alignment, | |
child: child!, | |
), | |
), | |
), | |
); | |
}, | |
child: widget.child, | |
); | |
} | |
} | |
class _ToastNotificationScope extends InheritedWidget { | |
const _ToastNotificationScope({ | |
Key? key, | |
required Widget child, | |
required ToastNotificationState toastState, | |
}) : _toastState = toastState, | |
super(key: key, child: child); | |
final ToastNotificationState _toastState; | |
@override | |
bool updateShouldNotify(_ToastNotificationScope old) => | |
_toastState != old._toastState; | |
} | |
class ToastNotificationController { | |
ToastNotificationController(this.entry, this.controller); | |
OverlayEntry? entry; | |
AnimationController? controller; | |
bool get isCompleted => controller?.status == AnimationStatus.completed; | |
bool get isDismissed => entry == null; | |
void dispose() { | |
entry?.remove(); | |
entry?.dispose(); | |
controller = null; | |
entry = null; | |
} | |
@override | |
bool operator ==(Object other) { | |
if (other is! ToastNotificationController) return false; | |
return entry == other.entry && controller == other.controller; | |
} | |
@override | |
int get hashCode => entry.hashCode ^ controller.hashCode; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment