route_flutter_bloc 9.2.2
route_flutter_bloc: ^9.2.2 copied to clipboard
Improve Flutter BLoC performance with navigation-aware widgets. Skip rebuilds and side-effects when screens are hidden. React on resume only if state changed.
π Route Flutter Bloc β smart BLoC widgets that understand navigation #
Avoid unnecessary rebuilds and side-effects when pages are hidden in the navigation stack.
React only when needed. Improve performance without changing your appβs architecture. Works with the bloc library β you donβt need to change your development paradigm. You can always add this package without touching your existing code.
Why use it? #
route_flutter_bloc is a collection of enhanced versions of standard flutter_bloc widgets like BlocBuilder, BlocListener, and others.
These widgets are navigation-aware β they know when your page is on screen or hidden inside the Navigator stack, and behave accordingly.
Will this package work with your navigation system? - If RouteAware works for you, then this package will work too.
In a typical Flutter app using flutter_bloc, your BlocBuilder or BlocListener widgets always work, even when the page is not visible.
This can lead to:
β’ π Unnecessary rebuilds that waste performance
β’ β οΈ Side-effects from BlocListener firing at the wrong time
How this package solves it #
This package upgrades the default behavior. All widgets behave the same as in flutter_bloc, except when the screen is hidden.
Hereβs what you get:
β’ β
When the page is visible β everything works as usual.
β’ π€ When the page is in the navigator stack but not on screen β widgets do nothing.
β’ π When the page becomes visible again β widgets can:
β’ Rebuild (RouteBlocBuilder)
β’ React with side-effects (RouteBlocListener)
β’ But only if the state changed while the page was hidden.
BY DEFAULT, THESE WIDGETS STAY QUIET WHEN COMING BACK TO THE SCREEN.
BUT IF YOU WANT THEM TO REACT ON RESUME, JUST ENABLE A FLAG:
β’ rebuildOnResume: true for RouteBlocBuilder, RouteBlocConsumer, RouteBlocSelector
β’ triggerOnResumed: true for RouteBlocListener, RouteBlocConsumer
This ensures the widget reacts once with the latest changed state β only if something really changed.
And yes β even if the final state equals the original, widgets still react if there was a transition.
For example:
β’ loaded β loading β loaded β Triggers β
β’ loaded β loaded β No trigger β
The widgets behave according to the standard BLoC paradigm.
Want the original behavior? #
No problem.
You can make any widget behave like its flutter_bloc counterpart by enabling:
β’ forceClassicBuilder: true
β’ forceClassicListener: true
β’ forceClassicSelector: true
This disables the route-aware logic and makes widgets work always, regardless of screen visibility.
π§ Route Awareness Setup #
This package relies on Flutter's RouteObserver
(via RouteAware
) to detect when a page is on screen or hidden in the navigation stack.
You need to pass the RouteObserver instance using the RouteObserverProvider widget.
final RouteObserver<Route<dynamic>> routeFlutterBlocObserver =
RouteObserver<Route<dynamic>>();
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return RouteObserverProvider(
observer: routeFlutterBlocObserver,
child: RouteBlocProvider(
create: (_) => CounterCubit(),
child: MaterialApp(
navigatorObservers: [routeFlutterBlocObserver],
home: const FirstPage(),
),
),
);
}
}
You can also pass the observer directly to widgets:
RouteBlocBuilder<CounterCubit, int>(
observer: routeFlutterBlocObserver,
rebuildOnResume: true,
builder: (_, state) {
return Text(
'State: $state',
);
},
);
RouteBlocListener behavior #
By default, RouteBlocListener fires only when the screen is visible. If the page is inside the navigator stack β it stays silent.
You can configure its behavior with:
bool triggerOnResumed = false; // Trigger once when returning to screen, only if state changed - false by default
bool forceClassicListener = false; // Always trigger like in flutter_bloc - false by default
Example usage
RouteBlocListener<CounterCubit, int>(
triggerOnResumed: true,
forceClassicListener: false,
listener: (context, state) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Listener: $state')),
);
},
);
RouteBlocListener now supports additional route lifecycle callbacks based on RouteAware:
RouteBlocListener<MyBloc, MyState>(
didPush: () => print('Route was pushed'),
didPushNext: () => print('Another route was pushed on top'),
didPop: () => print('Route was popped'),
didPopNext: () => print('Returned to this route'),
...
)
RouteBlocBuilder behavior #
By default, RouteBlocBuilder
rebuilds only when the screen is visible.
If the page is hidden inside the navigator stack, it does not rebuild.
You can configure its behavior with:
bool rebuildOnResume = true; // Rebuild once when returning to screen, only if state changed - true by default
bool forceClassicBuilder = false; // Always rebuild like in flutter_bloc - false by default
Example usage
RouteBlocBuilder<CounterCubit, int>(
rebuildOnResume: false,
forceClassicBuilder: false,
builder: (context, state) {
return Text('State: $state');
},
);
RouteBlocConsumer behavior #
By default, RouteBlocConsumer
rebuilds and listens only when the screen is visible.
If the page is hidden inside the navigator stack, neither builder nor listener will be called.
You can configure its behavior with:
bool rebuildOnResume = true; // Rebuild once when returning to screen, only if state changed - true by default
bool triggerOnResumed = false; // Trigger listener once when returning, only if state changed - false by default
bool forceClassicBuilder = false; // Always rebuild like in flutter_bloc - false by default
bool forceClassicListener = false; // Always listen like in flutter_bloc - false by default
Example usage
RouteBlocConsumer<CounterCubit, int>(
rebuildOnResume: false,
triggerOnResumed: true,
forceClassicBuilder: false,
forceClassicListener: false,
listener: (context, state) {
print('Listener: $state');
},
builder: (context, state) {
return Text('State: $state');
},
);
RouteBlocSelector behavior #
By default, RouteBlocSelector
rebuilds only when the screen is visible.
If the page is hidden inside the navigator stack, it stays inactive.
You can configure its behavior with:
bool rebuildOnResume = true; // Rebuild once when returning to screen, only if selected value changed - true by default
bool forceClassicSelector = false; // Always rebuild like in flutter_bloc - false by default
Example usage
RouteBlocSelector<CounterCubit, int, String>(
selector: (state) => 'Value: $state',
rebuildOnResume: false,
forceClassicSelector: false,
builder: (context, value) {
return Text(value);
},
);
RouteMultiBlocListener behavior #
By default, RouteMultiBlocListener
triggers its listeners only when the screen is visible.
If the page is hidden inside the navigator stack, the listeners stay silent.
Each listener inside RouteMultiBlocListener
accepts the same flags as RouteBlocListener
, giving you full control per-listener.
You can configure each listener with:
bool triggerOnResumed = false; // Trigger once when returning to screen, only if state changed - false by default
bool forceClassicListener = false; // Always trigger like in flutter_bloc - false by default
Example usage
RouteMultiBlocListener(
listeners: [
RouteBlocListener<CounterCubit, int>(
triggerOnResumed: false,
listener: (context, state) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Counter: $state')),
);
},
),
RouteBlocListener<CounterCubit, int>(
triggerOnResumed: true,
listener: (context, state) {
setState(() => _counter = state);
},
),
],
child: SizedBox(),
);
RouteObserverProvider #
RouteObserverProvider
is a helper widget that provides a RouteObserver
instance down the widget tree.
Itβs used internally by all RouteBloc*
widgets to track route visibility and navigation events.
You only need this if you want to inject your own RouteObserver
or use the system manually.
If you're using route_flutter_bloc
alone, wrap your app once at the top:
Example usage
final RouteObserver<Route<dynamic>> routeObserver = RouteObserver<Route<dynamic>>();
void main() {
runApp(
RouteObserverProvider(
observer: routeObserver,
child: MaterialApp(
navigatorObservers: [routeObserver],
home: MyHomePage(),
),
),
);
}
If you donβt provide a RouteObserver
, the system will try to find one from context or fallback to default.
RouteBlocProvider and MultiRouteBlocProvider #
are copies of the standard flutter_bloc widgets. They are included for convenience in case youβre using this package standalone. If youβre already using flutter_bloc in your project, you can continue using the original BlocProvider and MultiBlocProvider widgets β they will work just fine.
popUntilGuarded #
Use Navigator.popUntilGuarded instead of Navigator.popUntil when navigating back through the route stack.
Navigator.of(context).popUntilGuarded('/home');
This is a safe alternative to:
Navigator.of(context).popUntil((route) => route.settings.name == '/home');
Unlike popUntil, which may skip or break listener logic, popUntilGuarded:
When using Navigator.popUntilGuarded('/routeName'), only one route in the stack (the one matched by the name) will receive didPopNext β and only after the first frame.
All intermediate routes will not receive didPopNext. This is intentional and prevents unexpected re-activation logic.
So, if you use popUntilGuarded β only the final matching route gets didPopNext.
If you use popUntil β all routes below will get didPopNext as usual.
β οΈ The target route must have a name (RouteSettings.name) assigned when pushed.
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const HomePage(),
settings: const RouteSettings(name: '/home'),
),
);
MaterialApp(
initialRoute: '/home',
onGenerateRoute: (settings) {
if (settings.name == '/home') {
return MaterialPageRoute(
builder: (_) => const HomePage(),
settings: RouteSettings(name: '/home'),
);
}
// ...
},
)
When to use
Use Navigator.popUntilGuarded for multi-page back navigation (e.g. from page 3 to page 1) when working with RouteBlocListener and RouteObserverListener.
RouteObserverListener behavior #
By default, RouteObserverListener provides only route lifecycle awareness. It does not depend on Bloc and is designed for navigation-related side effects only.
You can safely use the following RouteAware callbacks:
didPush // called when this route is pushed
didPushNext // called when another route is pushed on top
didPop // called when this route is popped
didPopNext // called when another route above was popped and this route becomes active again
Example usage
Navigator.of(context).popUntilGuarded('/first');
static MaterialPageRoute route() {
return MaterialPageRoute(
builder: (BuildContext context) {
return FirstPage();
},
settings: RouteSettings(name: '/first'),
);
}
RouteObserverListener(
didPopNext: () {
print('β
Called ONLY on "/first" after popUntilGuarded');
},
child: FirstPage(),
);
static MaterialPageRoute route() {
return MaterialPageRoute(
builder: (BuildContext context) {
return FirstPage();
},
settings: RouteSettings(name: '/second'),
);
}
RouteObserverListener(
didPopNext: () {
print('β Will NOT be called on this page when using popUntilGuarded');
},
child: SecondPage(),
);
π¬ Social #
If you like the idea of improving the BLoC pattern and want to connect β
feel free to add me on LinkedIn.
β Support #
If you find this package useful, you can support the development by buying me a coffee!
Your support helps keep this project active and maintained.