Skip to main content

Managing Multiple Cubits Without MultiBlocProvider in Flutter

Updated 6 min readMarkdown

Managing Multiple Cubits Without MultiBlocProvider in Flutter

Managing Multiple Cubits Without MultiBlocProvider in Flutter

In Flutter development, using multiple state management solutions, such as Bloc or Cubit, can become quite common as the complexity of an application grows. The typical approach is to use MultiBlocProvider for providing multiple Cubits or Blocs throughout the widget tree. However, MultiBlocProvider can become cumbersome and harder to maintain, particularly for large projects. In this article, I'll discuss techniques to manage multiple Cubits effectively without relying on MultiBlocProvider and illustrate how to do this with the injectable package and freezed for state management.

Why Avoid MultiBlocProvider?

The main disadvantages of providing all blocs globally are:

  • Resource Consumption: The blocs are never closed, so they consume resources even if they aren’t being used by the current widget tree.
  • Access Control: The blocs can be accessed from anywhere, even if the state of the bloc is scoped to just a particular feature.
  • State Management Complexity: The blocs typically end up needing some sort of “reset” event to revert back to the initial state, which is not necessary if they are properly scoped and automatically disposed of by BlocProvider.

My recommendation is to create a bloc per feature and provide that bloc only to the specific subtree that needs it.

Source

MultiBlocProvider is a convenient tool, but it has a few drawbacks, especially when working on large-scale projects:

  • Tight Coupling: Using MultiBlocProvider can tightly couple the UI layer with the state management, making it less modular and more challenging to test or reuse components.
  • Scalability Issues: As the number of Cubits grows, the complexity of the MultiBlocProvider grows too, making the widget tree harder to maintain.
  • Reduced Flexibility: It limits where you provide specific Cubits, which can lead to performance issues and bloated widget trees.

Instead of using MultiBlocProvider, a dependency injection approach can be more effective. This approach makes the state management system more modular, easier to test, and more maintainable.

Managing Cubits Using Injectable and Freezed

The combination of injectable for dependency injection and freezed for immutable state management provides a powerful way to manage Cubits effectively without relying on MultiBlocProvider. Below, I'll guide you through an example of setting up login and signup Cubits with injectable and freezed.

Step 1: Setting Up Injectable

First, add the necessary dependencies to your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^8.0.0
  freezed_annotation: ^2.0.0
  injectable: ^2.0.0

dev_dependencies:
  build_runner: ^2.0.0
  freezed: ^2.0.0
  injectable_generator: ^2.0.0

Run flutter pub get to fetch the packages.

Next, configure injectable by creating a configuration file.

Create a file named configure_dependencies.dart:

import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
import 'configure_dependencies.config.dart';

final getIt = GetIt.instance;
@InjectableInit()
Future<void> configureDependencies() async => $initGetIt(getIt);

Run the command to generate the necessary code:

flutter pub run build_runner build

Step 2: Creating Login and Signup Cubits with Freezed State

Creating Login State and Cubit (Single Page)

Create a new file named login_state.dart:

import 'package:freezed_annotation/freezed_annotation.dart';

part 'login_state.freezed.dart';
@freezed
class LoginState with _$LoginState {
  const factory LoginState({
    @Default(false) bool isLoading,
    @Default('') String email,
    @Default('') String password,
    String? errorMessage,
  }) = _LoginState;
}

This LoginState class will represent the state of our login Cubit, including fields for isLoading, email, password, and an optional errorMessage.

Next, create the login Cubit:

Create a new file named login_cubit.dart:

import 'package:bloc/bloc.dart';
import 'package:injectable/injectable.dart';
import 'login_state.dart';

@injectable
class LoginCubit extends Cubit<LoginState> {
  LoginCubit() : super(const LoginState());
  void login() async {
    emit(state.copyWith(isLoading: true));
    // Simulate a login process
    await Future.delayed(Duration(seconds: 2));
    emit(state.copyWith(isLoading: false, errorMessage: null));
  }
  void setEmail(String email) => emit(state.copyWith(email: email));
  void setPassword(String password) => emit(state.copyWith(password: password));
}

Creating Signup State and Cubit (Single Page)

Create a new file named signup_state.dart:

import 'package:freezed_annotation/freezed_annotation.dart';

part 'signup_state.freezed.dart';
@freezed
class SignupState with _$SignupState {
  const factory SignupState({
    @Default(false) bool isLoading,
    @Default('') String email,
    @Default('') String password,
    @Default('') String confirmPassword,
    String? errorMessage,
  }) = _SignupState;
}

This SignupState class will represent the state of our signup Cubit.

Next, create the signup Cubit:

Create a new file named signup_cubit.dart:

import 'package:bloc/bloc.dart';
import 'package:injectable/injectable.dart';
import 'signup_state.dart';

@injectable
class SignupCubit extends Cubit<SignupState> {
  SignupCubit() : super(const SignupState());
  void signup() async {
    emit(state.copyWith(isLoading: true));
    // Simulate a signup process
    await Future.delayed(Duration(seconds: 2));
    emit(state.copyWith(isLoading: false, errorMessage: null));
  }
  void setEmail(String email) => emit(state.copyWith(email: email));
  void setPassword(String password) => emit(state.copyWith(password: password));
  void setConfirmPassword(String confirmPassword) => emit(state.copyWith(confirmPassword: confirmPassword));
}

Step 3: Injecting the Cubits into Your Widget Tree

Now that we have our login and signup Cubits set up with injectable, we can inject them directly into our widget tree without using MultiBlocProvider.

First, ensure you call configureDependencies() in the main function:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await configureDependencies();
  runApp(MyApp());
}

Login Page Implementation

Create a new file named login_page.dart:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'configure_dependencies.dart';
import 'login_cubit.dart';

class LoginPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final loginCubit = getIt<LoginCubit>();
    return BlocProvider<LoginCubit>(
      create: (context) => loginCubit,
      child: Scaffold(
        appBar: AppBar(
          title: Text('Login Page'),
        ),
        body: Column(
          children: [
            BlocBuilder<LoginCubit, LoginState>(
              builder: (context, state) {
                return Column(
                  children: [
                    TextField(
                      onChanged: (email) => loginCubit.setEmail(email),
                      decoration: InputDecoration(labelText: 'Email'),
                    ),
                    TextField(
                      onChanged: (password) => loginCubit.setPassword(password),
                      decoration: InputDecoration(labelText: 'Password'),
                    ),
                    ElevatedButton(
                      onPressed: state.isLoading ? null : () => loginCubit.login(),
                      child: state.isLoading ? CircularProgressIndicator() : Text('Login'),
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

Signup Page Implementation

Create a new file named signup_page.dart:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'configure_dependencies.dart';
import 'signup_cubit.dart';

class SignupPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final signupCubit = getIt<SignupCubit>();
    return BlocProvider<SignupCubit>(
      create: (context) => signupCubit,
      child: Scaffold(
        appBar: AppBar(
          title: Text('Signup Page'),
        ),
        body: Column(
          children: [
            BlocBuilder<SignupCubit, SignupState>(
              builder: (context, state) {
                return Column(
                  children: [
                    TextField(
                      onChanged: (email) => signupCubit.setEmail(email),
                      decoration: InputDecoration(labelText: 'Email'),
                    ),
                    TextField(
                      onChanged: (password) => signupCubit.setPassword(password),
                      decoration: InputDecoration(labelText: 'Password'),
                    ),
                    TextField(
                      onChanged: (confirmPassword) => signupCubit.setConfirmPassword(confirmPassword),
                      decoration: InputDecoration(labelText: 'Confirm Password'),
                    ),
                    ElevatedButton(
                      onPressed: state.isLoading ? null : () => signupCubit.signup(),
                      child: state.isLoading ? CircularProgressIndicator() : Text('Sign Up'),
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

Benefits of This Approach

  • Modularity: Each Cubit is independently injectable, which makes the code more modular and easier to test.
  • Scalability: By avoiding MultiBlocProvider, you reduce the coupling between different parts of the widget tree, making it easier to scale the application as it grows.
  • Improved Maintainability: Using injectable ensures that dependencies are managed in one place, which makes it easier to maintain and update Cubits across the entire application.

Final Thoughts

I’d also like to thank this video from Eng: Omar Ahmed for providing valuable insights into managing state with Cubit and Bloc effectively.

Managing multiple Cubits in Flutter without MultiBlocProvider can lead to a more maintainable and scalable codebase. By leveraging dependency injection with injectable and using freezed for immutable state, you can create a clean architecture that is easy to extend and test. This approach keeps your widget tree clean and allows you to focus on building features rather than managing state complexities.


Originally published on Medium on November 13, 2024.

About the author

  • Software Engineer FullStack Developer " nest-js & nuxt js (vue.js) & Flutter"Mobile" "

No comments yet. Be the first to comment.

More from this blog