route_lifecycle_detector 1.1.0+1
route_lifecycle_detector: ^1.1.0+1 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
- Detailed Lifecycle State Detection: Real-time detection of Route's current state (active/inactive/building/destroyed) and app foreground/background state
- Stream-based Monitoring: Monitor Route lifecycle changes via Stream
- Type-safe State Management: Type-safe pattern matching with sealed class using Freezed
- Flexible Waiting Feature: Wait for lifecycle state changes with custom conditions
- Lightweight Design: Minimal dependencies and performance overhead
Getting started #
Add the library to your pubspec.yaml
:
dependencies:
route_lifecycle_detector: ^1.1.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 context.waitLifecycleWith(
(lifecycle) => lifecycle is RouteLifecycleActive,
);
if (lifecycle is RouteLifecycleActive && 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 is RouteLifecycleActive && lifecycle.isForeground) {
// Display dialog only when Route is at foreground and app is in foreground
showDialog(
context: context,
builder: (_) => AlertDialog(title: Text('Safely Displayed')),
);
} else {
// Delay display when Route is inactive or app is in background
print('Route is inactive or app is in background, 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 = RouteLifecycle.streamOf(context).listen((lifecycle) {
if (lifecycle is RouteLifecycleActive &&
lifecycle.isForeground &&
_pendingDialogs.isNotEmpty) {
// Display pending dialogs when Route returns to foreground and app is in foreground
final dialogs = List<VoidCallback>.from(_pendingDialogs);
_pendingDialogs.clear();
for (final showDialog in dialogs) {
showDialog();
}
}
});
}
void showDialogWhenActive(BuildContext context, WidgetBuilder builder) {
final lifecycle = RouteLifecycle.of(context);
if (lifecycle is RouteLifecycleActive && lifecycle.isForeground) {
// 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 Lifecycle Changes #
// Wait until Route becomes active and app is in foreground
try {
final result = await context.waitLifecycleWith(
(lifecycle) => lifecycle is RouteLifecycleActive && lifecycle.isForeground,
);
// Route has resumed
print('Route has resumed: $result');
} on BadLifecycleException catch (e) {
// Route was destroyed or could not reach the expected state
print('Could not reach expected state: ${e.latestLifecycle}');
}
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 = RouteLifecycle.streamOf(context).listen((lifecycle) {
if (lifecycle is RouteLifecycleActive && lifecycle.isForeground) {
// Execute processing only when Route is at foreground and app is in 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 Recommendation |
---|---|---|
RouteLifecycleActive(isForeground: true) |
App is in foreground and Route is at top of stack | π’ Safe to display |
RouteLifecycleActive(isForeground: false) |
Route is at top of stack but app is in background | π΄ Should delay display |
RouteLifecycleInactive |
Route is not at top of stack | π΄ Should delay display |
RouteLifecycleBuilding |
Widget is being built and Route is not yet created | π΄ Cannot display |
RouteLifecycleDestroyed |
Route has been destroyed | π΄ Cannot display |
Note: The isForeground
property allows fine-grained control over app foreground/background state.
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.