route_lifecycle_detector 1.0.0
route_lifecycle_detector: ^1.0.0 copied to clipboard
A lightweight library for detecting Navigator/ModalRoute/BuildContext lifecycles.
A lightweight library for detecting Navigator/ModalRoute/BuildContext lifecycles. It solves dialog multiple display issues caused by multiple asynchronous processes and enables UI display at appropriate timing.
Problem Statement #
Typical problems that occur when multiple asynchronous processes are executed:
- Two heavy processes run in parallel on Screen A
- Process 1 completes and Screen B opens
- Process 2 completes and a dialog is displayed on top of Screen B
Using this library, you can automatically delay dialog display until Screen B closes and returns to Screen A.
Features #
- Prevent Multiple Dialog Display: Delay UI display while Route is inactive
- Lifecycle State Detection: Real-time detection of Route's current state (active/inactive/hidden/destroyed)
- Stream-based Monitoring: Monitor Route lifecycle changes via Stream
- Lightweight Design: Minimal dependencies and performance overhead
- Async Process Waiting: Wait for Route to reach appropriate state
Getting started #
Add the library to your pubspec.yaml:
dependencies:
route_lifecycle_detector: ^1.0.0
Add RouteLifecycleDetector observer to MaterialApp:
import 'package:route_lifecycle_detector/route_lifecycle_detector.dart';
MaterialApp(
navigatorObservers: [
RouteLifecycleDetector.navigatorObserver,
],
)
Usage #
Problem Example: Multiple Dialog Display #
// Bad example: Unintended multiple dialog display due to multiple async processes
class BadExamplePage extends StatefulWidget {
@override
_BadExamplePageState createState() => _BadExamplePageState();
}
class _BadExamplePageState extends State<BadExamplePage> {
@override
void initState() {
super.initState();
// Heavy process 1: Open Screen B after 2 seconds
Future.delayed(Duration(seconds: 2), () {
Navigator.push(context, MaterialPageRoute(builder: (_) => PageB()));
});
// Heavy process 2: Open dialog after 3 seconds (Problem occurs!)
Future.delayed(Duration(seconds: 3), () {
showDialog(
context: context,
builder: (_) => AlertDialog(title: Text('Task 2 Complete')),
); // Dialog appears on top of Screen B
});
}
@override
Widget build(BuildContext context) => Scaffold(/*...*/);
}
Solution: Lifecycle-aware Processing #
import 'package:route_lifecycle_detector/route_lifecycle_detector.dart';
class GoodExamplePage extends StatefulWidget {
@override
_GoodExamplePageState createState() => _GoodExamplePageState();
}
class _GoodExamplePageState extends State<GoodExamplePage> {
@override
void initState() {
super.initState();
// Heavy process 1: Open Screen B after 2 seconds
Future.delayed(Duration(seconds: 2), () {
Navigator.push(context, MaterialPageRoute(builder: (_) => PageB()));
});
// Heavy process 2: Open dialog after 3 seconds (with lifecycle consideration)
Future.delayed(Duration(seconds: 3), () async {
// Wait until Route becomes active
final lifecycle = await RouteLifecycleDetector.waitResumeOrDestroy(context);
if (lifecycle == RouteLifecycle.active && mounted) {
// Display dialog after screen returns to foreground
showDialog(
context: context,
builder: (_) => AlertDialog(title: Text('Task 2 Complete')),
);
}
});
}
@override
Widget build(BuildContext context) => Scaffold(/*...*/);
}
Getting Current Lifecycle State #
// Check current lifecycle state before displaying dialog
void showDialogSafely(BuildContext context) {
final lifecycle = RouteLifecycle.of(context);
if (lifecycle == RouteLifecycle.active) {
// Display dialog only when Route is at foreground
showDialog(
context: context,
builder: (_) => AlertDialog(title: Text('Safely Displayed')),
);
} else {
// Delay display when Route is inactive
print('Route is inactive, delaying dialog display');
}
}
Stream-based Monitoring #
// Monitor lifecycle changes to control dialog display
class SmartDialogController {
StreamSubscription? _subscription;
final List<VoidCallback> _pendingDialogs = [];
void startListening(BuildContext context) {
_subscription = RouteLifecycleDetector.streamOf(context).listen((lifecycle) {
if (lifecycle == RouteLifecycle.active && _pendingDialogs.isNotEmpty) {
// Display pending dialogs when Route returns to foreground
final dialogs = List<VoidCallback>.from(_pendingDialogs);
_pendingDialogs.clear();
for (final showDialog in dialogs) {
showDialog();
}
}
});
}
void showDialogWhenActive(BuildContext context, WidgetBuilder builder) {
if (RouteLifecycle.of(context) == RouteLifecycle.active) {
// Display immediately
showDialog(context: context, builder: builder);
} else {
// Hold for later display
_pendingDialogs.add(() => showDialog(context: context, builder: builder));
}
}
void dispose() {
_subscription?.cancel();
_pendingDialogs.clear();
}
}
Waiting for Route Resume #
// Wait until Route resumes or is destroyed
final result = await RouteLifecycleDetector.waitResumeOrDestroy(context);
if (result == RouteLifecycle.active) {
// Route has resumed
print('Route has resumed');
} else if (result == RouteLifecycle.destroyed) {
// Route has been destroyed
print('Route has been destroyed');
}
Practical Usage Example #
Example of stopping processing while dialog is open and resuming when dialog closes:
class MyPage extends StatefulWidget {
@override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
StreamSubscription? _subscription;
@override
void initState() {
super.initState();
_startProcessing();
}
void _startProcessing() {
_subscription = RouteLifecycleDetector.streamOf(context).listen((lifecycle) {
if (lifecycle == RouteLifecycle.active) {
// Execute processing only when Route is at foreground
_performBackgroundTask();
}
});
}
void _performBackgroundTask() {
// Execute background task
print('Task is running...');
}
@override
void dispose() {
_subscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('My Page')),
body: Center(
child: ElevatedButton(
onPressed: () async {
// Display dialog
await showDialog(
context: context,
builder: (_) => AlertDialog(
content: Text('Dialog'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Close'),
),
],
),
);
// Processing automatically resumes after dialog is closed
},
child: Text('Open Dialog'),
),
),
);
}
}
Lifecycle States #
| State | Description | Dialog Display |
|---|---|---|
active |
App is in foreground and Route is at top of stack | 🟢 Safe to display |
inactive |
App is in foreground but Route is not at top of stack | 🔴 Should delay display |
hidden |
App is in background state | 🔴 Should delay display |
building |
Widget is being built and Route is not yet created | 🔴 Cannot display |
destroyed |
Route has been destroyed | 🔴 Cannot display |
Additional information #
This library combines Flutter's NavigatorObserver with flutter_fgbg to accurately track Route lifecycles.
Key Benefits:
- Prevent unintended process execution due to dialogs and screen transitions
- Control considering app foreground/background state
- Stream-based reactive programming support
Contributing: Bug reports and feature requests are welcome at GitHub Issues.