Skip to content

Instantly share code, notes, and snippets.

@CoderNamedHendrick
Last active January 19, 2024 08:02
Show Gist options
  • Save CoderNamedHendrick/5d5fa70a42594caf10765d9a4077171f to your computer and use it in GitHub Desktop.
Save CoderNamedHendrick/5d5fa70a42594caf10765d9a4077171f to your computer and use it in GitHub Desktop.
A simple toast messaging service
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'),
),
],
),
);
}
}
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