isolate_bloc

A dart package that helps implement the BLoC pattern. BLoC works in Isolate and don't slow down UI.


License
MIT

Documentation

A dart package that helps implement the BLoC pattern.

Overview

You can read about BLoC pattern here.

The main difference from another BLoC pattern implementations is what blocs work in Isolate and don't slow down UI.

Attention

This package now in beta and you should use it in pet projects only. If you find a bug or want some new feature please create a new issue.

Creating a Bloc

class CounterBloc extends IsolateBloc<CountEvent, int> {
  /// The initial state of the `CounterBloc` is 0.
  CounterBloc() : super(0);

  /// When `CountEvent` is received, the current state
  /// of the bloc is accessed via `state` and
  /// a new `state` is emitted via `emit`.
  @override
  void onEventReceived(CountEvent event) {
    emit(event == CountEvent.increment ? state+1 : state-1);
  }
}

Registering a Bloc

void main() async {
  await initialize(isolatedFunc);
  ...
}

/// Global function which is used to register blocs and called in Isolate
void isolatedFunc() {
  /// Register a bloc to be able to create it in main Isolate
  register(create: () => CounterBloc());
}

Using Bloc in UI

YourWidget(
  /// Create CounterBloc and provide it down to the widget tree
  child: IsolateBlocProvider<CounterBloc, int>(
    child: CounterScreen(),
  ),
)
...
class CounterScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Counter'),
      ),
      body: Center(
        /// Listen for CounterBloc State
        child: IsolateBlocListener<CounterBloc, int>(
          listener: (context, state) => print("New bloc state: $state"),
          /// Build widget based on CounterBloc's State
          child: IsolateBlocBuilder<CounterBloc, int>(
            builder: (context, state) {
              return Text('You tapped $state times');
            },
          ),
        ),
      ),
      floatingActionButton: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          FloatingActionButton(
            heroTag: 'Increment',
            /// Get bloc using extension and add new event
            onPressed: () => context.isolateBloc<CounterBloc, int>().add(CountEvent.increment),
            child: Icon(Icons.add),
          ),
          SizedBox(height: 10),
          FloatingActionButton(
            heroTag: 'Decrement',
            /// Get bloc using provider class and add new event
            onPressed: () => IsolateBlocProvider.of<CounterBloc>(context).add(CountEvent.decrement),
            child: Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}

All Api

IsolateBlocWrapper

IsolateBlocWrapper work like a client for IsolateBloc. It receives IsolateBloc's states and send events added by wrapperInstance.add(YourEvent()). So you can listen for origin bloc's state with wrapperInstance.listen((state) { }) and add events as shown above. createBloc function create IsolateBloc in Isolate and return IsolateBlocWrapper.

Initialization

Initialize all services required to work with IsolateBloc and register an IsolateBloc. isolatedFunc may be a future and MUST be a GLOBAL or STATIC function.

void main() async {
  /// Initialize
  await initialize(isolatedFunc);
  ...
}

/// Global function which is used to register blocs and called in Isolate
void isolatedFunc() {
  /// Register a bloc to be able to create it in main Isolate
  register(create: () => CounterBloc());
}

For tests you can use initializeMock().

test('Bloc test', () async {
  await initializeMock(isolatedFunc);
});

Create new Bloc instance

To create a new instance of bloc you can use Widget or function.

/// Create with Widget
IsolateBlocProvider<BlocA, BlocAState>(
    child: ChildA(),
)
/// Create multiple blocs with Widget
MultiIsolateBlocProvider(
  providers: [
    IsolateBlocProvider<BlocA, BlocAState>(),
    IsolateBlocProvider<BlocB, BlocBState>(),
    IsolateBlocProvider<BlocC, BlocCState>(),
  ],
  child: ChildA(),
)
/// Create with function
final blocA = createBloc<BlocA, BlocAState>();

Get a Bloc

IsolateBlocBuilder<CounterBloc, int>(
  buildWhen: (state, newState) {
    /// return true/false to determine whether or not
    /// to rebuild the widget with state
  builder: (context, state) {
    /// return widget here based on BlocA's state
  },
)

IsolateBlocListener<CounterBloc, int>(
  listenWhen: (state, newState) {
    /// return true/false to determine whether or not
    /// to listen for state
  },
  listener: (context, state) {
    /// listen for state
  },
  child: ChildWidget(),
)

IsolateBlocConsumer<CounterHistoryBloc, List<int>>(
  listenWhen: (state, newState) {
    /// return true/false to determine whether or not
    /// to listen for state
  },
  listener: (context, state) {
    /// listen for state
  },
  buildWhen: (state, newState) {
    /// return true/false to determine whether or not
    /// to rebuild the widget with state
  },
  builder: (context, state) {
    /// return widget here based on BlocA's state
  },
)

Create Bloc Observer

void isolatedFunc() {
  IsolateBloc.observer = SimpleBlocObserver();
  register(create: () => CounterBloc());
}

class SimpleBlocObserver extends IsolateBlocObserver {
  void onEvent(IsolateBloc bloc, Object event) {
    print("New $event for $bloc");
    super.onEvent(bloc, event);
  }

  void onTransition(IsolateBloc bloc, Transition transition) {
    print("New state ${transition.nextState} from $bloc");
    super.onTransition(bloc, transition);
  }

  void onError(IsolateBloc bloc, Object error, StackTrace stackTrace) {
    print("$error in $bloc");
    super.onError(bloc, error, stackTrace);
  }
}

Use Bloc in another Bloc

You can use Bloc in another Bloc. You need to use getBloc<BlocA>() function which return IsolateBlocWrapper<BlocAState> to do so. Also, you may use BlocInjector typedef which is just a signature for getBloc.

getBloc<BlocA>() function works this way: firstly it is wait for user's initialization function secondly it is looks for created bloc with type BlocA. If it is finds any, so it returns this bloc. Else it checks whether the pool of free blocs contains the BlocA and return this bloc. Else it is creates a new BlocA and add to the pull of free blocs. So when UI will call create<BlocA, BlocAState>(), it will not create a new bloc but return free bloc from pull.

This mean you should call this function only once to prevent unexpected bloc creation.

void isolatedFunc() {
  register(create: () => CounterBloc());
  register(create: () => CounterHistoryBloc(getBloc));
}

class CounterBloc extends IsolateBloc<CountEvent, int> {
  CounterBloc() : super(0);

  @override
  void onEventReceived(CountEvent event) {
    emit(event == CountEvent.increment ? state + 1 : state - 1);
  }
}

class CounterHistoryBloc extends IsolateBloc<int, List<int>> {
  /// BlocInjector: IsolateBlocWrapper<State> Function<T extends IsolateBloc, State>()
  final BlocInjector injector;
  final _history = <int>[];

  CounterHistoryBloc(this.injector) : super([]) {
    injector<CounterBloc, int>().listen(onEventReceived);
  }

  @override
  void onEventReceived(int event) {
    emit(_history..add(event));
  }
}

Limitations

Your events and states cannot contain any objects. If you will try to send one of the following items you will get Illegal argument in isolate message runtime exception.

Lambda functions

Your event/state cannot contain anonymous functions (something like this final callback = () {}). Because of it you can't send BuildContext or ThemeData.

StackTrace

If you will try to send exception with StackTrace you will also get runtime exception.

ReceivePort

Just don't send this object.

Examples