Introduction
In large Flutter applications using BLoC, it's common to have complex screens with multiple parts reacting to state changes. If not structured properly, a single BlocBuilder at the top level can cause entire screens to rebuild unnecessarily, hurting performance and making code harder to maintain. Splitting widgets—breaking down UI into smaller, focused components—is a key practice to leverage BLoC's reactivity efficiently. This guide covers strategies for splitting widgets, using BlocBuilder at the right granularity, context.select, and organizing your UI for optimal rebuilds and maintainability.
Why Split Widgets?
- Performance – Reduce rebuilds by limiting
BlocBuilderto only the widgets that actually depend on state changes. - Separation of Concerns – Keep business logic in BLoC, UI rendering in widgets, and split widgets by responsibility.
- Testability – Smaller widgets are easier to test in isolation.
- Reusability – Granular components can be reused across different screens.
- Readability – Avoid monolithic
buildmethods; each widget has a clear purpose.
Strategy 1: Granular BlocBuilder Placement
Instead of placing a single BlocBuilder at the root of a screen, place it as close as possible to the UI that depends on the state. This isolates rebuilds to the smallest necessary subtree.
Strategy 2: Extract Widgets with Builders
For complex UI parts, extract them into separate widgets that internally use BlocBuilder or context.select. This keeps the main screen clean and allows independent testing.
Strategy 3: Using context.select
context.select allows you to listen to a specific part of the state without a BlocBuilder. The widget rebuilds only when the selected value changes. This is cleaner for simple selections.
Strategy 4: Decompose by Responsibility
Split widgets not only by state dependencies but also by functional responsibility: header, list, form, buttons, etc. Each can have its own BlocBuilder and possibly its own child BLoC if needed.
Strategy 5: Passing Bloc to Children
When splitting widgets, avoid passing the BLoC instance manually unless necessary. Use context.read or context.watch inside the child widget to access the BLoC. This keeps the widget tree clean and ensures the BLoC is properly scoped.
Strategy 6: Using BlocProvider.value for Deep Trees
If you have a deeply nested widget that needs access to the same BLoC, you can use BlocProvider.value to provide the existing instance to a subtree. This avoids passing the BLoC through constructors.
Best Practices
- Place
BlocBuilderas deep as possible – Rebuild only what needs to change. - Use
buildWhenorcontext.select– Fine‑tune rebuilds even further. - Extract reusable widgets – Each widget should have a single responsibility.
- Prefer
context.readfor events,context.selectfor state – AvoidBlocBuilderwhen a simple selector suffices. - Name widgets descriptively –
TodoList,TodoItem,AddTodoButtonmake the structure clear. - Avoid passing BLoC instances down – Use
BlocProviderandcontext.readto access them. - Test widgets in isolation – Mock the BLoC or use
BlocProvider.valuewith a mock.
Common Mistakes
- ❌ One giant
BlocBuilderat the top – Rebuilds the whole screen on any change. ✅ Split into smaller builders. - ❌ Passing BLoC instances through constructors – Makes testing harder and clutters code.
✅ Use
context.readorBlocProvider.of. - ❌ Creating widgets inside
buildwithout extracting – Can lead to unnecessary rebuilds and less readability. ✅ Extract into separate widgets. - ❌ Over‑splitting – Too many widgets can make navigation complex. ✅ Balance granularity with simplicity.
- ❌ Forgetting to use
constconstructors – Missing out on performance optimization.
Conclusion
Splitting widgets in BLoC applications is essential for achieving high performance and maintainable code. By placing BlocBuilder at the right granularity, using context.select, and extracting focused widgets, you ensure that your UI reacts efficiently to state changes. These practices also improve code organization, making it easier to test and extend your app over time.