Git Product home page Git Product logo

fluttet_bloc_manager's Introduction

Multiplier project design pattern using BLoC

This article is about structuring Flutter app using BLoC pattern and how we avoided all the architectural issues we faced in previous projects.

What is BLoC pattern?

Business Logic Component (BLoC) is a state management design pattern recommended for Flutter. It helps in managing the app state and acts as a bridge between a data source and the widgets that need the data. It receives data/events as a stream, handles any required business logic, then publishes results data streams to the interested widgets. To implement this pattern, we are using bloc and flutter_bloc plugins.

BLoC
BLoC pattern

The problem

The recommended approach of using flutter_bloc in Flutter projects is by having at least one bloc associated with each screen/feature - something like a viewmodel in MVVM architecture - At the start, everything seemed to work perfectly, however, when the app grew and became more complex, we started to have difficulties managing the app due to the bloc’s high dependency on each other especially for global/common blocs that their states/values might be needed in various parts of the application such as account expiry and connectivity status.

In addition, adding new states for blocs caused an unexpected behavior because there is no single source of truth and a change in one place requires visiting many many classes to ensure the newly added state does not affect other parts of the code.

Our Goal

In order to avoid the complexity and the difficulty to manage the app state and its business logic, we tried to find a way to achieve some kind of isolation between individual features/screen and global/common business logic, for example, feature A does not know about connectivity bloc and does not depend or listen to it, alternatively, the connectivity data will be provided to it.

The feature, however, will have its own widgets and business logic that are focused and only specific to that particular feature, hence, extracting it and reusing it in another project is possible and straightforward. That could only be achieved if the feature encapsulates its own data and business logic and has external data injected into it.

The proposed solution

The solution is to;

  • Separate global app state(s) from the app features and have the data provided to the feature when needed making it flexible and easier to test.
  • Create a bloc manager in a single place that is responsible for handling bloc registration, fetching or disposing a registered bloc, and adding or removing state change listeners from a specific bloc.
  • Use the bloc manager to listen to state changes in global/shared blocs and pass them to an state dispatcher.
  • To have a single state dispatcher that is in charge of broadcasting states sent from the bloc manager to the active bloc expecting these states.
  • Create a common/shared states interface that is used in different parts of the app, so they can be reused instead of code duplication.

The following diagrams show how the app is structured;

Proposed BLoC pattern implementation
Proposed BLoC pattern implementation

Usage

This section shows how to create a new bloc/cubit following the proposed architecture and use the bloc manager to coordinate and manage the blocs/cubits. In this example we will be using cubit;

1- Create a base states class which contains the states that are common in which a cubit class or other base states interfaces can implement. For example;

abstract class BaseStateListener {}

abstract class AuthStateListener implements BaseStateListener {
  void onLogin(Authorize authorizedAccount);

  void onLogout();
}

abstract class ConnectivityStateListener implements BaseStateListener {
  void onDisconnect(DisconnectSource source);

  void onConnect();
}

2- Create a cubit for a feature, let’s call it FeatureCubit. The cubit class will implement both AuthStateListener and ConnectivityStateListener so it can expose the 4 methods in addition to any other feature-specific states. The type of the state FeatureCubit is managing in this example, is Status with initial value as initial;

enum Status {
  initial,
  loading,
  disabled,
  enabled,
  loggedOut,
  loggedIn,
}

The FeatureCubit will expose the common/share onConnect, onDisconnect, onLogout, onLogin methods and a feature-specific loading method;

import 'package:bloc/bloc.dart';

class FeatureCubit extends Cubit<Status> implements ConnectivityStateListener, AuthStateListener {
  FeatureCubit() : super(Status.initial);

  void loading() => emit(Status.loading);

  @override
  void onConnect() => emit(Status.enabled);

  @override
  void onDisconnect(DisconnectSource source) => emit(Status.disabled);

  @override
  void onLogin(Authorize authorizedAccount) => emit(Status.loggedIn);

  @override
  void onLogout() => emit(Status.loggedOut);
}

3- Cubits created should be registered with BlocManager class. It is important to decide when a cubit should be registered/provided or not, you should register a bloc only when needed and should dispose of it if it is not needed anymore. For example, we have 2 global cubits in which their states/values are needed at every stage of the app’s lifecycle, named Connectivity and Account. These 2 important cubits should be registered early at the app’s lifecycle. For example;

void main() {
  _registerBlocs();

  runApp(CounterApp());
}
void _registerBlocs() {
  BlocManager.instance.register(() => ConnectivityCubit());
  BlocManager.instance.register(() => AccountCubit());
  ...
}

void _registerStateDispatchers() {
  StateDispatcher(BlocManager.instance)
    ..register<ConnectivityCubit, ConnectionStateEmitter>(
      (BaseBlocManager blocManager) => ConnectivityStateEmitter(blocManager),
    )
    ..register<AccountCubit>(
      (BaseBlocManager blocManager) => AccountStateEmitter(blocManager),
    )
    ...;
}

As the snippet shows, we use BlocManager.instance.register() to register a new cubit. In order to dispose of that cubit, BlocManager.instance.dispose() could be called. So what does StateDispatcher(BlocManager.instance) do?

As we mentioned earlier, bloc manager will handle listening to state changes in shared blocs in order to notify or broadcast the states to interested cubits that expect an update. And that’s exactly what StateDispatcher(BlocManager.instance) do, it adds listeners to get state changes update and then will dispatch states to cubits, for example, to FeatureCubit as shown below;

abstract class BaseStateEmitter<S extends BaseStateListener, B extends BlocBase<Object>> {
  /// Initializes base state emitter.
  BaseStateEmitter(this.blocManager) {
    blocManager.registerStateEmitter(this);
  }

  /// Bloc manager instance.
  final BaseBlocManager blocManager;

  /// Handles states for state listener [S].
  void handleStates({required S stateListener, required Object state});

  /// Emits state to the listener.
  void call({required BaseStateListener stateListener, Object? state}) {
    if (stateListener is S) {
      handleStates(
        stateListener: stateListener,
        state: state ?? blocManager.fetch<B>().state,
      );
    }
  }
}
class ConnectionStateEmitter extends BaseStateEmitter<ConnectionStateListener, ConnectionCubit> {
  /// Initializes connection state emitter.
  ConnectionStateEmitter(BaseBlocManager blocManager) : super(blocManager);

  @override
  void handleStates({
    required ConnectionStateListener stateListener,
    required Object state,
  }) {
    if (state is ConnectionConnectedState) {
      stateListener.onConnected();
    } else if (state is ConnectionDisconnectedState) {
      stateListener.onDisconnect();
    } else if (state is ConnectionErrorState) {
      stateListener.onConnectionError(state.error);
    }
  }
}
void register<B extends BlocBase<Object>, S extends BaseStateEmitter<BaseStateListener, B>>(
  StateEmitterBuilder stateEmitterBuilder,
) {
  stateEmitterBuilder(blocManager);

  if (!blocManager.hasListener<B>(key)) {
    blocManager.addListener<B>(
      key: key,
      handler: (Object state) => _dispatcher<S>(state),
    );
  }
}

The example shows a state is active or not coming from the global AccountCubit, then the StateDispatcher will broadcast login or logout states. _dispatcher function looks for all registered blocs and sends the states to blocs implementing the BaseCubit class.

void _dispatcher<S extends BaseStateEmitter<BaseStateListener, BlocBase<Object>>>(
  Object state,
) =>
    blocManager.repository.forEach(
      (String key, BlocBase<Object> bloc) =>
          blocManager.emitCoreStates<S>(bloc: bloc, state: state),
    );

For example, bloc.onLogout(); will call void logout() at FeatureCubit which in turns, emits (Status.loggedOut) to update the UI or do an action. So how can we use FeatureCubit in a widget or a screen?

4- Cubit is registered when needed, so we register FeatureCubit when we navigate to FeaturePage,

class FeaturePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    BlocManager.instance.register<FeatureCubit>(() => FeatureCubit());

    return FeatureView();
  }
}

In order to use FeatureCubit in FeatureView, we should call fetch method from BlocManager class as following;

final _featureCubit = BlocManager.instance.fetch<FeatureCubit>();

then use a BlocBuilder;

BlocBuilder<FeatureCubit, Status>(
  cubit: _featureCubit,
  builder: (context, state) => ...  // Use the state.
);

The full class implementation looks as follows;

class FeatureView extends StatelessWidget {
  final _featureCubit = BlocManager.instance.fetch<FeatureCubit>();

  @override
  Widget build(BuildContext context) =>
    Builder(
      builder: (BuildContext context) => BlocBuilder<FeatureCubit, Status>(
        cubit: _featureCubit,
        builder: (context, state) {
          if (state == Status.enabled) {
            // Enabled state logic.
          } else if (state == Status.disabled) {
            // Disabled state logic.
          } else if (state == Status.loggedIn) {
            // Logged in state logic.
          } else if (state == Status.loggedOut) {
            // Logged out state logic.
          } else {
            // Initial state logic.
          }
        },
      ),
    );
}

Example project

To see a sample app of this implementation, please refer to this link.

fluttet_bloc_manager's People

Contributors

hamed-rezaee avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.