From bca92b1d99e0705a4fa11caa439cc95beda125ee Mon Sep 17 00:00:00 2001 From: Jan Lewandowski Date: Thu, 20 Jul 2023 18:01:21 +0200 Subject: [PATCH 1/2] 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 --- packages/comms/CHANGELOG.md | 4 ++ packages/comms/lib/comms.dart | 8 ++- .../comms/lib/src/bloc/listener_bloc.dart | 27 +++++++ .../comms/lib/src/bloc/listener_cubit.dart | 29 ++++++++ packages/comms/lib/src/bloc/state_sender.dart | 11 +++ packages/comms/pubspec.yaml | 3 +- packages/comms/test/comms_test.dart | 70 +++++++++++++++++++ .../comms/test/listeners/product_count.dart | 8 +-- .../test/listeners/product_count_cubit.dart | 30 ++++++++ .../test/messages/product_count_changed.dart | 5 ++ packages/comms/test/senders/basket.dart | 2 +- packages/comms/test/senders/basket_cubit.dart | 18 +++++ 12 files changed, 205 insertions(+), 10 deletions(-) create mode 100644 packages/comms/lib/src/bloc/listener_bloc.dart create mode 100644 packages/comms/lib/src/bloc/listener_cubit.dart create mode 100644 packages/comms/lib/src/bloc/state_sender.dart create mode 100644 packages/comms/test/listeners/product_count_cubit.dart create mode 100644 packages/comms/test/messages/product_count_changed.dart create mode 100644 packages/comms/test/senders/basket_cubit.dart diff --git a/packages/comms/CHANGELOG.md b/packages/comms/CHANGELOG.md index 41eb452..c43db29 100644 --- a/packages/comms/CHANGELOG.md +++ b/packages/comms/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.1 + +- Move bloc related apis from flutter_comms package (#60) + ## 1.0.0 - Introduce covariant listening by filtering `Listener`s contravariantly (#58) diff --git a/packages/comms/lib/comms.dart b/packages/comms/lib/comms.dart index 2ee0c9f..0db4a77 100644 --- a/packages/comms/lib/comms.dart +++ b/packages/comms/lib/comms.dart @@ -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'; diff --git a/packages/comms/lib/src/bloc/listener_bloc.dart b/packages/comms/lib/src/bloc/listener_bloc.dart new file mode 100644 index 0000000..27c58a4 --- /dev/null +++ b/packages/comms/lib/src/bloc/listener_bloc.dart @@ -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 extends Bloc + with Listener { + ListenerBloc(super.initialState) { + super.listen(); + } + + @override + @mustCallSuper + Future close() { + super.cancel(); + return super.close(); + } +} diff --git a/packages/comms/lib/src/bloc/listener_cubit.dart b/packages/comms/lib/src/bloc/listener_cubit.dart new file mode 100644 index 0000000..0ae42e8 --- /dev/null +++ b/packages/comms/lib/src/bloc/listener_cubit.dart @@ -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 extends Cubit + with Listener { + ListenerCubit(super.initialState) { + super.listen(); + } + + @override + @mustCallSuper + Future close() { + super.cancel(); + return super.close(); + } +} diff --git a/packages/comms/lib/src/bloc/state_sender.dart b/packages/comms/lib/src/bloc/state_sender.dart new file mode 100644 index 0000000..c8d7b51 --- /dev/null +++ b/packages/comms/lib/src/bloc/state_sender.dart @@ -0,0 +1,11 @@ +part of '../../comms.dart'; + +/// Sends emitted [State]s to all [Listener]s of type [State]. +mixin StateSender on BlocBase { + @override + @mustCallSuper + void onChange(Change change) { + super.onChange(change); + getSend()(change.nextState); + } +} diff --git a/packages/comms/pubspec.yaml b/packages/comms/pubspec.yaml index c0b8a17..65a7103 100644 --- a/packages/comms/pubspec.yaml +++ b/packages/comms/pubspec.yaml @@ -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 diff --git a/packages/comms/test/comms_test.dart b/packages/comms/test/comms_test.dart index 2ac33fd..ec274c7 100644 --- a/packages/comms/test/comms_test.dart +++ b/packages/comms/test/comms_test.dart @@ -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().length; @@ -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.delayed(Duration.zero); + + expect(productCountCubit.state, basketCubit.state.length); + + basketCubit.removeLast(); + await Future.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.delayed(Duration.zero); + + expect(productCountCubit.state, basketCubit.state.length); + + basketCubit.removeLast(); + await Future.delayed(Duration.zero); + + expect(productCountCubit.state, basketCubit.state.length); + + await basketCubit.close(); + await productCountCubit.close(); + }, + ); + }); } diff --git a/packages/comms/test/listeners/product_count.dart b/packages/comms/test/listeners/product_count.dart index 60829d8..f78c017 100644 --- a/packages/comms/test/listeners/product_count.dart +++ b/packages/comms/test/listeners/product_count.dart @@ -1,5 +1,7 @@ import 'package:comms/comms.dart'; +import '../messages/product_count_changed.dart'; + class ProductCount with Listener { ProductCount() { listen(); @@ -55,9 +57,3 @@ class ProductCountIncrementedListener with Listener { cancel(); } } - -class ProductCountChangedMessage {} - -class ProductCountIncremented extends ProductCountChangedMessage {} - -class ProductCountDecremented extends ProductCountChangedMessage {} diff --git a/packages/comms/test/listeners/product_count_cubit.dart b/packages/comms/test/listeners/product_count_cubit.dart new file mode 100644 index 0000000..05e171d --- /dev/null +++ b/packages/comms/test/listeners/product_count_cubit.dart @@ -0,0 +1,30 @@ +import 'package:comms/comms.dart'; + +import '../messages/product_count_changed.dart'; + +class ProductCountListenerCubit + extends ListenerCubit { + 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); + } +} diff --git a/packages/comms/test/messages/product_count_changed.dart b/packages/comms/test/messages/product_count_changed.dart new file mode 100644 index 0000000..e8dcc64 --- /dev/null +++ b/packages/comms/test/messages/product_count_changed.dart @@ -0,0 +1,5 @@ +class ProductCountChangedMessage {} + +class ProductCountIncremented extends ProductCountChangedMessage {} + +class ProductCountDecremented extends ProductCountChangedMessage {} diff --git a/packages/comms/test/senders/basket.dart b/packages/comms/test/senders/basket.dart index 480d0b7..9f10df4 100644 --- a/packages/comms/test/senders/basket.dart +++ b/packages/comms/test/senders/basket.dart @@ -1,5 +1,5 @@ import 'package:comms/comms.dart'; -import '../listeners/product_count.dart'; +import '../messages/product_count_changed.dart'; class Basket with Sender { List products = []; diff --git a/packages/comms/test/senders/basket_cubit.dart b/packages/comms/test/senders/basket_cubit.dart new file mode 100644 index 0000000..332cbed --- /dev/null +++ b/packages/comms/test/senders/basket_cubit.dart @@ -0,0 +1,18 @@ +import 'package:bloc/bloc.dart'; +import 'package:comms/comms.dart'; +import '../messages/product_count_changed.dart'; + +class BasketCubit extends Cubit> + with Sender { + BasketCubit() : super([]); + + void add(String product) { + emit([...state, product]); + send(ProductCountIncremented()); + } + + void removeLast() { + emit([...state]..removeLast()); + send(ProductCountDecremented()); + } +} From 0106be4422a63f081398acdf2f1f29e47ee3c14d Mon Sep 17 00:00:00 2001 From: Jan Lewandowski Date: Thu, 20 Jul 2023 18:24:09 +0200 Subject: [PATCH 2/2] Update README --- packages/comms/CHANGELOG.md | 2 +- packages/comms/README.md | 86 +++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/packages/comms/CHANGELOG.md b/packages/comms/CHANGELOG.md index c43db29..5f39ee4 100644 --- a/packages/comms/CHANGELOG.md +++ b/packages/comms/CHANGELOG.md @@ -1,6 +1,6 @@ ## 1.0.1 -- Move bloc related apis from flutter_comms package (#60) +- Move bloc related apis from flutter_comms package and update README (#60) ## 1.0.0 diff --git a/packages/comms/README.md b/packages/comms/README.md index fa12208..d01e8e4 100644 --- a/packages/comms/README.md +++ b/packages/comms/README.md @@ -102,6 +102,8 @@ void main() async { print(lightBulbB.enabled); // false lightSwitchB.enable(); + + await Future.delayed(Duration.zero); print(lightBulbA.enabled); // true print(lightBulbB.enabled); // true @@ -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 { + 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 + with StateSender { + LightSwitchBloc() : super(false) { + on( + (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.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 \ No newline at end of file