Skip to content

Commit

Permalink
Move bloc related APIs to comms package (#60)
Browse files Browse the repository at this point in the history
* Move bloc related APIs to comms package

- update changelog and pubspec version

- add bloc package

- add ListenerBloc, ListenerCubit and StateSender

- add tests

- extract ProductCountChangedMessage to seperate file

Related to #59

* Update README
  • Loading branch information
lewandowski-jan committed Jul 20, 2023
1 parent e5002d8 commit 0049c21
Show file tree
Hide file tree
Showing 13 changed files with 291 additions and 10 deletions.
4 changes: 4 additions & 0 deletions packages/comms/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 1.0.1

- Move bloc related apis from flutter_comms package and update README (#60)

## 1.0.0

- Introduce covariant listening by filtering `Listener`s contravariantly (#58)
Expand Down
86 changes: 86 additions & 0 deletions packages/comms/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ void main() async {
print(lightBulbB.enabled); // false
lightSwitchB.enable();
await Future<void>.delayed(Duration.zero);
print(lightBulbA.enabled); // true
print(lightBulbB.enabled); // true
Expand Down Expand Up @@ -204,6 +206,90 @@ void main() {
}
```

## Communicating between blocs

To communicate between blocs you can just use `Sender` and `Listener` mixins, for
more convenience there are added `ListenerCubit` or `ListenerBloc` classes and
`StateSender` mixin.

### Creating a ListenerCubit

A `ListenerCubit` works exactly like `Listener` but calls `listen` and `cancel`
functions for you, enabling your `Cubit` to receive messages from any `Sender`
sharing the same message type.

```dart
/// Use `ListenerCubit` instead of `Cubit`, second type parameter specifies
/// message type to listen for.
class LightBulbCubit with ListenerCubit<LightBulbState, LightSwitchState> {
LightBulbCubit() : super(LightBulbState(false));
/// Override `onMessage` to specify how to react to messages.
@override
void onMessage(LightSwitchState message) {
if (message is LightSwitchEnabled) {
emit(LightBulbState(true));
} else if (message is LightSwitchDisabled) {
emit(LightBulbState(false));
}
}
}
class LightBulbState {
LightBulbState(this.enabled)
final bool enabled;
}
```

### Creating a StateSender
A `StateSender` mixin allows your bloc to send message with state every time a
new state is emitted.

```dart
/// Add a `Sender` mixin with type of messages to send.
class LightSwitchBloc extends Bloc<bool, LightSwitchState>
with StateSender {
LightSwitchBloc() : super(false) {
on<bool>(
(event, emit) {
if (event) {
emit(LightSwitchEnabled());
} else {
emit(LightSwitchDisabled());
}
}
)
};
}
abstract class LightSwitchState {}
class LightSwitchEnabled extends LightSwitchState {}
class LightSwitchDisabled extends LightSwitchState {}
```

### Using ListenerCubit and StateSender

```dart
void main() async {
// Just create instances of both classes, comms will
// handle connection between them.
final lightBulbCubit = LightBulCubit();
final lightSwitchBloc = LightSwitchBloc();
print(lightBulbCubit.state.enabled); // false
lightSwitchBloc.add(true);
await Future<void>.delayed(Duration.zero);
print(lightBulbCubit.state.enabled); // true
// comms will automatically clean up resources on close
lightBulbCubit.close();
lightSwitchBloc.close();
}
```

[pub_badge_style]: https://img.shields.io/badge/style-leancode__lint-black
[pub_badge_link]: https://pub.dartlang.org/packages/leancode_lint
[flutter_comms]: https://pub.dev/packages/flutter_comms
8 changes: 6 additions & 2 deletions packages/comms/lib/comms.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
library comms;

import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart' show nonVirtual, protected, visibleForTesting;
import 'package:meta/meta.dart'
show mustCallSuper, nonVirtual, protected, visibleForTesting;

part 'src/listener.dart';
part 'src/message_sink_register.dart';
part 'src/sender.dart';
part 'src/multi_listener.dart';
part 'src/bloc/listener_bloc.dart';
part 'src/bloc/listener_cubit.dart';
part 'src/bloc/state_sender.dart';
27 changes: 27 additions & 0 deletions packages/comms/lib/src/bloc/listener_bloc.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
part of '../../comms.dart';

/// Handles [Listener]'s [listen] and [close].
///
/// If your bloc needs to extend another class use [Listener] mixin.
///
/// Type argument [Message] marks what type of messages can be received.
///
/// See also:
///
/// * ListenerCubit, a class extending [Cubit] which handles calling [listen]
/// and [cancel] automatically.
/// * useMessageListener, a hook that provides [Listener]'s functionality
/// which handles calling [listen] and [cancel] automatically.
abstract class ListenerBloc<Event, State, Message> extends Bloc<Event, State>
with Listener<Message> {
ListenerBloc(super.initialState) {
super.listen();
}

@override
@mustCallSuper
Future<void> close() {
super.cancel();
return super.close();
}
}
29 changes: 29 additions & 0 deletions packages/comms/lib/src/bloc/listener_cubit.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
part of '../../comms.dart';

/// Handles [Listener]'s [listen] and [close].
///
/// Use instead of [Cubit].
///
/// If your cubit needs to extend another class use [Listener] mixin.
///
/// Type argument [Message] marks what type of messages can be received.
///
/// See also:
///
/// * ListenerBloc, a class extending [Bloc] which handles calling [listen]
/// and [cancel] automatically.
/// * useMessageListener, a hook that provides [Listener]'s functionality
/// which handles calling [listen] and [cancel] automatically.
abstract class ListenerCubit<State, Message> extends Cubit<State>
with Listener<Message> {
ListenerCubit(super.initialState) {
super.listen();
}

@override
@mustCallSuper
Future<void> close() {
super.cancel();
return super.close();
}
}
11 changes: 11 additions & 0 deletions packages/comms/lib/src/bloc/state_sender.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
part of '../../comms.dart';

/// Sends emitted [State]s to all [Listener]s of type [State].
mixin StateSender<State> on BlocBase<State> {
@override
@mustCallSuper
void onChange(Change<State> change) {
super.onChange(change);
getSend<State>()(change.nextState);
}
}
3 changes: 2 additions & 1 deletion packages/comms/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
name: comms
description: Simple communication pattern abstraction on streams, created for communication between logic classes.
version: 1.0.0
version: 1.0.1
homepage: https://github.com/leancodepl/comms

environment:
sdk: ">=3.0.0 <4.0.0"

dependencies:
bloc: ^8.1.2
logging: ^1.2.0
meta: ^1.9.1

Expand Down
70 changes: 70 additions & 0 deletions packages/comms/test/comms_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import 'package:comms/comms.dart';
import 'package:test/test.dart';

import 'listeners/product_count.dart';
import 'listeners/product_count_cubit.dart';
import 'messages/product_count_changed.dart';
import 'senders/basket.dart';
import 'senders/basket_cubit.dart';

int numberOfProductCountMessageSink() =>
MessageSinkRegister().getSinksOfType<ProductCountChangedMessage>().length;
Expand Down Expand Up @@ -151,4 +154,71 @@ void main() {
productCount.dispose();
},
);

group('ListenerCubit - Sender:', () {
test(
'ProductCountListenerCubit message sink is added to register after constructor',
() async {
final cubit = ProductCountListenerCubit();
expect(numberOfProductCountMessageSink(), 1);
await cubit.close();
},
);

test(
'ProductCountListenerCubit message sink is removed from register after close',
() async {
final cubit = ProductCountListenerCubit();
expect(numberOfProductCountMessageSink(), 1);
await cubit.close();
expect(numberOfProductCountMessageSink(), 0);
},
);

test(
'ProductCountListenerCubit state is consistent with number of elements in BasketCubit state',
() async {
final basketCubit = BasketCubit();
final productCountCubit = ProductCountListenerCubit();

basketCubit
..add('T-shirt')
..add('Socks');
await Future<void>.delayed(Duration.zero);

expect(productCountCubit.state, basketCubit.state.length);

basketCubit.removeLast();
await Future<void>.delayed(Duration.zero);

expect(productCountCubit.state, basketCubit.state.length);

await basketCubit.close();
await productCountCubit.close();
},
);

test(
'ProductCountListenerCubit correctly sets initial state using buffered message',
() async {
final basketCubit = BasketCubit()..add('Jeans');
final productCountCubit = ProductCountListenerCubit();

basketCubit
..add('T-shirt')
..add('Socks');
await Future<void>.delayed(Duration.zero);

expect(productCountCubit.state, basketCubit.state.length);

basketCubit.removeLast();
await Future<void>.delayed(Duration.zero);

expect(productCountCubit.state, basketCubit.state.length);

await basketCubit.close();
await productCountCubit.close();
},
);
});
}
8 changes: 2 additions & 6 deletions packages/comms/test/listeners/product_count.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'package:comms/comms.dart';

import '../messages/product_count_changed.dart';

class ProductCount with Listener<ProductCountChangedMessage> {
ProductCount() {
listen();
Expand Down Expand Up @@ -55,9 +57,3 @@ class ProductCountIncrementedListener with Listener<ProductCountIncremented> {
cancel();
}
}

class ProductCountChangedMessage {}

class ProductCountIncremented extends ProductCountChangedMessage {}

class ProductCountDecremented extends ProductCountChangedMessage {}
30 changes: 30 additions & 0 deletions packages/comms/test/listeners/product_count_cubit.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import 'package:comms/comms.dart';

import '../messages/product_count_changed.dart';

class ProductCountListenerCubit
extends ListenerCubit<int, ProductCountChangedMessage> {
ProductCountListenerCubit() : super(0);

@override
void onMessage(ProductCountChangedMessage message) {
if (message is ProductCountIncremented) {
_increment();
}
if (message is ProductCountDecremented) {
_decrement();
}
}

@override
void onInitialMessage(ProductCountChangedMessage message) =>
onMessage(message);

void _increment() {
emit(state + 1);
}

void _decrement() {
emit(state - 1);
}
}
5 changes: 5 additions & 0 deletions packages/comms/test/messages/product_count_changed.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class ProductCountChangedMessage {}

class ProductCountIncremented extends ProductCountChangedMessage {}

class ProductCountDecremented extends ProductCountChangedMessage {}
2 changes: 1 addition & 1 deletion packages/comms/test/senders/basket.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:comms/comms.dart';
import '../listeners/product_count.dart';
import '../messages/product_count_changed.dart';

class Basket with Sender<ProductCountChangedMessage> {
List<String> products = [];
Expand Down
18 changes: 18 additions & 0 deletions packages/comms/test/senders/basket_cubit.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'package:bloc/bloc.dart';
import 'package:comms/comms.dart';
import '../messages/product_count_changed.dart';

class BasketCubit extends Cubit<List<String>>
with Sender<ProductCountChangedMessage> {
BasketCubit() : super([]);

void add(String product) {
emit([...state, product]);
send(ProductCountIncremented());
}

void removeLast() {
emit([...state]..removeLast());
send(ProductCountDecremented());
}
}

0 comments on commit 0049c21

Please sign in to comment.