Skip to main content

centralized_bloc 1.0.1 copy "centralized_bloc: ^1.0.1" to clipboard
centralized_bloc: ^1.0.1 copied to clipboard

A lightweight reusable Bloc architecture with centralized state handling, loading, error handling, and UI building.

centralized_bloc #

A lightweight and reusable Flutter BLoC architecture for centralized state handling, loading, error handling, and UI building.

centralized_bloc provides a simple way to manage common BLoC states without repeatedly writing BlocBuilder, BlocListener, loading widgets, and error widgets throughout your application.

Features #

  • 🚀 Simple reusable BaseBloc
  • 🔄 Centralized loading state handling
  • ✅ Centralized success state handling
  • ❌ Centralized error state handling
  • ðŸŽĻ Global UI configuration
  • ⚙ïļ Configurable initial, loading, and error widgets
  • ðŸŽŊ Application-wide default UI
  • 🎧 Built-in BlocListener support
  • ðŸ§Đ Combines BlocBuilder and BlocListener
  • 🛠ïļ maybeMap() for handling only required states
  • 📝 Customizable error handling
  • ðŸŠķ Lightweight with minimal dependencies
  • 🔌 Works with any repository/API implementation

Installation #

Add centralized_bloc to your pubspec.yaml:

dependencies:
  centralized_bloc: ^1.0.1

Then run:

flutter pub get

Dependencies #

This package intentionally keeps dependencies minimal:

  • flutter_bloc

It does not depend on Dio, Freezed, Retrofit, or any specific networking package.

You can use it with:

  • HTTP
  • Dio
  • Firebase
  • GraphQL
  • Local databases
  • REST APIs
  • Any custom repository

Basic Architecture #

The package provides four main states:

Initial
   ↓
Loading
   ↓
Loaded
   ↓
Error

The main classes are:

BaseState<T>
    ├── Initial<T>
    ├── Loading<T>
    ├── Loaded<T>
    └── Error<T>

BaseBloc<E, M>

CentralizedBloc<B, T>

CentralizedBlocConfig

Where:

  • E = Event type
  • M = Model/data type
  • B = BLoC type
  • T = Loaded data type

1. Create Your Model #

For example:

class User {
  final int id;
  final String name;
  final String email;

  const User({
    required this.id,
    required this.name,
    required this.email,
  });
}

2. Create Your Repository #

The package does not require a specific networking library.

Your repository can contain any data source.

import 'package:example/model/user.dart';

class UserRepository {
  Future<User> getUser() async {
    await Future.delayed(
      const Duration(seconds: 2),
    );

    return const User(
      id: 1,
      name: 'Centralized Bloc',
      email: 'bloc@example.com',
    );
  }
}

For a real application, this could instead call an API, database, Firebase, etc.


3. Create Your Events #

Define your BLoC events normally:

sealed class UserEvent {
  const UserEvent();
}

final class LoadUser extends UserEvent {
  const LoadUser();
}

4. Create Your BLoC #

Extend BaseBloc:

import 'package:centralized_bloc/centralized_bloc.dart';
import 'package:example/model/user.dart';
import 'package:example/repository/user_repository.dart';
import 'user_event.dart';

class UserBloc extends BaseBloc<UserEvent, User> {
  final UserRepository repository;

  UserBloc({
    required this.repository,
  }) {
    on<LoadUser>(
      defaultHandler(
        (event, emit) {
          return repository.getUser();
        },
      ),
    );
  }
}

The defaultHandler() automatically handles:

emit Loading
      ↓
execute repository operation
      ↓
emit Loaded(data)

If an exception occurs:

Exception
   ↓
Error handler
   ↓
emit Error(message)

5. Provide the BLoC #

Use the normal BlocProvider from flutter_bloc:

import 'package:centralized_bloc/centralized_bloc.dart';
import 'package:example/bloc/user_bloc.dart';
import 'package:example/bloc/user_event.dart';
import 'package:example/repository/user_repository.dart';
import 'package:example/user_screen.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: BlocProvider(
        create: (_) => UserBloc(
          repository: UserRepository(),
        )..add(const LoadUser()),
        child: const UserScreen(),
      ),
    );
  }
}

6. Build the UI with CentralizedBloc #

Instead of writing:

BlocBuilder

and

BlocListener

separately, use CentralizedBloc. CentralizedBloc automatically displays:

  • SizedBox.shrink() for Initial
  • CircularProgressIndicator for Loading
  • Your widget for Loaded
  • Error message for Error

7. Add a listener and onDataLoaded #

You can also listen to state changes using the optional listener parameter.

CentralizedBloc<UserBloc, User>(
  listener: (context, state) {
    state.maybeMap(
      loaded: (user) {
        // Handle successful loading.
      },
      error: (message) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text(message),
          ),
        );
      },
    );
  },
  onDataLoaded: (context, user) {
    return _UserView(user: user);
  },
)

This allows you to keep UI rendering and side effects in one place.

For example:

CentralizedBloc
       │
       ├── UI building
       │     └── onDataLoaded
       │
       └── State listening
             └── listener

8. Complete User Screen #

import 'package:centralized_bloc/centralized_bloc.dart';
import 'package:example/bloc/user_bloc.dart';
import 'package:example/model/user.dart';
import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Centralized Bloc'),
      ),
      body: CentralizedBloc<UserBloc, User>(
        create: (_) => UserBloc(
          repository: UserRepository(),
        )..add(LoadUser()),
        listener: (context, state) {
          state.maybeMap(
            loaded: (user) {
              // User loaded successfully.
            },
            error: (message) {
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(
                  content: Text(message),
                ),
              );
            },
          );
        },
        onDataLoaded: (context, user) {
          return _UserView(user: user);
        },
      ),
    );
  }
}

class _UserView extends StatelessWidget {
  final User user;

  const _UserView({
    required this.user,
  });

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Text(
            user.name,
            style: const TextStyle(
              fontSize: 24,
              fontWeight: FontWeight.bold,
            ),
          ),
          const SizedBox(height: 8),
          Text(user.email),
        ],
      ),
    );
  }
}

9. Global UI Configuration #

Configure the default UI once for the entire application.


void main() {
  CentralizedBlocConfig.configure(
    loadingBuilder: (context) {
      return const Center(
        child: CircularProgressIndicator.adaptive(),
      );
    },
    errorBuilder: (context, message) {
      return Center(
        child: Text(message),
      );
    },
  );

  runApp(const MyApp());
}

You can configure:

initialBuilder loadingBuilder errorBuilder

If you do not configure them, the package uses its default widgets.

To restore the defaults:

CentralizedBlocConfig.reset();

Error Handling #

BaseBloc automatically catches exceptions thrown by your operation.

For example:

defaultHandler(
  (event, emit) async {
    return repository.getUser();
  },
);

If repository.getUser() throws an exception, the BLoC automatically changes to:

BaseState.error(message)

The default error message is generated from:

error.toString()

Custom Error Handling #

You can provide your own error handler:

class UserBloc extends BaseBloc<UserEvent, User> {
  final UserRepository repository;

  UserBloc({
    required this.repository,
  }) : super(
          errorHandler: (error) {
            return 'Unable to load user';
          },
        ) {
    on<LoadUser>(
      defaultHandler(
        (event, emit) {
          return repository.getUser();
        },
      ),
    );
  }
}

This is useful when your application uses Dio, HTTP, Firebase, or another API layer.

For example, your application can convert different exceptions into user-friendly messages:

errorHandler: (error) {
  if (error is SomeApiException) {
    return 'Something went wrong while contacting the server.';
  }

  return 'Something went wrong.';
},

The package itself does not need to know which networking library your application uses.


Why Use defaultHandler()? #

Without BaseBloc, you may repeatedly write:

on<LoadUser>((event, emit) async {
  try {
    emit(const BaseState.loading());

    final user = await repository.getUser();

    emit(BaseState.loaded(user));
  } catch (error) {
    emit(
      BaseState.error(
        error.toString(),
      ),
    );
  }
});

With BaseBloc:

on<LoadUser>(
  defaultHandler(
    (event, emit) {
      return repository.getUser();
    },
  ),
);

The common state-management logic is centralized.


Complete Flow #

A typical request follows this flow:

User opens screen
       │
       ▾
LoadUser event
       │
       ▾
UserBloc
       │
       ▾
defaultHandler()
       │
       ├───────────────┐
       ▾               │
   Loading             │
       │               │
       ▾               │
Repository             │
       │               │
       ├──── Success ──â”Ī
       │               │
       ▾               ▾
    Loaded           Error
       │               │
       ▾               ▾
onDataLoaded        Error UI
       │
       ▾
   User View

Philosophy #

centralized_bloc is designed to keep repetitive BLoC state-management code in one place while allowing application developers to keep their repositories and models completely independent.

The package focuses on:

  • Simple APIs
  • Minimal dependencies
  • Reusable architecture
  • Generic error handling
  • Less UI boilerplate
  • Compatibility with different data sources

Contact #

For questions, suggestions, or support:

Email: itsmaddeveloper@gmail.com

1
likes
150
points
342
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A lightweight reusable Bloc architecture with centralized state handling, loading, error handling, and UI building.

License

MIT (license)

Dependencies

flutter, flutter_bloc

More

Packages that depend on centralized_bloc