flutter
/

BLoC Rebuild Optimization: Efficient State Management with Minimal Rebuilds

Last Sync: Today

On this page

12
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

BLoC Rebuild Optimization: Efficient State Management with Minimal Rebuilds

Introduction

One of the key advantages of BLoC is its predictable state management. However, if not used carefully, it can lead to excessive widget rebuilds, hurting performance. This guide explores how to optimize rebuilds in BLoC by using Equatable for state comparison, context.select to listen only to specific parts of the state, controlling BlocBuilder granularity, and other advanced techniques to ensure your app stays fast and responsive.

Why Rebuild Optimization Matters

  • Better Performance – Avoid unnecessary widget rebuilds, especially in complex UIs.
  • Smoother Animations – Reduce jank by rebuilding only what’s needed.
  • Efficient Resource Usage – Less CPU/GPU work, longer battery life.
  • Scalability – Large apps with many BLoCs stay responsive.

Using Equatable for State Comparison

By default, BlocBuilder and BlocListener compare states using ==. If you don't override == and hashCode, every new state is considered different, causing unnecessary rebuilds. Equatable simplifies this by automatically generating equality based on the properties you define.

DARTRead-only
1
@immutable
class CounterState extends Equatable {
  final int count;
  final bool isLoading;

  const CounterState({required this.count, this.isLoading = false});

  @override
  List<Object?> get props => [count, isLoading];
}

// Now two states with the same count and isLoading are considered equal.
// BlocBuilder will rebuild only when these properties change.

Granular BlocBuilder with context.select

context.select (from flutter_bloc) allows you to listen to only a specific part of the state. The widget will rebuild only when the selected value changes.

DARTRead-only
1
class ProfilePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Rebuild only when the user's name changes
    final name = context.select((ProfileBloc bloc) => bloc.state.user.name);
    
    return Text('Hello, $name');
  }
}

// Instead of:
// BlocBuilder<ProfileBloc, ProfileState>(
//   builder: (context, state) => Text('Hello, ${state.user.name}'),
// );
// Which rebuilds on any state change.

BlocBuilder Granularity

Place BlocBuilder widgets as deep in the widget tree as possible. Instead of rebuilding the entire screen, rebuild only the part that depends on the state.

DARTRead-only
1
// ❌ Bad: Rebuilds entire screen on any state change
BlocBuilder<CartBloc, CartState>(
  builder: (context, state) => Scaffold(
    appBar: AppBar(title: Text('Cart')),
    body: Column(
      children: [
        Text('Total: \${state.total}'),
        ListView.builder(...),
      ],
    ),
  ),
);

// ✅ Good: Isolate rebuild to only the price
Scaffold(
  appBar: AppBar(title: Text('Cart')),
  body: Column(
    children: [
      BlocBuilder<CartBloc, CartState>(
        buildWhen: (previous, current) => previous.total != current.total,
        builder: (context, state) => Text('Total: \${state.total}'),
      ),
      ListView.builder(...),
    ],
  ),
);

Using buildWhen to Control Rebuilds

BlocBuilder and BlocListener accept a buildWhen or listenWhen predicate that lets you decide whether to rebuild based on the old and new states.

DARTRead-only
1
BlocBuilder<AuthBloc, AuthState>(
  buildWhen: (previous, current) {
    // Rebuild only when authentication status changes, not on user profile updates
    return previous.isAuthenticated != current.isAuthenticated;
  },
  builder: (context, state) {
    return state.isAuthenticated ? HomePage() : LoginPage();
  },
);

Memoizing Expensive Computations

If your UI derives data from the state (e.g., filtering a list), recompute it only when relevant parts of the state change. You can use select or memoization libraries like flutter_bloc's context.select combined with cached values.

DARTRead-only
1
class TodoList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Only rebuild when the todos list changes
    final todos = context.select((TodoBloc bloc) => bloc.state.todos);
    
    // Expensive filtering only happens when todos changes
    final completedTodos = todos.where((t) => t.completed).toList();
    
    return ListView.builder(
      itemCount: completedTodos.length,
      itemBuilder: (_, i) => TodoTile(completedTodos[i]),
    );
  }
}

Using BlocConsumer for Separated Build and Side Effects

BlocConsumer combines BlocBuilder and BlocListener but allows you to control rebuilds separately. You can set buildWhen for UI updates and listenWhen for side effects, preventing unnecessary rebuilds caused by side effect triggers.

DARTRead-only
1
BlocConsumer<FormBloc, FormState>(
  listenWhen: (previous, current) => previous.status != current.status,
  listener: (context, state) {
    if (state.status == FormStatus.success) {
      ScaffoldMessenger.showSnackBar(...);
    }
  },
  buildWhen: (previous, current) => previous.fields != current.fields,
  builder: (context, state) {
    return Form(...);
  },
);

Avoiding Unnecessary State Copies

Creating new state objects even when nothing changed can cause rebuilds. Ensure your state classes implement proper equality (Equatable) and only create new instances when data actually changes.

DARTRead-only
1
// ❌ Bad: Creates a new state even if count didn't change
void incrementIfNeeded() {
  if (someCondition) {
    emit(CounterState(count: state.count + 1));
  } else {
    emit(CounterState(count: state.count)); // Unnecessary new state
  }
}

// ✅ Good: Only emit when something actually changes
void incrementIfNeeded() {
  if (someCondition) {
    emit(CounterState(count: state.count + 1));
  }
  // No else, no state emission
}

Best Practices

  • Use Equatable for all states – Reduces unnecessary rebuilds and simplifies equality.
  • Use context.select for fine-grained listening – Rebuild only on relevant state changes.
  • Keep BlocBuilder as deep as possible – Rebuild only the parts that depend on state.
  • Use buildWhen and listenWhen – Avoid side effects and rebuilds for irrelevant changes.
  • Avoid emitting identical states – Check before emitting to prevent no‑op rebuilds.
  • Memoize expensive UI computations – Use context.select or caching to avoid recomputation.
  • Profile your app – Use Flutter DevTools to identify excessive rebuilds.

Common Mistakes

  • ❌ Not using Equatable – Causes rebuilds on every state change even if data is the same. ✅ Always extend Equatable or override == and hashCode.
  • ❌ Placing BlocBuilder at the root of a screen – Rebuilds the whole screen for minor changes. ✅ Place it around the widgets that actually depend on state.
  • ❌ Creating new state objects unnecessarily – Triggers rebuilds when nothing changed. ✅ Emit only when state differs.
  • ❌ Ignoring buildWhen – Leads to side effects running on every state change. ✅ Use buildWhen to filter irrelevant updates.
  • ❌ Using BlocConsumer when not needed – Adds unnecessary complexity. ✅ Use BlocBuilder for simple UI, BlocListener for side effects, and BlocConsumer when you need both with separate filters.

Conclusion

Optimizing rebuilds is crucial for building high‑performance Flutter apps with BLoC. By leveraging Equatable, context.select, granular BlocBuilder placement, and selective rebuild controls, you can ensure your app remains fast and responsive. Always profile your app to identify bottlenecks and apply these techniques where they matter most.

Try it yourself

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';

void main() => runApp(MyApp());

// State
class CounterState extends Equatable {
  final int count;
  final bool isLoading;

  const CounterState({required this.count, this.isLoading = false});

  CounterState copyWith({int? count, bool? isLoading}) {
    return CounterState(
      count: count ?? this.count,
      isLoading: isLoading ?? this.isLoading,
    );
  }

  @override
  List<Object?> get props => [count, isLoading];
}

// Events
abstract class CounterEvent {}
class Increment extends CounterEvent {}
class Decrement extends CounterEvent {}
class StartLoading extends CounterEvent {}
class StopLoading extends CounterEvent {}

// BLoC
class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(const CounterState(count: 0)) {
    on<Increment>((event, emit) {
      emit(state.copyWith(count: state.count + 1));
    });
    on<Decrement>((event, emit) {
      emit(state.copyWith(count: state.count - 1));
    });
    on<StartLoading>((event, emit) {
      emit(state.copyWith(isLoading: true));
    });
    on<StopLoading>((event, emit) {
      emit(state.copyWith(isLoading: false));
    });
  }
}

// UI
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BlocProvider(
        create: (_) => CounterBloc(),
        child: CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // This will rebuild only when count changes
    final count = context.select((CounterBloc bloc) => bloc.state.count);
    // This will rebuild only when isLoading changes
    final isLoading = context.select((CounterBloc bloc) => bloc.state.isLoading);

    return Scaffold(
      appBar: AppBar(title: Text('Rebuild Optimization')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Count:', style: TextStyle(fontSize: 20)),
            Text('$count', style: TextStyle(fontSize: 40, fontWeight: FontWeight.bold)),
            SizedBox(height: 20),
            if (isLoading) CircularProgressIndicator(),
            SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: () => context.read<CounterBloc>().add(Increment()),
                  child: Text('+'),
                ),
                SizedBox(width: 20),
                ElevatedButton(
                  onPressed: () => context.read<CounterBloc>().add(Decrement()),
                  child: Text('-'),
                ),
              ],
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                context.read<CounterBloc>().add(StartLoading());
                Future.delayed(Duration(seconds: 2), () {
                  context.read<CounterBloc>().add(StopLoading());
                });
              },
              child: Text('Simulate Loading'),
            ),
          ],
        ),
      ),
    );
  }
}

Test Your Knowledge

Q1
of 3

Which package is commonly used to simplify state equality in BLoC?

A
bloc_concurrency
B
equatable
C
flutter_bloc
D
rxdart
Q2
of 3

What method allows you to listen to a specific part of the BLoC state without rebuilding on unrelated changes?

A
BlocBuilder
B
context.select
C
buildWhen
D
BlocListener
Q3
of 3

What is the main benefit of placing BlocBuilder as deep as possible in the widget tree?

A
It improves code readability
B
It reduces the number of rebuilds
C
It simplifies state management
D
It makes testing easier

Frequently Asked Questions

What is the difference between `BlocBuilder` and `context.select`?

BlocBuilder listens to the entire state and rebuilds when any property changes (unless you provide buildWhen). context.select allows you to listen to a specific part of the state and rebuilds only when that part changes, without any additional boilerplate.

Do I always need Equatable for BLoC states?

It’s highly recommended. Without Equatable, each new state instance is considered different, causing unnecessary rebuilds even if the data is identical.

Can I use `context.select` with Cubit?

Yes, context.select works with any StateStreamableSource, including Bloc and Cubit.

How do I debug unnecessary rebuilds?

Use Flutter DevTools’ “Rebuild” tab to see which widgets are rebuilding. You can also use debugPrint inside buildWhen or builder to track rebuilds.

Is it okay to have many small `BlocBuilder`s?

Yes, it’s a best practice. Having many small, granular BlocBuilders is more efficient than one large BlocBuilder that rebuilds a large subtree.

Previous

bloc performance

Next

bloc lazy loading

Related Content

Need help?

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