Skip to content

Commit

Permalink
bloc v0.6.0
Browse files Browse the repository at this point in the history
  • Loading branch information
felangel committed Oct 26, 2018
1 parent 529b50a commit 00d3dd2
Show file tree
Hide file tree
Showing 9 changed files with 310 additions and 38 deletions.
7 changes: 7 additions & 0 deletions packages/bloc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,10 @@ Minor Updates to Documentation
# 0.5.2

Additional minor Updates to Documentation.

# 0.6.0

`Transitions` and `initialState` updates.

- Added `Transition`s and `onTransition`
- Made `initialState` required
11 changes: 9 additions & 2 deletions packages/bloc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,29 @@ This design pattern helps to separate _presentation_ from _business logic_. Foll

**States** are the output of a Bloc. Presentation components can listen to the stream of states and redraw portions of themselves based on the given state (see `BlocBuilder` for more details).

**Transitions** occur when an `Event` is `dispatched` after `mapEventToState` has been called but before the `Bloc`'s state has been updated. A `Transition` consists of the currentState, the event which was dispatched, and the nextState.

## Bloc Interface

**initialState** is the state before any events have been processed (before `mapEventToState` has ever been called). `initialState` **must be implemented**.

**mapEventToState** is a method that **must be implemented** when a class extends `Bloc`. The function takes two arguments: state and event. `mapEventToState` is called whenever an event is `dispatched` by the presentation layer. `mapEventToState` must convert that event, along with the current state, into a new state and return the new state in the form of a `Stream` which is consumed by the presentation layer.

**dispatch** is a method that takes an `event` and triggers `mapEventToState`. `dispatch` may be called from the presentation layer or from within the Bloc (see examples) and notifies the Bloc of a new `event`.

**initialState** is the state before any events have been processed (before `mapEventToState` has ever been called). `initialState` is an optional getter. If unimplemented, initialState will be `null`.

**transform** is a method that can be overridden to transform the `Stream<Event>` before `mapEventToState` is called. This allows for operations like `distinct()` and `debounce()` to be used.

**onTransition** is a method that can be overridden to handle whenever a `Transition` occurs. A `Transition` occurs when a new `Event` is dispatched and `mapEventToState` is called. `onTransition` is called before a `Bloc`'s state has been updated. **It is a great place to add logging/analytics**.

## Usage

For simplicity we can create a Bloc that always returns a stream of static strings in response to any event. That would look something like:

```dart
class SimpleBloc extends Bloc<dynamic, String> {
@override
String get initialState => '';
@override
Stream<String> mapEventToState(String state, dynamic event) async* {
yield 'data';
Expand Down
42 changes: 20 additions & 22 deletions packages/bloc/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ import 'package:bloc/bloc.dart';

abstract class CounterEvent {}

class IncrementCounter extends CounterEvent {}
class IncrementCounter extends CounterEvent {
@override
String toString() => 'IncrementCounter';
}

class DecrementCounter extends CounterEvent {}
class DecrementCounter extends CounterEvent {
@override
String toString() => 'DecrementCounter';
}

class CounterBloc extends Bloc<CounterEvent, int> {
int get initialState => 0;

void increment() {
dispatch(IncrementCounter());
}
Expand All @@ -20,13 +24,23 @@ class CounterBloc extends Bloc<CounterEvent, int> {
}

@override
Stream<int> mapEventToState(int state, CounterEvent event) async* {
print('event: ${event.runtimeType}');
int get initialState => 0;

@override
void onTransition(Transition<CounterEvent, int> transition) {
print(transition.toString());
}

@override
Stream<int> mapEventToState(int state, CounterEvent event) async* {
if (event is IncrementCounter) {
/// Simulating Network Latency etc...
await Future<void>.delayed(Duration(seconds: 1));
yield state + 1;
}
if (event is DecrementCounter) {
/// Simulating Network Latency etc...
await Future<void>.delayed(Duration(milliseconds: 500));
yield state - 1;
}
}
Expand All @@ -35,29 +49,13 @@ class CounterBloc extends Bloc<CounterEvent, int> {
void main() async {
final counterBloc = CounterBloc();

counterBloc.state.listen((state) {
print('state: ${state.toString()}');
});

print('initialState: ${counterBloc.initialState}');

// Increment Phase
counterBloc.dispatch(IncrementCounter());
await Future<void>.delayed(Duration(seconds: 1));

counterBloc.dispatch(IncrementCounter());
await Future<void>.delayed(Duration(seconds: 1));

counterBloc.dispatch(IncrementCounter());
await Future<void>.delayed(Duration(seconds: 1));

// Decrement Phase
counterBloc.dispatch(DecrementCounter());
await Future<void>.delayed(Duration(seconds: 1));

counterBloc.dispatch(DecrementCounter());
await Future<void>.delayed(Duration(seconds: 1));

counterBloc.dispatch(DecrementCounter());
await Future<void>.delayed(Duration(seconds: 1));
}
1 change: 1 addition & 0 deletions packages/bloc/lib/bloc.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
library bloc;

export './src/bloc.dart';
export './src/transition.dart';
34 changes: 26 additions & 8 deletions packages/bloc/lib/src/bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import 'dart:async';

import 'package:rxdart/rxdart.dart';

import 'package:bloc/bloc.dart';

/// Takes a [Stream] of events as input
/// and transforms them into a [Stream] of states as output.
abstract class Bloc<E, S> {
Expand All @@ -14,12 +16,18 @@ abstract class Bloc<E, S> {
Stream<S> get state => _stateSubject;

/// Returns the state before any events have been `dispatched`.
S get initialState => null;
S get initialState;

Bloc() {
_bindStateSubject();
}

/// Called whenever a transition occurs with the given [Transition].
/// A [Transition] occurs when a new [Event] is dispatched and `mapEventToState` executed.
/// `onTransition` is called before a [Bloc]'s state has been updated.
/// A great spot to add logging/analytics.
void onTransition(Transition<E, S> transition) => null;

/// Takes an event and triggers `mapEventToState`.
/// `Dispatch` may be called from the presentation layer or from within the Bloc.
/// `Dispatch` notifies the [Bloc] of a new event.
Expand All @@ -46,13 +54,23 @@ abstract class Bloc<E, S> {
Stream<S> mapEventToState(S state, E event);

void _bindStateSubject() {
(transform(_eventSubject) as Observable<E>)
.concatMap(
(E event) => mapEventToState(_stateSubject.value ?? initialState, event),
)
.forEach(
(S state) {
_stateSubject.add(state);
E currentEvent;
S currentState;

(transform(_eventSubject) as Observable<E>).concatMap((E event) {
currentEvent = event;
currentState = _stateSubject.value ?? initialState;
return mapEventToState(currentState, event);
}).forEach(
(S nextState) {
onTransition(
Transition(
currentState: currentState,
event: currentEvent,
nextState: nextState,
),
);
_stateSubject.add(nextState);
},
);
}
Expand Down
35 changes: 35 additions & 0 deletions packages/bloc/lib/src/transition.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'package:meta/meta.dart';

/// Occurs when an `Event` is `dispatched` after `mapEventToState` has been called
/// but before the `Bloc`'s state has been updated.
/// A `Transition` consists of the currentState, the event which was dispatched, and the nextState.
class Transition<E, S> {
final S currentState;
final E event;
final S nextState;

const Transition({
@required this.currentState,
@required this.event,
@required this.nextState,
}) : assert(currentState != null),
assert(event != null),
assert(nextState != null);

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Transition<E, S> &&
runtimeType == other.runtimeType &&
currentState == other.currentState &&
event == other.event &&
nextState == other.nextState;

@override
int get hashCode =>
currentState.hashCode ^ event.hashCode ^ nextState.hashCode;

@override
String toString() =>
'Transition { currentState: ${currentState.toString()}, event: ${event.toString()}, nextState: ${nextState.toString()} }';
}
3 changes: 2 additions & 1 deletion packages/bloc/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
name: bloc
description: The goal of this package is to make it easy to implement the BLoC Design Pattern (Business Logic Component).
version: 0.5.2
version: 0.6.0
author: felix.angelov <[email protected]>
homepage: https://github.com/felangel/bloc/tree/master/packages/bloc

environment:
sdk: ">=2.0.0-dev.28.0 <3.0.0"

dependencies:
meta: ^1.1.6
rxdart: ">=0.18.1 <1.0.0"

dev_dependencies:
Expand Down
48 changes: 43 additions & 5 deletions packages/bloc/test/bloc_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import 'package:test/test.dart';
import 'package:bloc/bloc.dart';

class SimpleBloc extends Bloc<dynamic, String> {
@override
String get initialState => '';

@override
Stream<String> mapEventToState(String state, dynamic event) {
return Observable.just('data');
Expand Down Expand Up @@ -166,6 +169,8 @@ class IncrementCounter extends CounterEvent {

@override
int get hashCode => 7;

String toString() => 'IncrementCounter';
}

class DecrementCounter extends CounterEvent {
Expand All @@ -181,11 +186,20 @@ class DecrementCounter extends CounterEvent {

@override
int get hashCode => 8;

@override
String toString() => 'DecrementCounter';
}

typedef OnTransitionCallback = Function(Transition<CounterEvent, int>);

class CounterBloc extends Bloc<CounterEvent, int> {
int get initialState => 0;

final OnTransitionCallback onTransitionCallback;

CounterBloc([this.onTransitionCallback]);

@override
Stream<int> mapEventToState(int state, CounterEvent event) async* {
if (event is IncrementCounter) {
Expand All @@ -196,6 +210,11 @@ class CounterBloc extends Bloc<CounterEvent, int> {
}
}

@override
void onTransition(Transition<CounterEvent, int> transition) {
onTransitionCallback(transition);
}

@override
bool operator ==(Object other) =>
identical(this, other) ||
Expand Down Expand Up @@ -228,8 +247,8 @@ void main() {
simpleBloc.dispose();
});

test('initialState returns null when not implemented', () {
expect(simpleBloc.initialState, null);
test('initialState returns correct value', () {
expect(simpleBloc.initialState, '');
});

test('should map single event to correct state', () {
Expand Down Expand Up @@ -302,24 +321,36 @@ void main() {

group('CounterBloc', () {
CounterBloc counterBloc;
List<String> transitions;

final OnTransitionCallback onTransitionCallback = (transition) {
transitions.add(transition.toString());
};

setUp(() {
counterBloc = CounterBloc();
transitions = [];
counterBloc = CounterBloc(onTransitionCallback);
});

test('initial state is 0', () {
expect(counterBloc.initialState, 0);
expect(transitions.isEmpty, true);
});

test('single IncrementCounter event updates state to 1', () {
final List<int> expected = [
1,
];
final expectedTransitions = [
'Transition { currentState: 0, event: IncrementCounter, nextState: 1 }'
];

expectLater(
counterBloc.state,
emitsInOrder(expected),
);
).then((dynamic _) {
expectLater(transitions, expectedTransitions);
});

counterBloc.dispatch(IncrementCounter());
});
Expand All @@ -330,11 +361,18 @@ void main() {
2,
3,
];
final expectedTransitions = [
'Transition { currentState: 0, event: IncrementCounter, nextState: 1 }',
'Transition { currentState: 1, event: IncrementCounter, nextState: 2 }',
'Transition { currentState: 2, event: IncrementCounter, nextState: 3 }',
];

expectLater(
counterBloc.state,
emitsInOrder(expected),
);
).then((dynamic _) {
expect(transitions, expectedTransitions);
});

counterBloc.dispatch(IncrementCounter());
counterBloc.dispatch(IncrementCounter());
Expand Down
Loading

0 comments on commit 00d3dd2

Please sign in to comment.