sectional_bloc_builder

Section-aware BlocBuilder for Flutter that rebuilds only the UI parts you tag with section enums. Keeps widget trees fast and predictable on complex screens.

Why

  • Avoid whole-screen rebuilds when only a small part should update.
  • Emit from your Bloc/Cubit who should rebuild via UiSectionStatus.
  • Consistent status model: RequestStatus + UiSectionStatus.
  • Optional targeting for list items via index or objectId.

Compared to other approaches

  • Smaller cubits per subtree: clear ownership and isolation; fewer unintended rebuilds. However, it increases the number of blocs to wire, can duplicate state, and makes cross-subtree coordination harder. Prefer this when feature boundaries are sharp and subtrees are truly independent.
  • BlocSelector / buildWhen (bloc built-ins, sometimes used as a “section builder”): no extra enums or library. You select the slice of state each widget needs and avoid rebuilds. Drawback: selection logic spreads across many widgets, coordinating multiple widgets for a single transition is repetitive, and item-scoped targeting (like objectId) is ad hoc. Prefer this on simple screens with a handful of consumers.
  • This library (section enums): the emitter says who should rebuild via UiSectionStatus(sections: [...]). It centralizes rebuild intent, keeps consumers simple, and adds optional objectId targeting. It does require maintaining a small enum per screen and being consistent about setting sections in emits.

Example with BlocSelector vs this package:

// BlocSelector
BlocSelector<ProfileCubit, ProfileState, String>(
  selector: (s) => s.email,
  builder: (_, email) => Text(email),
);

// SectionalBlocBuilder (emitter-driven targeting)
SectionalBlocBuilder<ProfileCubit, ProfileState, ProfileSection>(
  sections: const [ProfileSection.details],
  builder: (_, state) => Text(state.email),
);

Installation

Add to your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^9.0.0
  sectional_bloc_builder: ^0.0.1

Then import:

import 'package:sectional_bloc_builder/sectional_bloc_builder.dart';

Core Concepts

  • RequestStatus: initial | loading | success | failure.
  • UiSectionStatus<T extends Enum>: carries sections: List<T>, status, plus optional message, index, and objectId.
  • SectionalState<T extends Enum>: mix into your state to expose a uiStatus.
  • SectionalBlocBuilder<B, S extends SectionalState<T>, T extends Enum>: rebuilds only when emitted sections match the builder's sections (and optional objectId).

Quick Start

Define your sections, state, and cubit:

// 1) Sections
enum ProfileSection { header, details, submitButton }

// 2) State
class ProfileState extends SectionalState<ProfileSection> {
  final String name;
  final String email;

  ProfileState({
    required UiSectionStatus<ProfileSection> uiStatus,
    required this.name,
    required this.email,
  }) : super(uiStatus: uiStatus);

  ProfileState copyWith({
    UiSectionStatus<ProfileSection>? uiStatus,
    String? name,
    String? email,
  }) {
    return ProfileState(
      uiStatus: uiStatus ?? this.uiStatus,
      name: name ?? this.name,
      email: email ?? this.email,
    );
  }
}

// 3) Cubit
class ProfileCubit extends Cubit<ProfileState> {
  ProfileCubit()
      : super(ProfileState(
          uiStatus: UiSectionStatus(
            sections: const [], // empty = global
            status: RequestStatus.initial,
          ),
          name: 'Jane',
          email: 'jane@example.com',
        ));

  Future<void> updateEmail(String newEmail) async {
    // Tell only the details section to show loading
    emit(state.copyWith(
      uiStatus: UiSectionStatus(
        sections: const [ProfileSection.details],
        status: RequestStatus.loading,
      ),
    ));

    await Future<void>.delayed(const Duration(milliseconds: 600));

    emit(state.copyWith(
      email: newEmail,
      uiStatus: UiSectionStatus(
        sections: const [ProfileSection.details],
        status: RequestStatus.success,
      ),
    ));
  }
}

Build only what you need:

class ProfileView extends StatelessWidget {
  const ProfileView({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // Rebuilds when header is targeted
        SectionalBlocBuilder<ProfileCubit, ProfileState, ProfileSection>(
          sections: const [ProfileSection.header],
          builder: (context, state) {
            return Text('Hello, ${state.name}');
          },
        ),

        // Rebuilds when details is targeted
        SectionalBlocBuilder<ProfileCubit, ProfileState, ProfileSection>(
          sections: const [ProfileSection.details],
          builder: (context, state) {
            final loading = state.uiStatus.isLoading;
            return Row(
              children: [
                Text(state.email),
                if (loading) const Padding(
                  padding: EdgeInsets.only(left: 8),
                  child: SizedBox(width: 12, height: 12, child: CircularProgressIndicator(strokeWidth: 2)),
                ),
              ],
            );
          },
        ),

        // Rebuilds when submitButton is targeted
        SectionalBlocBuilder<ProfileCubit, ProfileState, ProfileSection>(
          sections: const [ProfileSection.submitButton],
          builder: (context, state) {
            final busy = state.uiStatus.isLoading;
            return ElevatedButton(
              onPressed: busy
                  ? null
                  : () => context.read<ProfileCubit>().updateEmail('new@example.com'),
              child: Text(busy ? 'Saving...' : 'Save'),
            );
          },
        ),
      ],
    );
  }
}

Wrap with a provider as usual:

MaterialApp(
  home: BlocProvider(
    create: (_) => ProfileCubit(),
    child: const Scaffold(body: Padding(
      padding: EdgeInsets.all(16),
      child: ProfileView(),
    )),
  ),
);

Targeting Specific Items

Two optional fields help scope updates within lists:

  • index: good for stable, in-memory lists.
  • objectId: safer when the visual position can shift; match by id.
// Emitting from cubit for a specific item
emit(state.copyWith(
  uiStatus: UiSectionStatus(
    sections: const [ProfileSection.details],
    status: RequestStatus.loading,
    index: 3,
    objectId: 'user:42',
  ),
));

// Filtering in the builder
SectionalBlocBuilder<MyCubit, MyState, MySection>(
  sections: const [MySection.details],
  objectId: 'user:42',
  builder: (context, state) => ..., 
);

If sections is empty in an emission, it's treated as global (affects all builders). When objectId is provided to a builder, it only reacts to emissions that match the same objectId.

Tips

  • Use sections: [] for global state transitions like initial loading.
  • Keep section enums small and meaningful (align to UI blocks).
  • Prefer objectId over index when list order can change.
  • You can pass listener: to SectionalBlocBuilder to get a section-aware BlocConsumer.

License

See LICENSE.

Libraries

sectional_bloc_builder
Public exports for sectional BLoC building.