route_observer_mixin Screen Tracking Guide
route_observer_mixin eliminates manual registration and disposal of Flutter RouteObserver and RouteAware subscriptions. It provides clean, declarative hooks for page lifecycle transitions (didPush, didPop, didPushNext, didPopNext).
Guidelines
- Setup in Navigator:
- Wrap your app with
RouteObserverProvideraboveMaterialApp(or provide it viaprovider/riverpod). - Pass
RouteObserverProvider.of(context)(or the provider's instance) toMaterialApp.navigatorObservers.
- Wrap your app with
- Implementing in State:
- Add
with RouteAware, RouteObserverMixinto the targetState<MyWidget>class. - Implement lifecycle methods:
didPush(): Called when the screen has been pushed and is now visible.didPopNext(): Called when the top route has been popped off, and this screen becomes visible again.didPushNext(): Called when a new route is pushed on top of this screen (this screen becomes obscured).didPop(): Called when this screen has been popped off the navigator.
- Add
- Analytics & Tracking:
- Track screen views inside
didPush()anddidPopNext()to accurately capture when the user returns to a previously viewed page.
- Track screen views inside
- Lifecycle Cleanup:
- Do not manually call
routeObserver.unsubscribe(this).RouteObserverMixinautomatically manages subscription registration indidChangeDependenciesand unsubscription indispose.
- Do not manually call
Examples
1. App Setup
import 'package:flutter/material.dart';
import 'package:route_observer_mixin/route_observer_mixin.dart';
void main() {
runApp(
RouteObserverProvider(
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorObservers: [
RouteObserverProvider.of(context),
],
home: const HomeScreen(),
);
}
}
2. Page Implementation with Analytics Tracking
import 'package:flutter/material.dart';
import 'package:route_observer_mixin/route_observer_mixin.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen>
with RouteAware, RouteObserverMixin {
@override
void didPush() {
super.didPush();
// Screen is pushed for the first time
_trackScreenView();
}
@override
void didPopNext() {
super.didPopNext();
// User returned back to this screen from a pushed page
_trackScreenView();
_refreshContent();
}
@override
void didPushNext() {
super.didPushNext();
// Another screen was pushed on top of this screen
_pauseMediaPlayback();
}
void _trackScreenView() {
debugPrint('Analytics: Screen viewed -> HomeScreen');
}
void _refreshContent() {
debugPrint('Refreshing active screen data');
}
void _pauseMediaPlayback() {
debugPrint('Pausing background video/audio');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Center(
child: ElevatedButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const DetailScreen()),
),
child: const Text('Go to Details'),
),
),
);
}
}
class DetailScreen extends StatelessWidget {
const DetailScreen({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Details')),
);
}
Common Pitfalls & Anti-Patterns
- ❌ Anti-pattern: Manually managing
RouteObserver().subscribe(this, ModalRoute.of(context)!)insidedidChangeDependencies, which frequently leaks subscriptions if unsubscription is missed indispose.- ✔️ Correct: Use
RouteObserverMixin, which handles registration and unsubscription safely and deterministically.
- ✔️ Correct: Use
- ❌ Anti-pattern: Only tracking screen view analytics in
initState, which fails to capture when users navigate back to the page via the back button.- ✔️ Correct: Track screen views in both
didPush()anddidPopNext().
- ✔️ Correct: Track screen views in both