Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move bloc related APIs to comms package #60

Merged
merged 2 commits into from
Jul 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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());
}
}