flutter
/

Bloc Navigation Events: Event-Driven Routing in Flutter

Last Sync: Today

On this page

10
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

Bloc Navigation Events: Event-Driven Routing in Flutter

In Bloc-based applications, navigation is a side effect that should be triggered by state changes. But what if you want to explicitly tie navigation to a specific event? Using navigation events – events that represent the intention to navigate – can make your navigation flow more explicit and traceable. This guide covers how to implement event-driven navigation with Flutter Bloc.

The Navigation Event Pattern

Instead of having a bloc emit a generic state (like LoggedIn) and then listening for it to navigate, you can define dedicated events that represent navigation intents. The bloc processes these events and may or may not emit additional states. The UI listens for these navigation events (or dedicated navigation states) and performs the actual navigation.

Approach 1: Dedicated Navigation States

Define states that represent navigation actions, and emit them from your bloc. This keeps the navigation intent in the state stream.

DARTRead-only
1
// Navigation states
abstract class AuthState {}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthSuccess extends AuthState {
  final User user;
  AuthSuccess(this.user);
}
class NavigateToHome extends AuthState {}
class NavigateToLogin extends AuthState {}

// In bloc:
on<LogoutRequested>((event, emit) async {
  await repository.logout();
  emit(NavigateToLogin());
});

// In UI:
BlocListener<AuthBloc, AuthState>(
  listener: (context, state) {
    if (state is NavigateToHome) {
      Navigator.pushReplacementNamed(context, '/home');
    } else if (state is NavigateToLogin) {
      Navigator.pushReplacementNamed(context, '/login');
    }
  },
  child: ...
)

Approach 2: Events as Navigation Triggers (without dedicated states)

Sometimes you don't need to change the application state; you just want to navigate. In that case, you can add events that are handled directly in the UI via BlocListener without emitting a state. This keeps the bloc's state focused on business data.

DARTRead-only
1
// Events
abstract class NavigationEvent {}
class NavigateToProfile extends NavigationEvent {}
class NavigateToSettings extends NavigationEvent {}

// Bloc – may not emit any state for navigation events
class AppBloc extends Bloc<AppEvent, AppState> {
  AppBloc() : super(AppInitial()) {
    on<NavigateToProfile>((event, emit) {
      // No state emission, just handle the event
      // Optionally, you could emit a state if needed
    });
  }
}

// In UI: use BlocListener to react to events?
// But BlocListener listens to states, not events. So this approach requires
// either emitting a navigation state or using a custom observer.
// Better to stick with state-based navigation.

Approach 3: Navigation Events as Part of Composite Events

Sometimes a single user action should trigger both business logic and navigation. You can use a composite event that includes navigation intent as part of the state.

DARTRead-only
1
class CheckoutRequested extends CartEvent {
  final bool navigateToSuccess;
  CheckoutRequested({this.navigateToSuccess = true});
}

// In bloc:
on<CheckoutRequested>((event, emit) async {
  emit(CheckoutLoading());
  try {
    await repository.checkout();
    if (event.navigateToSuccess) {
      emit(CheckoutSuccess(navigateTo: '/order-success'));
    } else {
      emit(CheckoutSuccess());
    }
  } catch (e) {
    emit(CheckoutFailure(e.toString()));
  }
});

Handling Multiple Navigation Events

When a bloc can emit different navigation states, you can centralise navigation handling in a single BlocListener or use multiple listeners for clarity.

DARTRead-only
1
BlocListener<AppBloc, AppState>(
  listener: (context, state) {
    switch (state.runtimeType) {
      case NavigateToLogin:
        Navigator.pushReplacementNamed(context, '/login');
        break;
      case NavigateToHome:
        Navigator.pushReplacementNamed(context, '/home');
        break;
      case NavigateToProfile:
        Navigator.pushNamed(context, '/profile', arguments: (state as NavigateToProfile).userId);
        break;
    }
  },
  child: ...
)

Resetting Navigation State After Use

To prevent repeated navigation (e.g., if the widget rebuilds), it's important to reset the navigation state after it has been handled. You can emit a neutral state or clear the navigation flag.

DARTRead-only
1
class AppState extends Equatable {
  final String? navigateTo;
  final dynamic arguments;
  // ... other state fields

  const AppState({this.navigateTo, this.arguments});

  AppState copyWith({String? navigateTo, dynamic arguments}) {
    return AppState(
      navigateTo: navigateTo ?? this.navigateTo,
      arguments: arguments,
    );
  }

  @override
  List<Object?> get props => [navigateTo, arguments];
}

// In bloc:
on<SomeAction>((event, emit) {
  emit(state.copyWith(navigateTo: '/target'));
});

// In UI listener:
BlocListener<AppBloc, AppState>(
  listener: (context, state) {
    if (state.navigateTo != null) {
      Navigator.pushNamed(context, state.navigateTo, arguments: state.arguments);
      // Reset after navigation
      context.read<AppBloc>().add(NavigationHandled());
    }
  },
  child: ...
);

// In bloc:
on<NavigationHandled>((event, emit) {
  emit(state.copyWith(navigateTo: null, arguments: null));
});

Testing Navigation Events

When testing navigation events, you can verify that the correct navigation state is emitted, or use a mock navigator to verify that navigation actually occurs.

DARTRead-only
1
test('logout emits NavigateToLogin', () async {
  final bloc = AuthBloc(mockRepository);
  expectLater(
    bloc.stream,
    emitsInOrder([
      isA<NavigateToLogin>(),
    ]),
  );
  bloc.add(LogoutRequested());
});

Best Practices

  • Use navigation states, not events – Emit a dedicated state to trigger navigation; it's easier to test and trace.
  • Reset navigation flags – Always clear navigation intents after they've been handled to avoid duplicate navigation.
  • Keep navigation states minimal – Include only what's needed to perform the navigation (route name, arguments).
  • Separate business states from navigation states – Don't mix them unless necessary; use composition or dedicated state classes.
  • Use a single BlocListener for navigation – Centralise all navigation logic in one place for a given bloc, or use multiple listeners if you prefer separation.
  • Prefer pushReplacement when appropriate – Avoid piling up the navigation stack unnecessarily.

Common Mistakes

  • ❌ Not resetting navigation state – Causes navigation to occur repeatedly on every rebuild.
  • ❌ Using events directly for navigation – Without emitting a state, you lose traceability and testing capabilities.
  • ❌ Mixing navigation logic with business logic in the bloc – Keep navigation intent separate from core state.
  • ❌ Using Navigator inside the bloc – Makes bloc hard to test and couples it to Flutter.
  • ❌ Forgetting to handle back navigation – When using pushReplacement, consider if the user should be able to go back.

What's Next?

Now that you understand event-driven navigation, explore more advanced patterns like deep linking with Bloc and integrating with GoRouter.

Next, explore Bloc navigation with GoRouter and Bloc testing.

Test Your Knowledge

Q1
of 3

What is the recommended way to trigger navigation from a bloc?

A
Call Navigator directly inside the bloc
B
Emit a dedicated navigation state and listen for it in UI
C
Add a navigation event and use BlocObserver
D
Use a global navigator key and access it from bloc
Q2
of 3

Why should you reset navigation state after handling it?

A
To save memory
B
To prevent repeated navigation on widget rebuilds
C
To improve performance
D
To satisfy lint rules
Q3
of 3

Which widget is best suited for listening to navigation states?

A
BlocBuilder
B
BlocListener
C
BlocProvider
D
MultiBlocProvider

Frequently Asked Questions

Should I create a separate bloc just for navigation?

For simple apps, it's okay to include navigation states in your existing blocs. For larger apps, consider a dedicated navigation bloc that manages the navigation stack and coordinates between features.

How do I handle navigation with arguments?

Include the arguments in your navigation state. For example, class NavigateToProfile extends AppState { final String userId; }. Then in the listener, pass state.userId as arguments to the route.

Can I use BlocListener with multiple blocs for navigation?

Yes, you can nest BlocListener widgets or use a single BlocListener that listens to multiple blocs by calling context.watch inside the listener? Actually, BlocListener only listens to one bloc. To listen to multiple, nest them or use a combination of BlocListener and BlocBuilder.

Is it okay to use context.go from GoRouter inside BlocListener?

Yes, that's the recommended pattern. Use context.go (or push) inside the listener to perform navigation. This keeps navigation logic out of your blocs.

Previous

bloc route management

Next

bloc clean architecture

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.