diff --git a/.github/workflows/bloc_test.yaml b/.github/workflows/bloc_test.yaml index 73ec8b025f8..2d19216b85a 100644 --- a/.github/workflows/bloc_test.yaml +++ b/.github/workflows/bloc_test.yaml @@ -18,7 +18,7 @@ jobs: working-directory: packages/bloc_test runs-on: ubuntu-latest container: - image: google/dart:2.10.0 + image: google/dart:2.12-dev steps: - uses: actions/checkout@v2 - name: Install Dependencies @@ -30,7 +30,7 @@ jobs: - name: Run tests run: dart test --coverage=coverage && pub run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --packages=.packages --report-on=lib - name: Check Code Coverage - uses: ChicagoFlutter/lcov-cop@v1.0.0 + uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 with: path: packages/bloc_test/coverage/lcov.info - name: Upload coverage to Codecov diff --git a/examples/flutter_complex_list/lib/simple_bloc_observer.dart b/examples/flutter_complex_list/lib/simple_bloc_observer.dart index 624547a936c..a8ef4795561 100644 --- a/examples/flutter_complex_list/lib/simple_bloc_observer.dart +++ b/examples/flutter_complex_list/lib/simple_bloc_observer.dart @@ -2,14 +2,14 @@ import 'package:bloc/bloc.dart'; class SimpleBlocObserver extends BlocObserver { @override - void onError(Cubit cubit, Object error, StackTrace stackTrace) { + void onError(Bloc bloc, Object error, StackTrace stackTrace) { print(error); - super.onError(cubit, error, stackTrace); + super.onError(bloc, error, stackTrace); } @override - void onChange(Cubit cubit, Change change) { - print(change); - super.onChange(cubit, change); + void onTransition(Bloc bloc, Transition transition) { + print(transition); + super.onTransition(bloc, transition); } } diff --git a/examples/flutter_complex_list/pubspec.yaml b/examples/flutter_complex_list/pubspec.yaml index 9d73f1c511e..b05fa058eab 100644 --- a/examples/flutter_complex_list/pubspec.yaml +++ b/examples/flutter_complex_list/pubspec.yaml @@ -14,5 +14,11 @@ dependencies: equatable: ^2.0.0-nullsafety.0 pedantic: ^1.10.0-nullsafety.3 +dependency_overrides: + bloc: + path: ../../packages/bloc + flutter_bloc: + path: ../../packages/flutter_bloc + flutter: uses-material-design: true diff --git a/examples/flutter_counter/lib/counter_observer.dart b/examples/flutter_counter/lib/counter_observer.dart index 57515955136..5f0e60c09e1 100644 --- a/examples/flutter_counter/lib/counter_observer.dart +++ b/examples/flutter_counter/lib/counter_observer.dart @@ -2,12 +2,12 @@ import 'package:bloc/bloc.dart'; /// {@template counter_observer} /// [BlocObserver] for the counter application which -/// observes all [Cubit] state changes. +/// observes all [Bloc] state changes. /// {@endtemplate} class CounterObserver extends BlocObserver { @override - void onChange(Cubit cubit, Change change) { - print('${cubit.runtimeType} $change'); - super.onChange(cubit, change); + void onTransition(Bloc bloc, Transition transition) { + print('${bloc.runtimeType} $transition'); + super.onTransition(bloc, transition); } } diff --git a/examples/flutter_counter/pubspec.yaml b/examples/flutter_counter/pubspec.yaml index 914dc305c7f..11dbb851c03 100644 --- a/examples/flutter_counter/pubspec.yaml +++ b/examples/flutter_counter/pubspec.yaml @@ -19,12 +19,14 @@ dependency_overrides: path: ../../packages/bloc flutter_bloc: path: ../../packages/flutter_bloc + bloc_test: + path: ../../packages/bloc_test dev_dependencies: flutter_test: sdk: flutter bloc_test: ^7.1.0 - mockito: ^4.0.0 + mocktail: ">=0.0.1-dev.7 <0.1.0" integration_test: ^1.0.0 flutter: diff --git a/examples/flutter_counter/test/counter/cubit/counter_cubit_test.dart b/examples/flutter_counter/test/counter/cubit/counter_cubit_test.dart index c0c079ac6b1..45e70b51319 100644 --- a/examples/flutter_counter/test/counter/cubit/counter_cubit_test.dart +++ b/examples/flutter_counter/test/counter/cubit/counter_cubit_test.dart @@ -1,4 +1,3 @@ -// ignore: import_of_legacy_library_into_null_safe import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_counter/counter/counter.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/examples/flutter_counter/test/counter/view/counter_view_test.dart b/examples/flutter_counter/test/counter/view/counter_view_test.dart index 5a8eeeee53b..95a62fc4cdb 100644 --- a/examples/flutter_counter/test/counter/view/counter_view_test.dart +++ b/examples/flutter_counter/test/counter/view/counter_view_test.dart @@ -1,14 +1,12 @@ -// ignore: import_of_legacy_library_into_null_safe import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_counter/counter/counter.dart'; import 'package:flutter_counter/counter/view/counter_view.dart'; import 'package:flutter_test/flutter_test.dart'; -// ignore: import_of_legacy_library_into_null_safe -import 'package:mockito/mockito.dart'; +import 'package:mocktail/mocktail.dart'; -class MockCounterCubit extends MockBloc implements CounterCubit {} +class MockCounterCubit extends MockCubit implements CounterCubit {} const _incrementButtonKey = Key('counterView_increment_floatingActionButton'); const _decrementButtonKey = Key('counterView_decrement_floatingActionButton'); @@ -18,13 +16,15 @@ void main() { setUp(() { counterCubit = MockCounterCubit(); - when(counterCubit.state).thenReturn(0); + when(counterCubit).calls(#state).thenReturn(0); + when(counterCubit).calls(#increment).thenReturn(() {}); + when(counterCubit).calls(#decrement).thenReturn(() {}); whenListen(counterCubit, Stream.value(0)); }); group('CounterView', () { testWidgets('renders current CounterCubit state', (tester) async { - when(counterCubit.state).thenReturn(42); + when(counterCubit).calls(#state).thenReturn(42); await tester.pumpWidget( MaterialApp( home: BlocProvider.value( @@ -46,7 +46,7 @@ void main() { ), ); await tester.tap(find.byKey(_incrementButtonKey)); - verify(counterCubit.increment()).called(1); + verify(counterCubit).called(#increment).once(); }); testWidgets('tapping decrement button invokes decrement', (tester) async { @@ -61,7 +61,7 @@ void main() { final decrementFinder = find.byKey(_decrementButtonKey); await tester.ensureVisible(decrementFinder); await tester.tap(decrementFinder); - verify(counterCubit.decrement()).called(1); + verify(counterCubit).called(#decrement).once(); }); }); } diff --git a/examples/flutter_shopping_cart/lib/simple_bloc_observer.dart b/examples/flutter_shopping_cart/lib/simple_bloc_observer.dart index 9346cd0e7c8..e06be9e4bf6 100644 --- a/examples/flutter_shopping_cart/lib/simple_bloc_observer.dart +++ b/examples/flutter_shopping_cart/lib/simple_bloc_observer.dart @@ -8,9 +8,9 @@ class SimpleBlocObserver extends BlocObserver { } @override - void onError(Cubit cubit, Object error, StackTrace stackTrace) { - print('${cubit.runtimeType} $error'); - super.onError(cubit, error, stackTrace); + void onError(Bloc bloc, Object error, StackTrace stackTrace) { + print('${bloc.runtimeType} $error'); + super.onError(bloc, error, stackTrace); } @override diff --git a/examples/flutter_todos/lib/blocs/simple_bloc_observer.dart b/examples/flutter_todos/lib/blocs/simple_bloc_observer.dart index 14fe810a0e7..6c22c3ac43b 100644 --- a/examples/flutter_todos/lib/blocs/simple_bloc_observer.dart +++ b/examples/flutter_todos/lib/blocs/simple_bloc_observer.dart @@ -16,8 +16,8 @@ class SimpleBlocObserver extends BlocObserver { } @override - void onError(Cubit cubit, Object error, StackTrace stackTrace) { + void onError(Bloc bloc, Object error, StackTrace stackTrace) { print(error); - super.onError(cubit, error, stackTrace); + super.onError(bloc, error, stackTrace); } } diff --git a/packages/bloc/example/main.dart b/packages/bloc/example/main.dart index 9ac82ad4310..a8fbc53b3cd 100644 --- a/packages/bloc/example/main.dart +++ b/packages/bloc/example/main.dart @@ -4,9 +4,9 @@ import 'package:bloc/bloc.dart'; class SimpleBlocObserver extends BlocObserver { @override - void onCreate(Cubit cubit) { - super.onCreate(cubit); - print('onCreate -- cubit: ${cubit.runtimeType}'); + void onCreate(Bloc bloc) { + super.onCreate(bloc); + print('onCreate -- bloc: ${bloc.runtimeType}'); } @override @@ -15,12 +15,6 @@ class SimpleBlocObserver extends BlocObserver { print('onEvent -- bloc: ${bloc.runtimeType}, event: $event'); } - @override - void onChange(Cubit cubit, Change change) { - super.onChange(cubit, change); - print('onChange -- cubit: ${cubit.runtimeType}, change: $change'); - } - @override void onTransition(Bloc bloc, Transition transition) { super.onTransition(bloc, transition); @@ -28,15 +22,15 @@ class SimpleBlocObserver extends BlocObserver { } @override - void onError(Cubit cubit, Object error, StackTrace stackTrace) { - print('onError -- cubit: ${cubit.runtimeType}, error: $error'); - super.onError(cubit, error, stackTrace); + void onError(Bloc bloc, Object error, StackTrace stackTrace) { + print('onError -- bloc: ${bloc.runtimeType}, error: $error'); + super.onError(bloc, error, stackTrace); } @override - void onClose(Cubit cubit) { - super.onClose(cubit); - print('onClose -- cubit: ${cubit.runtimeType}'); + void onClose(Bloc bloc) { + super.onClose(bloc); + print('onClose -- bloc: ${bloc.runtimeType}'); } } diff --git a/packages/bloc/lib/bloc.dart b/packages/bloc/lib/bloc.dart index 9556504c90d..45b4a235306 100644 --- a/packages/bloc/lib/bloc.dart +++ b/packages/bloc/lib/bloc.dart @@ -2,6 +2,4 @@ library bloc; export './src/bloc.dart'; export './src/bloc_observer.dart'; -export './src/change.dart'; -export './src/cubit.dart'; export './src/transition.dart'; diff --git a/packages/bloc/lib/src/bloc.dart b/packages/bloc/lib/src/bloc.dart index d5b25c7d97b..e322b06807b 100644 --- a/packages/bloc/lib/src/bloc.dart +++ b/packages/bloc/lib/src/bloc.dart @@ -1,33 +1,75 @@ import 'dart:async'; +import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; -import 'bloc_observer.dart'; -import 'cubit.dart'; -import 'transition.dart'; - /// Signature for a mapper function which takes an [Event] as input /// and outputs a [Stream] of [Transition] objects. typedef TransitionFunction = Stream> Function(Event); +/// {@template bloc_unhandled_error_exception} +/// Exception thrown when an unhandled error occurs within a [Bloc]. +/// +/// _Note: thrown in debug mode only_ +/// {@endtemplate} +class BlocUnhandledErrorException implements Exception { + /// {@macro bloc_unhandled_error_exception} + BlocUnhandledErrorException( + this.bloc, + this.error, [ + this.stackTrace = StackTrace.empty, + ]); + + /// The [bloc] in which the unhandled error occurred. + final Bloc bloc; + + /// The unhandled [error] object. + final Object error; + + /// Stack trace which accompanied the error. + /// May be [StackTrace.empty] if no stack trace was provided. + final StackTrace stackTrace; + + @override + String toString() { + return 'Unhandled error $error occurred in $bloc.\n' + '$stackTrace'; + } +} + /// {@template bloc} /// Takes a `Stream` of `Events` as input /// and transforms them into a `Stream` of `States` as output. /// {@endtemplate} -abstract class Bloc extends Cubit +abstract class Bloc extends Stream implements EventSink { /// {@macro bloc} - Bloc(State initialState) : super(initialState) { - _bindEventsToStates(); + Bloc(this._state) { + // ignore: invalid_use_of_protected_member + observer.onCreate(this); + if (this is! Cubit) _bindEventsToStates(); } + /// The current [state]. + State get state => _state; + /// The current [BlocObserver]. static BlocObserver observer = BlocObserver(); - final _eventController = StreamController.broadcast(); + State _state; + + StreamSubscription>? _transitionSubscription; + + StreamController? __stateController; + StreamController get _stateController { + return __stateController ??= StreamController.broadcast(); + } - late StreamSubscription> _transitionSubscription; + StreamController? __eventController; + StreamController get _eventController { + return __eventController ??= StreamController(); + } bool _emitted = false; @@ -45,6 +87,30 @@ abstract class Bloc extends Cubit } } + /// Adds a subscription to the `Stream`. + /// Returns a [StreamSubscription] which handles events from + /// the `Stream` using the provided [onData], [onError] and [onDone] + /// handlers. + @override + StreamSubscription listen( + void Function(State)? onData, { + Function? onError, + void Function()? onDone, + bool? cancelOnError, + }) { + return _stateController.stream.listen( + onData, + onError: onError, + onDone: onDone, + cancelOnError: cancelOnError, + ); + } + + /// Returns whether the `Stream` is a broadcast stream. + /// Every [Bloc] is a broadcast stream. + @override + bool get isBroadcast => true; + /// Called whenever an [event] is [add]ed to the [Bloc]. /// A great spot to add logging/analytics at the individual [Bloc] level. /// @@ -109,6 +175,31 @@ abstract class Bloc extends Cubit return events.asyncExpand(transitionFn); } + /// {@template emit} + /// **[emit] should never be used outside of tests.** + /// + /// Updates the state of the bloc to the provided [state]. + /// A bloc's state should only be updated by `yielding` a new `state` + /// from `mapEventToState` in response to an event. + /// {@endtemplate} + @protected + @visibleForTesting + void emit(State state) { + if (_stateController.isClosed) return; + if (state == _state && _emitted) return; + if (this is Cubit) { + onTransition( + Transition( + currentState: this.state, + nextState: state, + ), + ); + } + _state = state; + _stateController.add(_state); + _emitted = true; + } + /// Must be implemented when a class extends [Bloc]. /// [mapEventToState] is called whenever an [event] is [add]ed /// and is responsible for converting that [event] into a new [state]. @@ -172,7 +263,7 @@ abstract class Bloc extends Cubit onError(error, stackTrace ?? StackTrace.current); } - /// Called whenever an [error] is thrown within [mapEventToState]. + /// Called whenever an [error] occurs within a [Bloc]. /// By default all [error]s will be ignored and [Bloc] functionality will be /// unaffected. /// A great spot to handle errors at the individual [Bloc] level. @@ -187,16 +278,14 @@ abstract class Bloc extends Cubit /// super.onError(error, stackTrace); /// } /// ``` - /// - /// See also: - /// - /// * [BlocObserver] for observing [Bloc] behavior globally. - /// @protected @mustCallSuper - @override void onError(Object error, StackTrace stackTrace) { - super.onError(error, stackTrace); + // ignore: invalid_use_of_protected_member + observer.onError(this, error, stackTrace); + assert(() { + throw BlocUnhandledErrorException(this, error, stackTrace); + }()); } /// Closes the `event` and `state` `Streams`. @@ -208,20 +297,15 @@ abstract class Bloc extends Cubit @override @mustCallSuper Future close() async { - await _eventController.close(); - await _transitionSubscription.cancel(); - return super.close(); + // ignore: invalid_use_of_protected_member + observer.onClose(this); + if (this is! Cubit) { + await _eventController.close(); + await _transitionSubscription?.cancel(); + } + await _stateController.close(); } - /// **[emit] should never be used outside of tests.** - /// - /// Updates the state of the bloc to the provided [state]. - /// A bloc's state should only be updated by `yielding` a new `state` - /// from `mapEventToState` in response to an event. - @visibleForTesting - @override - void emit(State state) => super.emit(state); - void _bindEventsToStates() { _transitionSubscription = transformTransitions( transformEvents( @@ -243,9 +327,65 @@ abstract class Bloc extends Cubit } catch (error, stackTrace) { onError(error, stackTrace); } - _emitted = true; }, onError: onError, ); } } + +/// {@template cubit} +/// A [Cubit] is a type of [Bloc] which has no notion of events +/// and relies on methods to [emit] new states. +/// +/// Every [Cubit] requires an initial state which will be the +/// state of the [Cubit] before [emit] has been called. +/// +/// The current state of a [Cubit] can be accessed via the [state] getter. +/// +/// ```dart +/// class CounterCubit extends Cubit { +/// CounterCubit() : super(0); +/// +/// void increment() => emit(state + 1); +/// } +/// ``` +/// +/// {@endtemplate} +abstract class Cubit extends Bloc { + /// {@macro cubit} + Cubit(State initialState) : super(initialState); + + /// Updates the [state] to the provided [state]. + /// [emit] does nothing if the [Cubit] has been closed or if the + /// [state] being emitted is equal to the current [state]. + /// + /// To allow for the possibility of notifying listeners of the initial state, + /// emitting a state which is equal to the initial state is allowed as long + /// as it is the first thing emitted by the [Cubit]. + @override + void emit(State state) => super.emit(state); + + @nonVirtual + @override + Stream mapEventToState(Null event) { + throw StateError( + 'mapEventToState should never be invoked on an instance of type Cubit', + ); + } + + @mustCallSuper + @override + void onError(Object error, StackTrace stackTrace) { + super.onError(error, stackTrace); + } + + @mustCallSuper + @override + void onTransition(Transition transition) { + super.onTransition(transition); + } + + @mustCallSuper + @override + Future close() => super.close(); +} diff --git a/packages/bloc/lib/src/bloc_observer.dart b/packages/bloc/lib/src/bloc_observer.dart index c1e0ee626b5..590369b3ff8 100644 --- a/packages/bloc/lib/src/bloc_observer.dart +++ b/packages/bloc/lib/src/bloc_observer.dart @@ -1,19 +1,15 @@ +import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; -import 'bloc.dart'; -import 'change.dart'; -import 'cubit.dart'; -import 'transition.dart'; - -/// An interface for observing the behavior of [Bloc] and [Cubit] instances. +/// An interface for observing the behavior of [Bloc]] instances. class BlocObserver { - /// Called whenever a [Cubit] is instantiated. + /// Called whenever a [Bloc] is instantiated. /// In many cases, a cubit may be lazily instantiated and /// [onCreate] can be used to observe exactly when the cubit /// instance is created. @protected @mustCallSuper - void onCreate(Cubit cubit) {} + void onCreate(Bloc bloc) {} /// Called whenever an [event] is `added` to any [bloc] with the given [bloc] /// and [event]. @@ -21,13 +17,6 @@ class BlocObserver { @mustCallSuper void onEvent(Bloc bloc, Object? event) {} - /// Called whenever a [Change] occurs in any [cubit] - /// A [change] occurs when a new state is emitted. - /// [onChange] is called before a cubit's state has been updated. - @protected - @mustCallSuper - void onChange(Cubit cubit, Change change) {} - /// Called whenever a transition occurs in any [bloc] with the given [bloc] /// and [transition]. /// A [transition] occurs when a new `event` is `added` and `mapEventToState` @@ -42,13 +31,13 @@ class BlocObserver { /// was received without a stack trace. @protected @mustCallSuper - void onError(Cubit cubit, Object error, StackTrace stackTrace) {} + void onError(Bloc bloc, Object error, StackTrace stackTrace) {} - /// Called whenever a [Cubit] is closed. - /// [onClose] is called just before the [Cubit] is closed + /// Called whenever a [Bloc] is closed. + /// [onClose] is called just before the [Bloc] is closed /// and indicates that the particular instance will no longer /// emit new states. @protected @mustCallSuper - void onClose(Cubit cubit) {} + void onClose(Bloc bloc) {} } diff --git a/packages/bloc/lib/src/change.dart b/packages/bloc/lib/src/change.dart deleted file mode 100644 index ed7d3e98379..00000000000 --- a/packages/bloc/lib/src/change.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:meta/meta.dart'; - -/// {@template change} -/// A [Change] represents the change from one [State] to another. -/// A [Change] consists of the [currentState] and [nextState]. -/// {@endtemplate} -@immutable -class Change { - /// {@macro change} - const Change({required this.currentState, required this.nextState}); - - /// The current [State] at the time of the [Change]. - final State currentState; - - /// The next [State] at the time of the [Change]. - final State nextState; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is Change && - runtimeType == other.runtimeType && - currentState == other.currentState && - nextState == other.nextState; - - @override - int get hashCode => currentState.hashCode ^ nextState.hashCode; - - @override - String toString() { - return 'Change { currentState: $currentState, nextState: $nextState }'; - } -} diff --git a/packages/bloc/lib/src/cubit.dart b/packages/bloc/lib/src/cubit.dart deleted file mode 100644 index 9a89ffa251d..00000000000 --- a/packages/bloc/lib/src/cubit.dart +++ /dev/null @@ -1,182 +0,0 @@ -import 'dart:async'; - -import 'package:meta/meta.dart'; - -import 'bloc.dart'; -import 'bloc_observer.dart'; -import 'change.dart'; - -/// {@template cubit_unhandled_error_exception} -/// Exception thrown when an unhandled error occurs within a [Cubit]. -/// -/// _Note: thrown in debug mode only_ -/// {@endtemplate} -class CubitUnhandledErrorException implements Exception { - /// {@macro cubit_unhandled_error_exception} - CubitUnhandledErrorException( - this.cubit, - this.error, [ - this.stackTrace = StackTrace.empty, - ]); - - /// The [cubit] in which the unhandled error occurred. - final Cubit cubit; - - /// The unhandled [error] object. - final Object error; - - /// Stack trace which accompanied the error. - /// May be [StackTrace.empty] if no stack trace was provided. - final StackTrace stackTrace; - - @override - String toString() { - return 'Unhandled error $error occurred in $cubit.\n' - '$stackTrace'; - } -} - -/// {@template cubit} -/// A [Cubit] is a subset of [Bloc] which has no notion of events -/// and relies on methods to [emit] new states. -/// -/// Every [Cubit] requires an initial state which will be the -/// state of the [Cubit] before [emit] has been called. -/// -/// The current state of a [Cubit] can be accessed via the [state] getter. -/// -/// ```dart -/// class CounterCubit extends Cubit { -/// CounterCubit() : super(0); -/// -/// void increment() => emit(state + 1); -/// } -/// ``` -/// -/// {@endtemplate} -abstract class Cubit extends Stream { - /// {@macro cubit} - Cubit(this._state) { - // ignore: invalid_use_of_protected_member - _observer.onCreate(this); - } - - /// The current [state]. - State get state => _state; - - BlocObserver get _observer => Bloc.observer; - - late final _controller = StreamController.broadcast(); - - State _state; - - bool _emitted = false; - - /// {@template emit} - /// Updates the [state] to the provided [state]. - /// [emit] does nothing if the [Cubit] has been closed or if the - /// [state] being emitted is equal to the current [state]. - /// - /// To allow for the possibility of notifying listeners of the initial state, - /// emitting a state which is equal to the initial state is allowed as long - /// as it is the first thing emitted by the [Cubit]. - /// {@endtemplate} - @protected - @visibleForTesting - void emit(State state) { - if (_controller.isClosed) return; - if (state == _state && _emitted) return; - onChange(Change(currentState: this.state, nextState: state)); - _state = state; - _controller.add(_state); - _emitted = true; - } - - /// Notifies the [Cubit] of an [error] which triggers [onError]. - void addError(Object error, [StackTrace? stackTrace]) { - onError(error, stackTrace ?? StackTrace.current); - } - - /// Called whenever a [change] occurs with the given [change]. - /// A [change] occurs when a new `state` is emitted. - /// [onChange] is called before the `state` of the `cubit` is updated. - /// [onChange] is a great spot to add logging/analytics for a specific `cubit`. - /// - /// **Note: `super.onChange` should always be called first.** - /// ```dart - /// @override - /// void onChange(Change change) { - /// // Always call super.onChange with the current change - /// super.onChange(change); - /// - /// // Custom onChange logic goes here - /// } - /// ``` - /// - /// See also: - /// - /// * [BlocObserver] for observing [Cubit] behavior globally. - @mustCallSuper - void onChange(Change change) { - // ignore: invalid_use_of_protected_member - _observer.onChange(this, change); - } - - /// Called whenever an [error] occurs within a [Cubit]. - /// By default all [error]s will be ignored and [Cubit] functionality will be - /// unaffected. - /// A great spot to handle errors at the individual [Cubit] level. - /// - /// **Note: `super.onError` should always be called last.** - /// ```dart - /// @override - /// void onError(Object error, StackTrace stackTrace) { - /// // Custom onError logic goes here - /// - /// // Always call super.onError with the current error and stackTrace - /// super.onError(error, stackTrace); - /// } - /// ``` - @protected - @mustCallSuper - void onError(Object error, StackTrace stackTrace) { - // ignore: invalid_use_of_protected_member - _observer.onError(this, error, stackTrace); - assert(() { - throw CubitUnhandledErrorException(this, error, stackTrace); - }()); - } - - /// Adds a subscription to the `Stream`. - /// Returns a [StreamSubscription] which handles events from - /// the `Stream` using the provided [onData], [onError] and [onDone] - /// handlers. - @override - StreamSubscription listen( - void Function(State)? onData, { - Function? onError, - void Function()? onDone, - bool? cancelOnError, - }) { - return _controller.stream.listen( - onData, - onError: onError, - onDone: onDone, - cancelOnError: cancelOnError, - ); - } - - /// Returns whether the `Stream` is a broadcast stream. - /// Every [Cubit] is a broadcast stream. - @override - bool get isBroadcast => true; - - /// Closes the [Cubit]. - /// When close is called, new states can no longer be emitted. - @mustCallSuper - Future close() { - // ignore: invalid_use_of_protected_member - _observer.onClose(this); - return _controller.close(); - } -} diff --git a/packages/bloc/lib/src/transition.dart b/packages/bloc/lib/src/transition.dart index 33fc8ab2ed8..72755b4dd16 100644 --- a/packages/bloc/lib/src/transition.dart +++ b/packages/bloc/lib/src/transition.dart @@ -1,7 +1,5 @@ import 'package:meta/meta.dart'; -import 'change.dart'; - /// {@template transition} /// Occurs when an [event] is `added` after `mapEventToState` has been called /// but before the bloc's [State] has been updated. @@ -9,16 +7,22 @@ import 'change.dart'; /// `added`, and the [nextState]. /// {@endtemplate} @immutable -class Transition extends Change { +class Transition { /// {@macro transition} const Transition({ - required State currentState, - required this.event, - required State nextState, - }) : super(currentState: currentState, nextState: nextState); + required this.currentState, + this.event, + required this.nextState, + }); + + /// The current [State] at the time of the [Transition]. + final State currentState; /// The [Event] which triggered the current [Transition]. - final Event event; + final Event? event; + + /// The next [State] at the time of the [Transition]. + final State nextState; @override bool operator ==(Object other) => diff --git a/packages/bloc/pubspec.yaml b/packages/bloc/pubspec.yaml index 83bc99b13ac..0720204929d 100644 --- a/packages/bloc/pubspec.yaml +++ b/packages/bloc/pubspec.yaml @@ -10,10 +10,10 @@ environment: sdk: ">=2.12.0-0 <3.0.0" dependencies: - meta: ^1.3.0-nullsafety.6 + meta: ">=1.3.0-nullsafety.6 <1.3.0" dev_dependencies: coverage: ^0.14.2 - pedantic: ^1.10.0-nullsafety.3 - rxdart: ^0.26.0-nullsafety.0 - test: ^1.16.0-nullsafety.12 + pedantic: ">=1.10.0-nullsafety.3 <1.10.0" + rxdart: ">=0.26.0-nullsafety.0 <0.26.0" + test: ">=1.16.0-nullsafety.12 <1.16.0" diff --git a/packages/bloc/test/bloc_observer_test.dart b/packages/bloc/test/bloc_observer_test.dart index b1cc2ca3e0f..ec8c349970f 100644 --- a/packages/bloc/test/bloc_observer_test.dart +++ b/packages/bloc/test/bloc_observer_test.dart @@ -26,13 +26,6 @@ void main() { }); }); - group('onChange', () { - test('does nothing by default', () { - // ignore: invalid_use_of_protected_member - BlocObserver().onChange(bloc, transition); - }); - }); - group('onTransition', () { test('does nothing by default', () { // ignore: invalid_use_of_protected_member diff --git a/packages/bloc/test/bloc_test.dart b/packages/bloc/test/bloc_test.dart index 54c6bad29c7..4a5c111cef7 100644 --- a/packages/bloc/test/bloc_test.dart +++ b/packages/bloc/test/bloc_test.dart @@ -59,12 +59,6 @@ void main() { ), ) ]); - expect(observer.onChangeCalls, [ - OnChangeCall( - simpleBloc, - const Change(currentState: '', nextState: 'data'), - ) - ]); expect(simpleBloc.state, 'data'); }); @@ -90,12 +84,6 @@ void main() { ), ) ]); - expect(observer.onChangeCalls, [ - OnChangeCall( - simpleBloc, - const Change(currentState: '', nextState: 'data'), - ) - ]); expect(simpleBloc.state, 'data'); }); @@ -168,15 +156,6 @@ void main() { ), ) ]); - expect(observer.onChangeCalls, [ - OnChangeCall( - complexBloc, - Change( - currentState: ComplexStateA(), - nextState: ComplexStateB(), - ), - ) - ]); expect(complexBloc.state, ComplexStateB()); }); @@ -273,12 +252,6 @@ void main() { ), ) ]); - expect(observer.onChangeCalls, [ - OnChangeCall( - counterBloc, - const Change(currentState: 0, nextState: 1), - ) - ]); expect(counterBloc.state, 1); }); @@ -326,20 +299,6 @@ void main() { ), ), ]); - expect(observer.onChangeCalls, [ - OnChangeCall( - counterBloc, - const Change(currentState: 0, nextState: 1), - ), - OnChangeCall( - counterBloc, - const Change(currentState: 1, nextState: 2), - ), - OnChangeCall( - counterBloc, - const Change(currentState: 2, nextState: 3), - ) - ]); }); counterBloc @@ -458,38 +417,6 @@ void main() { ), ) ]); - expect(observer.onChangeCalls, [ - OnChangeCall( - asyncBloc, - Change( - currentState: AsyncState( - isLoading: false, - hasError: false, - isSuccess: false, - ), - nextState: AsyncState( - isLoading: true, - hasError: false, - isSuccess: false, - ), - ), - ), - OnChangeCall( - asyncBloc, - Change( - currentState: AsyncState( - isLoading: true, - hasError: false, - isSuccess: false, - ), - nextState: AsyncState( - isLoading: false, - hasError: false, - isSuccess: true, - ), - ), - ) - ]); expect( asyncBloc.state, AsyncState( @@ -549,22 +476,15 @@ void main() { nextState: 0, ), ]; - final expectedChanges = const >[ - Change(currentState: 0, nextState: -1), - Change(currentState: -1, nextState: 0), - ]; + final expectedStates = [-1, 0, emitsDone]; - final changes = >[]; final transitions = >[]; - final flatMapBloc = FlatMapBloc( - onChangeCallback: changes.add, onTransitionCallback: transitions.add, ); expectLater(flatMapBloc, emitsInOrder(expectedStates)) .then((dynamic _) { - expect(changes, expectedChanges); expect(transitions, expectedTransitions); }); flatMapBloc @@ -651,7 +571,7 @@ void main() { ..close(); }, (Object error, StackTrace stackTrace) { expect( - (error as CubitUnhandledErrorException).toString(), + (error as BlocUnhandledErrorException).toString(), contains( 'Unhandled error Exception: fatal exception occurred ' 'in Instance of \'CounterExceptionBloc\'.', @@ -672,7 +592,7 @@ void main() { )..addError(expectedError, StackTrace.current); }, (Object error, StackTrace stackTrace) { expect( - (error as CubitUnhandledErrorException).toString(), + (error as BlocUnhandledErrorException).toString(), contains( 'Unhandled error Exception: fatal exception occurred ' 'in Instance of \'OnExceptionBloc\'.', @@ -710,7 +630,7 @@ void main() { ..close(); }, (Object error, StackTrace stackTrace) { expect( - (error as CubitUnhandledErrorException).toString(), + (error as BlocUnhandledErrorException).toString(), contains( 'Unhandled error Exception: fatal exception occurred ' 'in Instance of \'OnExceptionBloc\'.', @@ -730,7 +650,7 @@ void main() { ..close(); }, (Object error, StackTrace stackTrace) { expect( - (error as CubitUnhandledErrorException).toString(), + (error as BlocUnhandledErrorException).toString(), contains( 'Unhandled error Exception: fatal exception occurred ' 'in Instance of \'OnEventErrorBloc\'.', diff --git a/packages/bloc/test/blocs/counter/flat_map_bloc.dart b/packages/bloc/test/blocs/counter/flat_map_bloc.dart index 1ea7c026256..ae46eabc199 100644 --- a/packages/bloc/test/blocs/counter/flat_map_bloc.dart +++ b/packages/bloc/test/blocs/counter/flat_map_bloc.dart @@ -4,17 +4,10 @@ import 'package:rxdart/rxdart.dart'; import '../blocs.dart'; class FlatMapBloc extends Bloc { - FlatMapBloc({this.onChangeCallback, this.onTransitionCallback}) : super(0); + FlatMapBloc({this.onTransitionCallback}) : super(0); - final void Function(Change)? onChangeCallback; final void Function(Transition)? onTransitionCallback; - @override - void onChange(Change change) { - super.onChange(change); - onChangeCallback?.call(change); - } - @override void onTransition(Transition transition) { super.onTransition(transition); diff --git a/packages/bloc/test/change_test.dart b/packages/bloc/test/change_test.dart deleted file mode 100644 index d8493a5f186..00000000000 --- a/packages/bloc/test/change_test.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:test/test.dart'; -import 'package:bloc/bloc.dart'; - -void main() { - group('Change Tests', () { - group('constructor', () { - test( - 'should not throw assertion error when initialized ' - 'with a null currentState', () { - expect( - () => const Change(currentState: null, nextState: 1), - isNot(throwsA(isA())), - ); - }); - - test( - 'should not throw assertion error ' - 'when initialized with a null nextState', () { - expect( - () => const Change(currentState: 0, nextState: null), - isNot(throwsA(isA())), - ); - }); - - test( - 'should not throw assertion error when initialized with ' - 'all required parameters', () { - try { - const Change(currentState: 0, nextState: 1); - } catch (_) { - fail( - 'should not throw error when initialized ' - 'with all required parameters', - ); - } - }); - }); - - group('== operator', () { - test('should return true if 2 Changes are equal', () { - final changeA = const Change(currentState: 0, nextState: 1); - final changeB = const Change(currentState: 0, nextState: 1); - - expect(changeA == changeB, isTrue); - }); - - test('should return false if 2 Changes are not equal', () { - final changeA = const Change(currentState: 0, nextState: 1); - final changeB = const Change(currentState: 0, nextState: -1); - - expect(changeA == changeB, isFalse); - }); - }); - - group('hashCode', () { - test('should return correct hashCode', () { - final change = const Change(currentState: 0, nextState: 1); - expect( - change.hashCode, - change.currentState.hashCode ^ change.nextState.hashCode, - ); - }); - }); - - group('toString', () { - test('should return correct string representation of Change', () { - final change = const Change(currentState: 0, nextState: 1); - - expect( - change.toString(), - 'Change { currentState: ${change.currentState.toString()}, ' - 'nextState: ${change.nextState.toString()} }', - ); - }); - }); - }); -} diff --git a/packages/bloc/test/cubit_test.dart b/packages/bloc/test/cubit_test.dart index ec2c5a50b51..adf647b0c0b 100644 --- a/packages/bloc/test/cubit_test.dart +++ b/packages/bloc/test/cubit_test.dart @@ -29,7 +29,7 @@ void main() { CounterCubit().addError(expectedError, StackTrace.current); }, (Object error, StackTrace stackTrace) { expect( - (error as CubitUnhandledErrorException).toString(), + (error as BlocUnhandledErrorException).toString(), contains( 'Unhandled error Exception: fatal exception occurred ' 'in Instance of \'CounterCubit\'.', @@ -41,7 +41,7 @@ void main() { }); }); - group('onChange', () { + group('onTransition', () { late MockBlocObserver observer; setUp(() { @@ -50,44 +50,51 @@ void main() { }); test('is not called for the initial state', () async { - final changes = >[]; - final cubit = CounterCubit(onChangeCallback: changes.add); + final transitions = >[]; + final cubit = CounterCubit(onTransitionCallback: transitions.add); await cubit.close(); - expect(changes, isEmpty); - expect(observer.onChangeCalls, isEmpty); + expect(transitions, isEmpty); }); test('is called with correct change for a single state change', () async { - final changes = >[]; - final cubit = CounterCubit(onChangeCallback: changes.add)..increment(); + final transitions = >[]; + final cubit = CounterCubit(onTransitionCallback: transitions.add) + ..increment(); await cubit.close(); expect( - changes, - const [Change(currentState: 0, nextState: 1)], + transitions, + const [Transition(currentState: 0, nextState: 1)], ); - expect(observer.onChangeCalls, [ - OnChangeCall(cubit, const Change(currentState: 0, nextState: 1)), - ]); }); - test('is called with correct changes for multiple state changes', + test('is called with correct transitions for multiple state transitions', () async { - final changes = >[]; - final cubit = CounterCubit(onChangeCallback: changes.add) + final transitions = >[]; + final cubit = CounterCubit(onTransitionCallback: transitions.add) ..increment() ..increment(); await cubit.close(); expect( - changes, + transitions, const [ - Change(currentState: 0, nextState: 1), - Change(currentState: 1, nextState: 2), + Transition(currentState: 0, nextState: 1), + Transition(currentState: 1, nextState: 2), ], ); - expect(observer.onChangeCalls, [ - OnChangeCall(cubit, const Change(currentState: 0, nextState: 1)), - OnChangeCall(cubit, const Change(currentState: 1, nextState: 2)), - ]); + }); + }); + + group('mapEventToState', () { + test('throws StateError', () { + final cubit = CounterCubit(); + final message = 'mapEventToState should never be invoked ' + 'on an instance of type Cubit'; + expect( + () => cubit.mapEventToState(null), + throwsA( + isA().having((e) => e.message, 'message', message), + ), + ); }); }); diff --git a/packages/bloc/test/cubits/counter_cubit.dart b/packages/bloc/test/cubits/counter_cubit.dart index 27661acc163..aadf6157394 100644 --- a/packages/bloc/test/cubits/counter_cubit.dart +++ b/packages/bloc/test/cubits/counter_cubit.dart @@ -1,16 +1,16 @@ import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { - CounterCubit({this.onChangeCallback}) : super(0); + CounterCubit({this.onTransitionCallback}) : super(0); - final void Function(Change)? onChangeCallback; + final void Function(Transition)? onTransitionCallback; void increment() => emit(state + 1); void decrement() => emit(state - 1); @override - void onChange(Change change) { - super.onChange(change); - onChangeCallback?.call(change); + void onTransition(Transition transition) { + super.onTransition(transition); + onTransitionCallback?.call(transition); } } diff --git a/packages/bloc/test/main.dart b/packages/bloc/test/main.dart index 3ebff542bfe..529d4c6ccbb 100644 --- a/packages/bloc/test/main.dart +++ b/packages/bloc/test/main.dart @@ -1,13 +1,11 @@ import 'bloc_observer_test.dart' as bloc_observer_test; import 'bloc_test.dart' as bloc_test; -import 'change_test.dart' as change_test; import 'cubit_test.dart' as cubit_test; import 'transition_test.dart' as transition_test; void main() { bloc_test.main(); bloc_observer_test.main(); - change_test.main(); transition_test.main(); cubit_test.main(); } diff --git a/packages/bloc/test/mocks/mock_bloc_observer.dart b/packages/bloc/test/mocks/mock_bloc_observer.dart index b873a032100..e25705317a4 100644 --- a/packages/bloc/test/mocks/mock_bloc_observer.dart +++ b/packages/bloc/test/mocks/mock_bloc_observer.dart @@ -1,36 +1,19 @@ import 'package:bloc/bloc.dart'; class OnCreateCall { - const OnCreateCall(this.cubit); + const OnCreateCall(this.bloc); - final Cubit cubit; - - @override - bool operator ==(Object o) { - if (identical(this, o)) return true; - - return o is OnCreateCall && o.cubit == cubit; - } - - @override - int get hashCode => cubit.hashCode; -} - -class OnChangeCall { - const OnChangeCall(this.cubit, this.change); - - final Change change; - final Cubit cubit; + final Bloc bloc; @override bool operator ==(Object o) { if (identical(this, o)) return true; - return o is OnChangeCall && o.change == change && o.cubit == cubit; + return o is OnCreateCall && o.bloc == bloc; } @override - int get hashCode => change.hashCode ^ cubit.hashCode; + int get hashCode => bloc.hashCode; } class OnTransitionCall { @@ -70,9 +53,9 @@ class OnEventCall { } class OnErrorCall { - const OnErrorCall(this.cubit, this.error, this.stackTrace); + const OnErrorCall(this.bloc, this.error, this.stackTrace); - final Cubit cubit; + final Bloc bloc; final Object error; final StackTrace? stackTrace; @@ -81,47 +64,41 @@ class OnErrorCall { if (identical(this, o)) return true; return o is OnErrorCall && - o.cubit == cubit && + o.bloc == bloc && o.error == error && o.stackTrace == stackTrace; } @override - int get hashCode => cubit.hashCode ^ error.hashCode ^ stackTrace.hashCode; + int get hashCode => bloc.hashCode ^ error.hashCode ^ stackTrace.hashCode; } class OnCloseCall { - const OnCloseCall(this.cubit); + const OnCloseCall(this.bloc); - final Cubit cubit; + final Bloc bloc; @override bool operator ==(Object o) { if (identical(this, o)) return true; - return o is OnCloseCall && o.cubit == cubit; + return o is OnCloseCall && o.bloc == bloc; } @override - int get hashCode => cubit.hashCode; + int get hashCode => bloc.hashCode; } class MockBlocObserver implements BlocObserver { final onCreateCalls = []; - final onChangeCalls = []; final onTransitionCalls = []; final onEventCalls = []; final onErrorCalls = []; final onCloseCalls = []; @override - void onCreate(Cubit cubit) { - onCreateCalls.add(OnCreateCall(cubit)); - } - - @override - void onChange(Cubit cubit, Change change) { - onChangeCalls.add(OnChangeCall(cubit, change)); + void onCreate(Bloc bloc) { + onCreateCalls.add(OnCreateCall(bloc)); } @override @@ -135,12 +112,12 @@ class MockBlocObserver implements BlocObserver { } @override - void onError(Cubit cubit, Object error, StackTrace? stackTrace) { - onErrorCalls.add(OnErrorCall(cubit, error, stackTrace)); + void onError(Bloc bloc, Object error, StackTrace? stackTrace) { + onErrorCalls.add(OnErrorCall(bloc, error, stackTrace)); } @override - void onClose(Cubit cubit) { - onCloseCalls.add(OnCloseCall(cubit)); + void onClose(Bloc bloc) { + onCloseCalls.add(OnCloseCall(bloc)); } } diff --git a/packages/bloc_test/CHANGELOG.md b/packages/bloc_test/CHANGELOG.md index 4fd7f15fbd0..3b4ef6f9fca 100644 --- a/packages/bloc_test/CHANGELOG.md +++ b/packages/bloc_test/CHANGELOG.md @@ -1,3 +1,12 @@ +# 8.0.0-nullsafety.0 + +- **BREAKING**: feat: opt into null safety +- **BREAKING**: feat: upgrade Dart SDK constraints to `>=2.12.0-0 <3.0.0` +- **BREAKING**: refactor: remove `emitsExactly` +- **BREAKING**: refactor: `MockBloc` uses [package:mocktail](https://pub.dev/packages/mocktail) +- **BREAKING**: feat: introduce `MockCubit` which uses [package:mocktail](https://pub.dev/packages/mocktail) +- feat: introduce `MockCubit` + # 7.1.0 - feat: add `seed` property to `blocTest` diff --git a/packages/bloc_test/example/main.dart b/packages/bloc_test/example/main.dart index 9044a8357c7..a36ff524025 100644 --- a/packages/bloc_test/example/main.dart +++ b/packages/bloc_test/example/main.dart @@ -5,10 +5,11 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:test/test.dart'; // Mock Cubit -class MockCounterCubit extends MockBloc implements CounterCubit {} +class MockCounterCubit extends MockCubit implements CounterCubit {} // Mock Bloc -class MockCounterBloc extends MockBloc implements CounterBloc {} +class MockCounterBloc extends MockBloc + implements CounterBloc {} void main() { mainCubit(); @@ -40,7 +41,7 @@ void mainCubit() { blocTest( 'emits [1] when increment is called', build: () => CounterCubit(), - act: (cubit) async => cubit.increment(), + act: (cubit) => cubit.increment(), expect: const [1], ); }); @@ -71,7 +72,7 @@ void mainBloc() { blocTest( 'emits [1] when CounterEvent.increment is added', build: () => CounterBloc(), - act: (bloc) async => bloc.add(CounterEvent.increment), + act: (bloc) => bloc.add(CounterEvent.increment), expect: const [1], ); }); diff --git a/packages/bloc_test/lib/bloc_test.dart b/packages/bloc_test/lib/bloc_test.dart index 6506cbb535f..8ef594961b2 100644 --- a/packages/bloc_test/lib/bloc_test.dart +++ b/packages/bloc_test/lib/bloc_test.dart @@ -1,6 +1,5 @@ library bloc_test; export 'src/bloc_test.dart'; -export 'src/emits_exactly.dart'; export 'src/mock_bloc.dart'; export 'src/when_listen.dart'; diff --git a/packages/bloc_test/lib/src/bloc_test.dart b/packages/bloc_test/lib/src/bloc_test.dart index 493c680cf80..7e2ed19a679 100644 --- a/packages/bloc_test/lib/src/bloc_test.dart +++ b/packages/bloc_test/lib/src/bloc_test.dart @@ -4,40 +4,40 @@ import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; import 'package:test/test.dart' as test; -/// Creates a new `cubit`-specific test case with the given [description]. -/// [blocTest] will handle asserting that the `cubit` emits the [expect]ed +/// Creates a new `bloc`-specific test case with the given [description]. +/// [blocTest] will handle asserting that the `bloc` emits the [expect]ed /// states (in order) after [act] is executed. /// [blocTest] also handles ensuring that no additional states are emitted -/// by closing the `cubit` stream before evaluating the [expect]ation. +/// by closing the `bloc` stream before evaluating the [expect]ation. /// -/// [build] should be used for all `cubit` initialization and preparation -/// and must return the `cubit` under test. +/// [build] should be used for all `bloc` initialization and preparation +/// and must return the `bloc` under test. /// -/// [seed] is an optional state which will be used to seed the `cubit` before +/// [seed] is an optional state which will be used to seed the `bloc` before /// [act] is called. /// -/// [act] is an optional callback which will be invoked with the `cubit` under -/// test and should be used to interact with the `cubit`. +/// [act] is an optional callback which will be invoked with the `bloc` under +/// test and should be used to interact with the `bloc`. /// /// [skip] is an optional `int` which can be used to skip any number of states. /// [skip] defaults to 0. /// /// [wait] is an optional `Duration` which can be used to wait for -/// async operations within the `cubit` under test such as `debounceTime`. +/// async operations within the `bloc` under test such as `debounceTime`. /// -/// [expect] is an optional `Iterable` of matchers which the `cubit` +/// [expect] is an optional `Iterable` of matchers which the `bloc` /// under test is expected to emit after [act] is executed. /// /// [verify] is an optional callback which is invoked after [expect] /// and can be used for additional verification/assertions. -/// [verify] is called with the `cubit` returned by [build]. +/// [verify] is called with the `bloc` returned by [build]. /// /// /// ```dart /// blocTest( -/// 'CounterCubit emits [1] when increment is called', -/// build: () => CounterCubit(), -/// act: (cubit) => cubit.increment(), +/// 'CounterBloc emits [1] when increment is added', +/// build: () => CounterBloc(), +/// act: (bloc) => bloc.add(CounterEvent.increment), /// expect: [1], /// ); /// ``` @@ -46,10 +46,10 @@ import 'package:test/test.dart' as test; /// /// ```dart /// blocTest( -/// 'CounterCubit emits [10] when seeded with 9', -/// build: () => CounterCubit(), +/// 'CounterBloc emits [10] when seeded with 9', +/// build: () => CounterBloc(), /// seed: 9, -/// act: (cubit) => cubit.increment(), +/// act: (bloc) => bloc.add(CounterEvent.increment), /// expect: [10], /// ); /// ``` @@ -60,12 +60,12 @@ import 'package:test/test.dart' as test; /// /// ```dart /// blocTest( -/// 'CounterCubit emits [2] when increment is called twice', -/// build: () => CounterCubit(), -/// act: (cubit) { -/// cubit -/// ..increment() -/// ..increment(); +/// 'CounterBloc emits [2] when increment is added twice', +/// build: () => CounterBloc(), +/// act: (bloc) { +/// bloc +/// ..add(CounterEvent.increment) +/// ..add(CounterEvent.increment); /// }, /// skip: 1, /// expect: [2], @@ -77,21 +77,21 @@ import 'package:test/test.dart' as test; /// /// ```dart /// blocTest( -/// 'CounterCubit emits [1] when increment is called', -/// build: () => CounterCubit(), -/// act: (cubit) => cubit.increment(), +/// 'CounterBloc emits [1] when increment is added', +/// build: () => CounterBloc(), +/// act: (bloc) => bloc.add(CounterEvent.increment), /// wait: const Duration(milliseconds: 300), /// expect: [1], /// ); /// ``` /// -/// [blocTest] can also be used to [verify] internal cubit functionality. +/// [blocTest] can also be used to [verify] internal bloc functionality. /// /// ```dart /// blocTest( -/// 'CounterCubit emits [1] when increment is called', -/// build: () => CounterCubit(), -/// act: (cubit) => cubit.increment(), +/// 'CounterBloc emits [1] when increment is added', +/// build: () => CounterBloc(), +/// act: (bloc) => bloc.add(CounterEvent.increment), /// expect: [1], /// verify: (_) { /// verify(repository.someMethod(any)).called(1); @@ -105,27 +105,26 @@ import 'package:test/test.dart' as test; /// /// ```dart /// blocTest( -/// 'emits [StateB] when emitB is called', -/// build: () => MyCubit(), -/// act: (cubit) => cubit.emitB(), +/// 'emits [StateB] when EventB is added', +/// build: () => MyBloc(), +/// act: (bloc) => bloc.add(EventB()), /// expect: [isA()], /// ); /// ``` @isTest -void blocTest, State>( +void blocTest, State>( String description, { - @required C Function() build, - State seed, - Function(C cubit) act, - Duration wait, + required B Function() build, + State? seed, + Function(B bloc)? act, + Duration? wait, int skip = 0, - Iterable expect, - Function(C cubit) verify, - Iterable errors, + Iterable? expect, + Function(B bloc)? verify, + Iterable? errors, }) { test.test(description, () async { - await runBlocTest( - description, + await testBloc( build: build, seed: seed, act: act, @@ -141,45 +140,44 @@ void blocTest, State>( /// Internal [blocTest] runner which is only visible for testing. /// This should never be used directly -- please use [blocTest] instead. @visibleForTesting -Future runBlocTest, State>( - String description, { - @required C Function() build, - State seed, - Function(C cubit) act, - Duration wait, +Future testBloc, State>({ + required B Function() build, + State? seed, + Function(B bloc)? act, + Duration? wait, int skip = 0, - Iterable expect, - Function(C cubit) verify, - Iterable errors, + Iterable? expect, + Function(B bloc)? verify, + Iterable? errors, }) async { final unhandledErrors = []; var shallowEquality = false; - await runZoned( + await runZonedGuarded( () async { final states = []; - final cubit = build(); + final bloc = build(); // ignore: invalid_use_of_visible_for_testing_member, invalid_use_of_protected_member - if (seed != null) cubit.emit(seed); - final subscription = cubit.skip(skip).listen(states.add); + if (seed != null) bloc.emit(seed); + final subscription = bloc.skip(skip).listen(states.add); try { - await act?.call(cubit); + await act?.call(bloc); } on Exception catch (error) { unhandledErrors.add( - error is CubitUnhandledErrorException ? error.error : error, + error is BlocUnhandledErrorException ? error.error : error, ); } if (wait != null) await Future.delayed(wait); await Future.delayed(Duration.zero); - await cubit.close(); + await bloc.close(); if (expect != null) { shallowEquality = '$states' == '$expect'; test.expect(states, expect); } await subscription.cancel(); - await verify?.call(cubit); + await verify?.call(bloc); }, - onError: (Object error) { - if (error is CubitUnhandledErrorException) { + (Object error, _) { + if (error is BlocUnhandledErrorException) { unhandledErrors.add(error.error); } else if (shallowEquality && error is test.TestFailure) { // ignore: only_throw_errors diff --git a/packages/bloc_test/lib/src/emits_exactly.dart b/packages/bloc_test/lib/src/emits_exactly.dart deleted file mode 100644 index f190714b0e0..00000000000 --- a/packages/bloc_test/lib/src/emits_exactly.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; -import 'package:bloc_test/bloc_test.dart'; -import 'package:test/test.dart'; - -/// **Deprecated and will be removed in v8.0.0. Please use [blocTest] instead.** -/// -/// Similar to `emitsInOrder` but asserts that the provided [bloc] -/// emits **only** the [expected] states in the **exact** order in which -/// they were provided. -/// -/// ```dart -/// test('emits [1] when CounterEvent.increment is added', () async { -/// final bloc = CounterBloc(); -/// bloc.add(CounterEvent.increment); -/// await emitsExactly(bloc, [1]); -/// }); -/// ``` -/// -/// [emitsExactly] also supports `Matchers` for states -/// which don't override `==` and `hashCode`. -/// -/// ```dart -/// test('emits [StateB] when EventB is added', () async { -/// final bloc = MyBloc(); -/// bloc.add(EventB()); -/// await emitsExactly(bloc, [isA()]); -/// }); -/// ``` -/// -/// [skip] is an optional `int` which defaults to 0 and can be used to skip any -/// number of states. -/// -/// ```dart -/// test('emits [2] when CounterEvent.increment is added twice', () async { -/// final bloc = CounterBloc(); -/// bloc..add(CounterEvent.increment)..add(CounterEvent.increment); -/// await emitsExactly(bloc, [2], skip: 1); -/// }); -/// ``` -/// -/// [emitsExactly] also takes an optional [duration] which is useful in cases -/// where the [bloc] is using `debounceTime` or other similar operators. -/// -/// ```dart -/// test('emits [1] when CounterEvent.increment is added', () async { -/// final bloc = CounterBloc(); -/// bloc.add(CounterEvent.increment); -/// await emitsExactly( -/// bloc, -/// [1], -/// duration: const Duration(milliseconds: 300), -/// ); -/// }); -/// ``` -@Deprecated('Use blocTest instead') -Future emitsExactly, State>( - B bloc, - Iterable expected, { - Duration duration, - int skip = 0, -}) async { - assert(bloc != null); - final states = []; - final subscription = bloc.skip(skip).listen(states.add); - if (duration != null) await Future.delayed(duration); - await bloc.close(); - expect(states, expected); - await subscription.cancel(); -} diff --git a/packages/bloc_test/lib/src/mock_bloc.dart b/packages/bloc_test/lib/src/mock_bloc.dart index 2716487b008..aead072c550 100644 --- a/packages/bloc_test/lib/src/mock_bloc.dart +++ b/packages/bloc_test/lib/src/mock_bloc.dart @@ -1,36 +1,60 @@ -// ignore_for_file: invalid_use_of_visible_for_testing_member -import 'dart:async'; - -import 'package:mockito/mockito.dart'; +import 'package:bloc/bloc.dart'; +import 'package:mocktail/mocktail.dart'; +/// {@template mock_bloc} /// Extend or mixin this class to mark the implementation as a [MockBloc]. /// -/// A mocked bloc/cubit implements all fields and methods with a default +/// A mocked bloc implements all fields and methods with a default /// implementation that does not throw a [NoSuchMethodError], /// and may be further customized at runtime to define how it may behave using /// [when] and `whenListen`. /// -/// _**Note**: It is critical to explicitly provide the bloc state -/// type when extending [MockBloc]_. +/// _**Note**: It is critical to explicitly provide the event and state +/// types when extending [MockBloc]_. /// /// **GOOD** /// ```dart -/// class MockCounterBloc extends MockBloc implements CounterBloc {} -/// class MockCounterCubit extends MockBloc implements CounterCubit {} +/// class MockCounterBloc extends MockBloc +/// implements CounterBloc {} /// ``` /// /// **BAD** /// ```dart /// class MockCounterBloc extends MockBloc implements CounterBloc {} -/// class MockCounterCubit extends MockBloc implements CounterCubit {} /// ``` -class MockBloc extends Mock { - @override - dynamic noSuchMethod(Invocation invocation, [Object returnValue]) { - final memberName = invocation.memberName.toString().split('"')[1]; - final dynamic result = super.noSuchMethod(invocation); - return (memberName == 'skip' && result == null) - ? Stream.empty() - : result; +/// {@endtemplate} +class MockBloc extends Mock implements Bloc { + /// {@macro mock_bloc} + MockBloc() { + when(this).calls(#add).thenReturn(null); + when(this).calls(#isBroadcast).thenReturn(true); + when(this).calls(#skip).thenAnswer((_) => Stream.empty()); + when(this) + .calls(#listen) + .thenAnswer((_) => Stream.empty().listen((_) {})); } } + +/// {@template mock_cubit} +/// Extend or mixin this class to mark the implementation as a [MockCubit]. +/// +/// A mocked cubit implements all fields and methods with a default +/// implementation that does not throw a [NoSuchMethodError], +/// and may be further customized at runtime to define how it may behave using +/// [when] and `whenListen`. +/// +/// _**Note**: It is critical to explicitly provide the state +/// types when extending [MockCubit]_. +/// +/// **GOOD** +/// ```dart +/// class MockCounterCubit extends MockCubit +/// implements CounterCubit {} +/// ``` +/// +/// **BAD** +/// ```dart +/// class MockCounterCubit extends MockBloc implements CounterCubit {} +/// ``` +/// {@endtemplate} +class MockCubit extends MockBloc implements Cubit {} diff --git a/packages/bloc_test/lib/src/when_listen.dart b/packages/bloc_test/lib/src/when_listen.dart index fd6f4149dc0..514d7f9ff1a 100644 --- a/packages/bloc_test/lib/src/when_listen.dart +++ b/packages/bloc_test/lib/src/when_listen.dart @@ -1,70 +1,63 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; -import 'package:mockito/mockito.dart'; +import 'package:mocktail/mocktail.dart'; -/// Creates a stub response for the `listen` method on a [cubit]. +/// Creates a stub response for the `listen` method on a [bloc]. /// Use [whenListen] if you want to return a canned `Stream` of states -/// for a [cubit] instance. +/// for a [bloc] instance. /// -/// [whenListen] also handles stubbing the `state` of the [cubit] to stay +/// [whenListen] also handles stubbing the `state` of the [bloc] to stay /// in sync with the emitted state. /// /// Return a canned state stream of `[0, 1, 2, 3]` -/// when `counterCubit.listen` is called. +/// when `counterBloc.listen` is called. /// /// ```dart -/// whenListen(counterCubit, Stream.fromIterable([0, 1, 2, 3])); +/// whenListen(counterBloc, Stream.fromIterable([0, 1, 2, 3])); /// ``` /// -/// Assert that the `counterCubit` state `Stream` is the canned `Stream`. +/// Assert that the `counterBloc` state `Stream` is the canned `Stream`. /// /// ```dart /// await expectLater( -/// counterCubit, +/// counterBloc, /// emitsInOrder( /// [equals(0), equals(1), equals(2), equals(3), emitsDone], /// ) /// ); -/// expect(counterCubit.state, equals(3)); +/// expect(counterBloc.state, equals(3)); /// ``` -void whenListen( - Cubit cubit, +void whenListen( + Bloc bloc, Stream stream, ) { final broadcastStream = stream.asBroadcastStream(); - StreamSubscription subscription; - when(cubit.isBroadcast).thenReturn(true); - when(cubit.skip(any)).thenAnswer( + StreamSubscription? subscription; + when(bloc).calls(#isBroadcast).thenReturn(true); + when(bloc).calls(#skip).thenAnswer( (invocation) { final stream = broadcastStream.skip( invocation.positionalArguments.first as int, ); subscription?.cancel(); subscription = stream.listen( - (state) => when(cubit.state).thenReturn(state), + (state) => when(bloc).calls(#state).thenReturn(state), onDone: () => subscription?.cancel(), ); return stream; }, ); - when(cubit.listen( - any, - onError: captureAnyNamed('onError'), - onDone: captureAnyNamed('onDone'), - cancelOnError: captureAnyNamed('cancelOnError'), - )).thenAnswer((invocation) { + when(bloc).calls(#listen).thenAnswer((invocation) { return broadcastStream.listen( (state) { - when(cubit.state).thenReturn(state); + when(bloc).calls(#state).thenReturn(state); (invocation.positionalArguments.first as Function(State)).call(state); }, - onError: invocation.namedArguments[const Symbol('onError')] as Function, - onDone: - invocation.namedArguments[const Symbol('onDone')] as void Function(), - cancelOnError: - invocation.namedArguments[const Symbol('cancelOnError')] as bool, + onError: invocation.namedArguments[#onError] as Function?, + onDone: invocation.namedArguments[#onDone] as void Function()?, + cancelOnError: invocation.namedArguments[#cancelOnError] as bool?, ); }); } diff --git a/packages/bloc_test/pubspec.yaml b/packages/bloc_test/pubspec.yaml index d02150d9d2f..035044dee9e 100644 --- a/packages/bloc_test/pubspec.yaml +++ b/packages/bloc_test/pubspec.yaml @@ -1,21 +1,25 @@ name: bloc_test description: A testing library which makes it easy to test blocs. Built to be used with the bloc state management package. -version: 7.1.0 +version: 8.0.0-nullsafety.0 repository: https://github.com/felangel/bloc/tree/master/packages/bloc_test issue_tracker: https://github.com/felangel/bloc/issues homepage: https://bloclibrary.dev documentation: https://bloclibrary.dev/#/gettingstarted environment: - sdk: ">=2.7.0 <3.0.0" + sdk: ">=2.12.0-0 <3.0.0" dependencies: - bloc: ^6.0.0 - test: ^1.14.2 - meta: ^1.1.8 - mockito: ^4.1.2 + bloc: ">=7.0.0-nullsafty.0 <7.0.0" + test: ">=1.16.0-nullsafety.13 <1.16.0" + meta: ">=1.3.0-nullsafety.6 <1.3.0" + mocktail: ">=0.0.1-dev.7 <0.1.0" + +dependency_overrides: + bloc: + path: ../bloc dev_dependencies: coverage: ^0.14.1 - pedantic: ^1.9.0 - rxdart: ^0.24.0 + pedantic: ">=1.10.0-nullsafety.3 < 1.10.0" + rxdart: ">=0.26.0-nullsafety.0 <0.26.0" diff --git a/packages/bloc_test/test/bloc_bloc_test_test.dart b/packages/bloc_test/test/bloc_bloc_test_test.dart index f5aba246397..1dae57bcc36 100644 --- a/packages/bloc_test/test/bloc_bloc_test_test.dart +++ b/packages/bloc_test/test/bloc_bloc_test_test.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:bloc_test/bloc_test.dart'; -import 'package:mockito/mockito.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:pedantic/pedantic.dart'; import 'package:test/test.dart'; @@ -77,17 +77,16 @@ void main() { Actual: [1] Which: at location [0] is <1> instead of <2> '''; - Object actualError; + late Object actualError; final completer = Completer(); - await runZoned(() async { - unawaited(runBlocTest( - 'fails immediately', + await runZonedGuarded(() async { + unawaited(testBloc( build: () => CounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: const [2], ).then((_) => completer.complete())); await completer.future; - }, onError: (Object error) { + }, (Object error, _) { actualError = error; completer.complete(); }); @@ -360,10 +359,11 @@ void main() { }); group('SideEffectCounterBloc', () { - Repository repository; + late Repository repository; setUp(() { repository = MockRepository(); + when(repository).calls(#sideEffect).thenReturn(null); }); blocTest( @@ -378,7 +378,7 @@ void main() { act: (bloc) => bloc.add(CounterEvent.increment), expect: const [1], verify: (_) { - verify(repository.sideEffect()).called(1); + verify(repository).called(#sideEffect).once(); }, ); @@ -397,7 +397,7 @@ void main() { build: () => SideEffectCounterBloc(repository), act: (bloc) => bloc.add(CounterEvent.increment), verify: (_) { - verify(repository.sideEffect()).called(1); + verify(repository).called(#sideEffect).times(1); }, ); @@ -407,32 +407,29 @@ void main() { act: (bloc) => bloc.add(CounterEvent.increment), verify: (_) async { await Future.delayed(Duration.zero); - verify(repository.sideEffect()).called(1); + verify(repository).called(#sideEffect).once(); }, ); test('fails immediately when verify is incorrect', () async { - const expectedError = '''Expected: <2> - Actual: <1> -Unexpected number of calls -'''; - Object actualError; + const expectedError = + '''Expected MockRepository.sideEffect() to be called <2> time(s) but actual call count was <1>.'''; + late Object actualError; final completer = Completer(); - await runZoned(() async { - unawaited(runBlocTest( - 'fails immediately', + await runZonedGuarded(() async { + unawaited(testBloc( build: () => SideEffectCounterBloc(repository), act: (bloc) => bloc.add(CounterEvent.increment), verify: (_) { - verify(repository.sideEffect()).called(2); + verify(repository).called(#sideEffect).times(2); }, ).then((_) => completer.complete())); await completer.future; - }, onError: (Object error) { + }, (Object error, _) { actualError = error; completer.complete(); }); - expect((actualError as TestFailure).message, expectedError); + expect((actualError as MocktailFailure).message, expectedError); }); test('shows equality warning when strings are identical', () async { @@ -441,17 +438,16 @@ Unexpected number of calls Which: at location [0] is instead of \n WARNING: Please ensure state instances extend Equatable, override == and hashCode, or implement Comparable. Alternatively, consider using Matchers in the expect of the blocTest rather than concrete state instances.\n'''; - Object actualError; + late Object actualError; final completer = Completer(); - await runZoned(() async { - unawaited(runBlocTest( - 'fails immediately', + await runZonedGuarded(() async { + unawaited(testBloc( build: () => ComplexBloc(), act: (bloc) => bloc.add(ComplexEventA()), expect: [ComplexStateA()], ).then((_) => completer.complete())); await completer.future; - }, onError: (Object error) { + }, (Object error, _) { actualError = error; completer.complete(); }); diff --git a/packages/bloc_test/test/blocs/exception_counter_bloc.dart b/packages/bloc_test/test/blocs/exception_counter_bloc.dart index ad88a6dbb06..f91988a187a 100644 --- a/packages/bloc_test/test/blocs/exception_counter_bloc.dart +++ b/packages/bloc_test/test/blocs/exception_counter_bloc.dart @@ -15,7 +15,6 @@ class ExceptionCounterBloc extends Bloc { case CounterEvent.increment: yield state + 1; throw ExceptionCounterBlocException(); - break; } } } diff --git a/packages/bloc_test/test/blocs/sum_bloc.dart b/packages/bloc_test/test/blocs/sum_bloc.dart index 320e8fc8c22..957d71d1c42 100644 --- a/packages/bloc_test/test/blocs/sum_bloc.dart +++ b/packages/bloc_test/test/blocs/sum_bloc.dart @@ -15,7 +15,7 @@ class SumBloc extends Bloc { _countSubscription = counterBloc.listen((count) => add(SumEvent(count))); } - StreamSubscription _countSubscription; + late StreamSubscription _countSubscription; @override Stream mapEventToState( diff --git a/packages/bloc_test/test/cubit_bloc_test_test.dart b/packages/bloc_test/test/cubit_bloc_test_test.dart index 6586bc1afb3..26b34a6f07e 100644 --- a/packages/bloc_test/test/cubit_bloc_test_test.dart +++ b/packages/bloc_test/test/cubit_bloc_test_test.dart @@ -1,5 +1,5 @@ import 'package:bloc_test/bloc_test.dart'; -import 'package:mockito/mockito.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; import 'cubits/cubits.dart'; @@ -158,10 +158,11 @@ void main() { }); group('SideEffectCounterCubit', () { - Repository repository; + late Repository repository; setUp(() { repository = MockRepository(); + when(repository).calls(#sideEffect).thenReturn(null); }); blocTest( @@ -176,7 +177,7 @@ void main() { act: (cubit) => cubit.increment(), expect: [1], verify: (_) async { - verify(repository.sideEffect()).called(1); + verify(repository).called(#sideEffect).once(); }, ); @@ -185,7 +186,7 @@ void main() { build: () => SideEffectCounterCubit(repository), act: (cubit) => cubit.increment(), verify: (_) async { - verify(repository.sideEffect()).called(1); + verify(repository).called(#sideEffect).once(); }, ); }); diff --git a/packages/bloc_test/test/cubits/sum_cubit.dart b/packages/bloc_test/test/cubits/sum_cubit.dart index 2cf0bcddea4..0a175761426 100644 --- a/packages/bloc_test/test/cubits/sum_cubit.dart +++ b/packages/bloc_test/test/cubits/sum_cubit.dart @@ -11,7 +11,7 @@ class SumCubit extends Cubit { ); } - StreamSubscription _countSubscription; + late StreamSubscription _countSubscription; @override Future close() { diff --git a/packages/bloc_test/test/emits_exactly_test.dart b/packages/bloc_test/test/emits_exactly_test.dart deleted file mode 100644 index 73de10a153d..00000000000 --- a/packages/bloc_test/test/emits_exactly_test.dart +++ /dev/null @@ -1,413 +0,0 @@ -// ignore_for_file: deprecated_member_use_from_same_package -import 'package:bloc_test/bloc_test.dart'; -import 'package:test/test.dart'; - -import 'blocs/blocs.dart'; - -void main() { - group('emitsExactly', () { - test('throws AssertionError if bloc is null', () async { - try { - await emitsExactly(null, const []); - } on dynamic catch (error) { - expect(error is AssertionError, true); - } - }); - - group('CounterBloc', () { - test('emits [1] when CounterEvent.increment is added', () async { - final bloc = CounterBloc()..add(CounterEvent.increment); - await emitsExactly(bloc, const [1]); - }); - - test('emits [2] when CounterEvent.increment twice is added and skip: 1', - () async { - final bloc = CounterBloc() - ..add(CounterEvent.increment) - ..add(CounterEvent.increment); - await emitsExactly(bloc, const [2], skip: 1); - }); - - test('fails if bloc does not emit all states', () async { - try { - await emitsExactly(CounterBloc(), const [1]); - fail('should throw'); - } on TestFailure catch (error) { - expect( - error.message, - 'Expected: [1]\n' - ' Actual: []\n' - ' Which: at location [0] is [] which shorter than expected\n' - ''); - } - }); - - test('fails if bloc does not emit correct states', () async { - try { - final bloc = CounterBloc()..add(CounterEvent.increment); - await emitsExactly(bloc, const [2]); - fail('should throw'); - } on TestFailure catch (error) { - expect( - error.message, - 'Expected: [2]\n' - ' Actual: [1]\n' - ' Which: at location [0] is <1> instead of <2>\n' - '', - ); - } - }); - - test('fails if expecting extra states', () async { - try { - final bloc = CounterBloc()..add(CounterEvent.increment); - await emitsExactly(bloc, const [1, 2]); - fail('should throw'); - } on TestFailure catch (error) { - expect( - error.message, - 'Expected: [1, 2]\n' - ' Actual: [1]\n' - ' Which: at location [1] is [1] which shorter than expected\n' - '', - ); - } - }); - }); - - group('AsyncCounterBloc', () { - test('emits [1] when CounterEvent.increment is added', () async { - final bloc = AsyncCounterBloc()..add(CounterEvent.increment); - await emitsExactly(bloc, const [1]); - }); - - test('emits [2] when CounterEvent.increment is added twice and skip: 1', - () async { - final bloc = AsyncCounterBloc() - ..add(CounterEvent.increment) - ..add(CounterEvent.increment); - await emitsExactly( - bloc, - const [2], - skip: 1, - ); - }); - - test('fails if bloc does not emit all states', () async { - try { - await emitsExactly( - AsyncCounterBloc(), - const [1], - ); - fail('should throw'); - } on TestFailure catch (error) { - expect( - error.message, - 'Expected: [1]\n' - ' Actual: []\n' - ' Which: at location [0] is [] which shorter than expected\n' - ''); - } - }); - - test('fails if bloc does not emit correct states', () async { - try { - final bloc = AsyncCounterBloc()..add(CounterEvent.increment); - await emitsExactly(bloc, const [2]); - fail('should throw'); - } on TestFailure catch (error) { - expect( - error.message, - 'Expected: [2]\n' - ' Actual: [1]\n' - ' Which: at location [0] is <1> instead of <2>\n' - '', - ); - } - }); - - test('fails if expecting extra states', () async { - try { - final bloc = AsyncCounterBloc()..add(CounterEvent.increment); - await emitsExactly(bloc, const [1, 2]); - fail('should throw'); - } on TestFailure catch (error) { - expect( - error.message, - 'Expected: [1, 2]\n' - ' Actual: [1]\n' - ' Which: at location [1] is [1] which shorter than expected\n' - '', - ); - } - }); - }); - - group('DebounceCounterBloc', () { - test('emits [1] when CounterEvent.increment is added', () async { - final bloc = DebounceCounterBloc()..add(CounterEvent.increment); - await emitsExactly(bloc, const [1], - duration: const Duration(milliseconds: 305)); - }); - - test('emits [2] when CounterEvent.increment is added twice and skip: 0', - () async { - final bloc = DebounceCounterBloc()..add(CounterEvent.increment); - await Future.delayed(const Duration(milliseconds: 305)); - bloc.add(CounterEvent.increment); - await emitsExactly( - bloc, - const [2], - duration: const Duration(milliseconds: 305), - skip: 0, - ); - }); - - test('fails if bloc does not emit all states', () async { - try { - await emitsExactly( - DebounceCounterBloc(), - const [1], - ); - fail('should throw'); - } on TestFailure catch (error) { - expect( - error.message, - 'Expected: [1]\n' - ' Actual: []\n' - ' Which: at location [0] is [] which shorter than expected\n' - ''); - } - }); - - test('fails if bloc does not emit correct states', () async { - try { - final bloc = DebounceCounterBloc()..add(CounterEvent.increment); - await emitsExactly( - bloc, - const [2], - duration: const Duration(milliseconds: 305), - ); - fail('should throw'); - } on TestFailure catch (error) { - expect( - error.message, - 'Expected: [2]\n' - ' Actual: [1]\n' - ' Which: at location [0] is <1> instead of <2>\n' - '', - ); - } - }); - - test('fails if expecting extra states', () async { - try { - final bloc = DebounceCounterBloc()..add(CounterEvent.increment); - await emitsExactly( - bloc, - const [1, 2], - duration: const Duration(milliseconds: 305), - ); - fail('should throw'); - } on TestFailure catch (error) { - expect( - error.message, - 'Expected: [1, 2]\n' - ' Actual: [1]\n' - ' Which: at location [1] is [1] which shorter than expected\n' - '', - ); - } - }); - }); - - group('InstantEmitBloc', () { - test('emits [1] when nothing is added', () async { - final bloc = InstantEmitBloc(); - await emitsExactly(bloc, const [1]); - }); - - test('emits [1, 2] when CounterEvent.increment is added', () async { - final bloc = InstantEmitBloc()..add(CounterEvent.increment); - await emitsExactly(bloc, const [1, 2]); - }); - - test('emits [2] when CounterEvent.increment is added and skip: 1', - () async { - final bloc = InstantEmitBloc()..add(CounterEvent.increment); - await emitsExactly( - bloc, - const [2], - skip: 1, - ); - }); - - test('fails if bloc does not emit all states', () async { - try { - await emitsExactly( - InstantEmitBloc(), - const [2], - ); - fail('should throw'); - } on TestFailure catch (error) { - expect( - error.message, - 'Expected: [2]\n' - ' Actual: [1]\n' - ' Which: at location [0] is <1> instead of <2>\n' - ''); - } - }); - - group('MultiCounterBloc', () { - test('emits [1, 2] when CounterEvent.increment is added', () async { - final bloc = MultiCounterBloc()..add(CounterEvent.increment); - await emitsExactly(bloc, const [1, 2]); - }); - - test('emits [2] when CounterEvent.increment is added and skip: 1', - () async { - final bloc = MultiCounterBloc()..add(CounterEvent.increment); - await emitsExactly( - bloc, - const [2], - skip: 1, - ); - }); - - test('fails if bloc does not emit all states', () async { - try { - await emitsExactly( - MultiCounterBloc(), - const [1], - ); - fail('should throw'); - } on TestFailure catch (error) { - expect( - error.message, - 'Expected: [1]\n' - ' Actual: []\n' - ' Which: at location [0] is [] which shorter than expected\n' - ''); - } - }); - - test('fails if bloc does not emit correct states', () async { - try { - final bloc = MultiCounterBloc()..add(CounterEvent.increment); - await emitsExactly(bloc, const [2]); - fail('should throw'); - } on TestFailure catch (error) { - expect( - error.message, - 'Expected: [2]\n' - ' Actual: [1, 2]\n' - ' Which: at location [0] is <1> instead of <2>\n' - '', - ); - } - }); - - test('fails if expecting extra states', () async { - try { - final bloc = MultiCounterBloc()..add(CounterEvent.increment); - await emitsExactly( - bloc, const [1, 2, 3]); - fail('should throw'); - } on TestFailure catch (error) { - expect( - error.message, - 'Expected: [1, 2, 3]\n' - ' Actual: [1, 2]\n' - // ignore: lines_longer_than_80_chars - ' Which: at location [2] is [1, 2] which shorter than expected\n' - '', - ); - } - }); - }); - - group('ComplexBloc', () { - test('emits [ComplexStateB] when ComplexEventB is added', () async { - final bloc = ComplexBloc()..add(ComplexEventB()); - await emitsExactly( - bloc, - [isA()], - ); - }); - - test( - 'emits [ComplexStateA] when [ComplexEventB, ComplexEventA] ' - 'is added and skip: 1', () async { - final bloc = ComplexBloc() - ..add(ComplexEventB()) - ..add(ComplexEventA()); - await emitsExactly( - bloc, - [isA()], - skip: 1, - ); - }); - - test('fails if bloc does not emit all states', () async { - try { - await emitsExactly( - ComplexBloc(), - [isA()], - ); - fail('should throw'); - } on TestFailure catch (error) { - expect( - error.message, - 'Expected: [<>]\n' - ' Actual: []\n' - ' Which: at location [0] is [] which shorter than expected\n' - '', - ); - } - }); - - test('fails if bloc does not emit correct states', () async { - try { - final bloc = ComplexBloc()..add(ComplexEventA()); - await emitsExactly( - bloc, - [isA()], - ); - fail('should throw'); - } on TestFailure catch (error) { - expect( - error.message, - 'Expected: [<>]\n' - ' Actual: [Instance of \'ComplexStateA\']\n' - // ignore: lines_longer_than_80_chars - ' Which: at location [0] is which is not an instance of \'ComplexStateB\'\n' - '', - ); - } - }); - - test('fails if expecting extra states', () async { - try { - final bloc = ComplexBloc()..add(ComplexEventB()); - await emitsExactly( - bloc, - [isA(), isA()], - ); - fail('should throw'); - } on TestFailure catch (error) { - expect( - error.message, - // ignore: lines_longer_than_80_chars - 'Expected: [<>, <>]\n' - ' Actual: [Instance of \'ComplexStateB\']\n' - // ignore: lines_longer_than_80_chars - ' Which: at location [1] is [Instance of \'ComplexStateB\'] which shorter than expected\n' - '', - ); - } - }); - }); - }); - }); -} diff --git a/packages/bloc_test/test/main.dart b/packages/bloc_test/test/main.dart deleted file mode 100644 index 8bfdbc9125d..00000000000 --- a/packages/bloc_test/test/main.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'bloc_bloc_test_test.dart' as bloc_bloc_test_test; -import 'cubit_bloc_test_test.dart' as cubit_bloc_test_test; -import 'emits_exactly_test.dart' as emits_exactly_test; -import 'mock_bloc_test.dart' as mock_bloc_test; -import 'when_listen_test.dart' as when_listen_test; - -void main() { - when_listen_test.main(); - mock_bloc_test.main(); - cubit_bloc_test_test.main(); - emits_exactly_test.main(); - bloc_bloc_test_test.main(); -} diff --git a/packages/bloc_test/test/mock_bloc_test.dart b/packages/bloc_test/test/mock_bloc_test.dart index dc6955d029f..d86a93eeb1f 100644 --- a/packages/bloc_test/test/mock_bloc_test.dart +++ b/packages/bloc_test/test/mock_bloc_test.dart @@ -1,36 +1,36 @@ import 'dart:async'; import 'package:bloc_test/bloc_test.dart'; -import 'package:mockito/mockito.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; import 'blocs/blocs.dart'; import 'cubits/cubits.dart'; -class MockCounterBloc extends MockBloc implements CounterBloc {} +class MockCounterBloc extends MockBloc + implements CounterBloc {} -class MockCounterCubit extends MockBloc implements CounterCubit {} +class MockCounterCubit extends MockCubit implements CounterCubit {} void main() { group('MockBloc', () { - CounterBloc counterBloc; + late CounterBloc counterBloc; setUp(() { counterBloc = MockCounterBloc(); }); - test('is compatible with skip', () { - expect(counterBloc.skip(1) is Stream, isTrue); - }); - test('is compatible with when', () { - when(counterBloc.state).thenReturn(10); + when(counterBloc).calls(#state).thenReturn(10); expect(counterBloc.state, 10); }); test('is automatically compatible with whenListen', () { - whenListen(counterBloc, Stream.fromIterable([0, 1, 2, 3])); + whenListen( + counterBloc, + Stream.fromIterable([0, 1, 2, 3]), + ); expectLater( counterBloc, emitsInOrder( @@ -39,4 +39,30 @@ void main() { ); }); }); + + group('MockCubit', () { + late CounterCubit counterCubit; + + setUp(() { + counterCubit = MockCounterCubit(); + }); + + test('is compatible with when', () { + when(counterCubit).calls(#state).thenReturn(10); + expect(counterCubit.state, 10); + }); + + test('is automatically compatible with whenListen', () { + whenListen( + counterCubit, + Stream.fromIterable([0, 1, 2, 3]), + ); + expectLater( + counterCubit, + emitsInOrder( + [equals(0), equals(1), equals(2), equals(3), emitsDone], + ), + ); + }); + }); } diff --git a/packages/bloc_test/test/when_listen_test.dart b/packages/bloc_test/test/when_listen_test.dart index 364b323724a..fca9c031f6a 100644 --- a/packages/bloc_test/test/when_listen_test.dart +++ b/packages/bloc_test/test/when_listen_test.dart @@ -6,19 +6,19 @@ import 'package:test/test.dart'; import 'cubits/cubits.dart'; -class MockCounterCubit extends MockBloc implements CounterCubit {} +class MockCounterCubit extends MockCubit implements CounterCubit {} void main() { group('whenListen', () { test('can mock the stream of a single cubit with an empty Stream', () { final counterCubit = MockCounterCubit(); - whenListen(counterCubit, const Stream.empty()); + whenListen(counterCubit, const Stream.empty()); expectLater(counterCubit, emitsInOrder([])); }); test('can mock the stream of a single cubit', () async { final counterCubit = MockCounterCubit(); - whenListen( + whenListen( counterCubit, Stream.fromIterable([0, 1, 2, 3]), ); @@ -33,7 +33,7 @@ void main() { test('can mock the stream of a single cubit with delays', () async { final counterCubit = MockCounterCubit(); final controller = StreamController(); - whenListen(counterCubit, controller.stream); + whenListen(counterCubit, controller.stream); unawaited(expectLater( counterCubit, emitsInOrder( @@ -54,7 +54,7 @@ void main() { () async { final counterCubit = MockCounterCubit(); final controller = StreamController(); - whenListen(counterCubit, controller.stream); + whenListen(counterCubit, controller.stream); unawaited(expectLater( counterCubit.skip(1), emitsInOrder( @@ -74,7 +74,7 @@ void main() { test('can mock the state of a single cubit with delays', () async { final counterCubit = MockCounterCubit(); final controller = StreamController(); - whenListen(counterCubit, controller.stream); + whenListen(counterCubit, controller.stream); unawaited(expectLater( counterCubit, emitsInOrder( @@ -97,7 +97,7 @@ void main() { () async { final counterCubit = MockCounterCubit(); final controller = StreamController(); - whenListen(counterCubit, controller.stream); + whenListen(counterCubit, controller.stream); unawaited(expectLater( counterCubit.skip(1), emitsInOrder( @@ -118,7 +118,7 @@ void main() { test('can mock the state of a single cubit', () async { final counterCubit = MockCounterCubit(); - whenListen( + whenListen( counterCubit, Stream.fromIterable([0, 1, 2, 3]), ); @@ -133,7 +133,7 @@ void main() { test('can mock the stream of a single cubit as broadcast stream', () { final counterCubit = MockCounterCubit(); - whenListen( + whenListen( counterCubit, Stream.fromIterable([0, 1, 2, 3]), ); @@ -154,7 +154,7 @@ void main() { test('can mock the stream of a single cubit as broadcast stream with skips', () { final counterCubit = MockCounterCubit(); - whenListen( + whenListen( counterCubit, Stream.fromIterable([0, 1, 2, 3]), ); @@ -174,7 +174,7 @@ void main() { test('can mock the stream of a single cubit with skip(1)', () { final counterCubit = MockCounterCubit(); - whenListen( + whenListen( counterCubit, Stream.fromIterable([0, 1, 2, 3]), ); @@ -188,7 +188,7 @@ void main() { test('can mock the state of a single cubit with skip(1)', () async { final counterCubit = MockCounterCubit(); - whenListen( + whenListen( counterCubit, Stream.fromIterable([0, 1, 2, 3]), ); @@ -204,7 +204,7 @@ void main() { test('can mock the state of a single cubit with skip(1).listen', () async { final counterCubit = MockCounterCubit(); final states = []; - whenListen( + whenListen( counterCubit, Stream.fromIterable([0, 1, 2, 3]), ); @@ -216,7 +216,7 @@ void main() { test('can mock the stream of a single cubit with skip(2)', () { final counterCubit = MockCounterCubit(); - whenListen( + whenListen( counterCubit, Stream.fromIterable([0, 1, 2, 3]), ); @@ -231,7 +231,7 @@ void main() { test('can mock the state of a single cubit with skip(2).listen', () async { final counterCubit = MockCounterCubit(); final states = []; - whenListen( + whenListen( counterCubit, Stream.fromIterable([0, 1, 2, 3]), ); @@ -243,7 +243,7 @@ void main() { test('can mock the state of a single cubit with skip(2)', () async { final counterCubit = MockCounterCubit(); - whenListen( + whenListen( counterCubit, Stream.fromIterable([0, 1, 2, 3]), ); @@ -259,7 +259,7 @@ void main() { test('can mock the state of a single cubit with skip(3).listen', () async { final counterCubit = MockCounterCubit(); final states = []; - whenListen( + whenListen( counterCubit, Stream.fromIterable([0, 1, 2, 3]), ); @@ -271,7 +271,7 @@ void main() { test('can mock the stream of a single cubit with skip(3)', () { final counterCubit = MockCounterCubit(); - whenListen( + whenListen( counterCubit, Stream.fromIterable([0, 1, 2, 3]), ); @@ -285,7 +285,7 @@ void main() { test('can mock the state of a single cubit with skip(3)', () async { final counterCubit = MockCounterCubit(); - whenListen( + whenListen( counterCubit, Stream.fromIterable([0, 1, 2, 3]), ); @@ -303,7 +303,7 @@ void main() { '(with initial state)', () async { final controller = StreamController(); final counterCubit = MockCounterCubit(); - whenListen(counterCubit, controller.stream); + whenListen(counterCubit, controller.stream); final sumCubit = SumCubit(counterCubit); unawaited(expectLater(sumCubit, emitsInOrder([0, 1, 3, 6]))); controller..add(0)..add(1)..add(2)..add(3); @@ -314,7 +314,7 @@ void main() { test('can mock the stream of a cubit dependency', () async { final controller = StreamController(); final counterCubit = MockCounterCubit(); - whenListen(counterCubit, controller.stream); + whenListen(counterCubit, controller.stream); final sumCubit = SumCubit(counterCubit); unawaited(expectLater(sumCubit, emitsInOrder([1, 3, 6]))); controller..add(1)..add(2)..add(3); diff --git a/packages/flutter_bloc/example/lib/main.dart b/packages/flutter_bloc/example/lib/main.dart index d94a793a0ef..8c50bb7f67c 100644 --- a/packages/flutter_bloc/example/lib/main.dart +++ b/packages/flutter_bloc/example/lib/main.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; - import 'package:flutter_bloc/flutter_bloc.dart'; /// Custom [BlocObserver] which observes all bloc and cubit instances. @@ -12,12 +11,6 @@ class SimpleBlocObserver extends BlocObserver { super.onEvent(bloc, event); } - @override - void onChange(Cubit cubit, Change change) { - print(change); - super.onChange(cubit, change); - } - @override void onTransition(Bloc bloc, Transition transition) { print(transition); @@ -25,9 +18,9 @@ class SimpleBlocObserver extends BlocObserver { } @override - void onError(Cubit cubit, Object error, StackTrace stackTrace) { + void onError(Bloc bloc, Object error, StackTrace stackTrace) { print(error); - super.onError(cubit, error, stackTrace); + super.onError(bloc, error, stackTrace); } } diff --git a/packages/flutter_bloc/example/pubspec.yaml b/packages/flutter_bloc/example/pubspec.yaml index 677644e4e78..aeadcfdf25c 100644 --- a/packages/flutter_bloc/example/pubspec.yaml +++ b/packages/flutter_bloc/example/pubspec.yaml @@ -12,6 +12,10 @@ dependencies: flutter_bloc: path: ../ +dependency_overrides: + bloc: + path: ../../bloc + dev_dependencies: effective_dart: ^1.2.0 diff --git a/packages/flutter_bloc/lib/src/bloc_builder.dart b/packages/flutter_bloc/lib/src/bloc_builder.dart index 980a9560cc2..d4473d69258 100644 --- a/packages/flutter_bloc/lib/src/bloc_builder.dart +++ b/packages/flutter_bloc/lib/src/bloc_builder.dart @@ -1,10 +1,8 @@ import 'package:bloc/bloc.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; -import 'bloc_listener.dart'; -import 'bloc_provider.dart'; - /// Signature for the `builder` function which takes the `BuildContext` and /// [state] and is responsible for returning a widget which is to be rendered. /// This is analogous to the `builder` function in [StreamBuilder]. @@ -18,13 +16,13 @@ typedef BlocBuilderCondition = bool Function(S previous, S current); /// {@template bloc_builder} /// [BlocBuilder] handles building a widget in response to new `states`. /// [BlocBuilder] is analogous to [StreamBuilder] but has simplified API to -/// reduce the amount of boilerplate code needed as well as bloc-specific -/// optimizations. +/// reduce the amount of boilerplate code needed as well as [bloc]-specific +/// performance improvements. /// Please refer to `BlocListener` if you want to "do" anything in response to /// `state` changes such as navigation, showing a dialog, etc... /// -/// If the [value] parameter is omitted, [BlocBuilder] will automatically +/// If the [bloc] parameter is omitted, [BlocBuilder] will automatically /// perform a lookup using [BlocProvider] and the current `BuildContext`. /// /// ```dart @@ -35,12 +33,12 @@ typedef BlocBuilderCondition = bool Function(S previous, S current); /// ) /// ``` /// -/// Only specify the [value] if you wish to provide a bloc/cubit that is otherwise +/// Only specify the [bloc] if you wish to provide a [bloc] that is otherwise /// not accessible via [BlocProvider] and the current `BuildContext`. /// /// ```dart /// BlocBuilder( -/// value: blocA, +/// bloc: blocA, /// builder: (context, state) { /// // return widget here based on BlocA's state /// } @@ -53,11 +51,11 @@ typedef BlocBuilderCondition = bool Function(S previous, S current); /// how often [BlocBuilder] rebuilds. /// [buildWhen] should only be used for performance optimizations as it /// provides no security about the state passed to the [builder] function. -/// [buildWhen] will be invoked on each `state` change. +/// [buildWhen] will be invoked on each [bloc] `state` change. /// [buildWhen] takes the previous `state` and current `state` and must /// return a [bool] which determines whether or not the [builder] function will /// be invoked. -/// The previous `state` will be initialized to the `state` of the bloc/cubit when +/// The previous `state` will be initialized to the `state` of the [bloc] when /// the [BlocBuilder] is initialized. /// [buildWhen] is optional and if omitted, it will default to `true`. /// @@ -73,14 +71,14 @@ typedef BlocBuilderCondition = bool Function(S previous, S current); ///) /// ``` /// {@endtemplate} -class BlocBuilder, S> extends BlocBuilderBase { +class BlocBuilder, S> extends BlocBuilderBase { /// {@macro bloc_builder} const BlocBuilder({ Key? key, required this.builder, - T? value, + B? bloc, BlocBuilderCondition? buildWhen, - }) : super(key: key, value: value, buildWhen: buildWhen); + }) : super(key: key, bloc: bloc, buildWhen: buildWhen); /// The [builder] function which will be invoked on each widget build. /// The [builder] takes the `BuildContext` and current `state` and @@ -94,21 +92,22 @@ class BlocBuilder, S> extends BlocBuilderBase { /// {@template bloc_builder_base} /// Base class for widgets that build themselves based on interaction with -/// a specified bloc/cubit. +/// a specified [bloc]. /// /// A [BlocBuilderBase] is stateful and maintains the state of the interaction /// so far. The type of the state and how it is updated with each interaction /// is defined by sub-classes. /// {@endtemplate} -abstract class BlocBuilderBase, S> extends StatefulWidget { +abstract class BlocBuilderBase, S> + extends StatefulWidget { /// {@macro bloc_builder_base} - const BlocBuilderBase({Key? key, this.value, this.buildWhen}) + const BlocBuilderBase({Key? key, this.bloc, this.buildWhen}) : super(key: key); - /// The bloc/cubit that the [BlocBuilderBase] will interact with. + /// The [bloc] that the [BlocBuilderBase] will interact with. /// If omitted, [BlocBuilderBase] will automatically perform a lookup using /// [BlocProvider] and the current `BuildContext`. - final T? value; + final B? bloc; /// {@macro bloc_builder_build_when} final BlocBuilderCondition? buildWhen; @@ -117,26 +116,26 @@ abstract class BlocBuilderBase, S> extends StatefulWidget { Widget build(BuildContext context, S state); @override - State> createState() => _BlocBuilderBaseState(); + State> createState() => _BlocBuilderBaseState(); } -class _BlocBuilderBaseState, S> - extends State> { - late T _bloc; +class _BlocBuilderBaseState, S> + extends State> { + late B _bloc; late S _state; @override void initState() { super.initState(); - _bloc = widget.value ?? context.read(); + _bloc = widget.bloc ?? context.read(); _state = _bloc.state; } @override - void didUpdateWidget(BlocBuilderBase oldWidget) { + void didUpdateWidget(BlocBuilderBase oldWidget) { super.didUpdateWidget(oldWidget); - final oldBloc = oldWidget.value ?? context.read(); - final currentBloc = widget.value ?? oldBloc; + final oldBloc = oldWidget.bloc ?? context.read(); + final currentBloc = widget.bloc ?? oldBloc; if (oldBloc != currentBloc) { _bloc = currentBloc; _state = _bloc.state; @@ -145,8 +144,8 @@ class _BlocBuilderBaseState, S> @override Widget build(BuildContext context) { - return BlocListener( - value: _bloc, + return BlocListener( + bloc: _bloc, listenWhen: widget.buildWhen, listener: (context, state) => setState(() => _state = state), child: widget.build(context, _state), diff --git a/packages/flutter_bloc/lib/src/bloc_consumer.dart b/packages/flutter_bloc/lib/src/bloc_consumer.dart index 6fc6304115e..54f9cf37e24 100644 --- a/packages/flutter_bloc/lib/src/bloc_consumer.dart +++ b/packages/flutter_bloc/lib/src/bloc_consumer.dart @@ -1,23 +1,21 @@ import 'package:bloc/bloc.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; -import 'bloc_builder.dart'; -import 'bloc_listener.dart'; - /// {@template bloc_consumer} /// [BlocConsumer] exposes a [builder] and [listener] in order react to new /// states. /// [BlocConsumer] is analogous to a nested `BlocListener` /// and `BlocBuilder` but reduces the amount of boilerplate needed. /// [BlocConsumer] should only be used when it is necessary to both rebuild UI -/// and execute other reactions to state changes in a bloc/cubit. +/// and execute other reactions to state changes in the [bloc]. /// /// [BlocConsumer] takes a required `BlocWidgetBuilder` -/// and `BlocWidgetListener` and an optional [value], +/// and `BlocWidgetListener` and an optional [bloc], /// `BlocBuilderCondition`, and `BlocListenerCondition`. /// -/// If the [value] parameter is omitted, [BlocConsumer] will automatically +/// If the [bloc] parameter is omitted, [BlocConsumer] will automatically /// perform a lookup using `BlocProvider` and the current `BuildContext`. /// /// ```dart @@ -33,12 +31,12 @@ import 'bloc_listener.dart'; /// /// An optional [listenWhen] and [buildWhen] can be implemented for more /// granular control over when [listener] and [builder] are called. -/// The [listenWhen] and [buildWhen] will be invoked on each `state` +/// The [listenWhen] and [buildWhen] will be invoked on each [bloc] `state` /// change. /// They each take the previous `state` and current `state` and must return /// a [bool] which determines whether or not the [builder] and/or [listener] /// function will be invoked. -/// The previous `state` will be initialized to the `state` of the bloc/cubit when +/// The previous `state` will be initialized to the `state` of the [bloc] when /// the [BlocConsumer] is initialized. /// [listenWhen] and [buildWhen] are optional and if they aren't implemented, /// they will default to `true`. @@ -62,21 +60,21 @@ import 'bloc_listener.dart'; /// ) /// ``` /// {@endtemplate} -class BlocConsumer, S> extends StatefulWidget { +class BlocConsumer, S> extends StatefulWidget { /// {@macro bloc_consumer} const BlocConsumer({ Key? key, required this.builder, required this.listener, - this.value, + this.bloc, this.buildWhen, this.listenWhen, }) : super(key: key); - /// The bloc/cubit that the [BlocConsumer] will interact with. + /// The [bloc] that the [BlocConsumer] will interact with. /// If omitted, [BlocConsumer] will automatically perform a lookup using /// `BlocProvider` and the current `BuildContext`. - final T? value; + final B? bloc; /// The [builder] function which will be invoked on each widget build. /// The [builder] takes the `BuildContext` and current `state` and @@ -84,7 +82,7 @@ class BlocConsumer, S> extends StatefulWidget { /// This is analogous to the [builder] function in [StreamBuilder]. final BlocWidgetBuilder builder; - /// Takes the `BuildContext` along with the current `state` + /// Takes the `BuildContext` along with the [bloc] `state` /// and is responsible for executing in response to `state` changes. final BlocWidgetListener listener; @@ -99,24 +97,24 @@ class BlocConsumer, S> extends StatefulWidget { final BlocListenerCondition? listenWhen; @override - State> createState() => _BlocConsumerState(); + State> createState() => _BlocConsumerState(); } -class _BlocConsumerState, S> - extends State> { - late T _bloc; +class _BlocConsumerState, S> + extends State> { + late B _bloc; @override void initState() { super.initState(); - _bloc = widget.value ?? context.read(); + _bloc = widget.bloc ?? context.read(); } @override - void didUpdateWidget(BlocConsumer oldWidget) { + void didUpdateWidget(BlocConsumer oldWidget) { super.didUpdateWidget(oldWidget); - final oldBloc = oldWidget.value ?? context.read(); - final currentBloc = widget.value ?? oldBloc; + final oldBloc = oldWidget.bloc ?? context.read(); + final currentBloc = widget.bloc ?? oldBloc; if (oldBloc != currentBloc) { _bloc = currentBloc; } @@ -124,8 +122,8 @@ class _BlocConsumerState, S> @override Widget build(BuildContext context) { - return BlocBuilder( - value: _bloc, + return BlocBuilder( + bloc: _bloc, builder: widget.builder, buildWhen: (previous, current) { if (widget.listenWhen?.call(previous, current) ?? true) { diff --git a/packages/flutter_bloc/lib/src/bloc_listener.dart b/packages/flutter_bloc/lib/src/bloc_listener.dart index 422e1244536..60c4674badc 100644 --- a/packages/flutter_bloc/lib/src/bloc_listener.dart +++ b/packages/flutter_bloc/lib/src/bloc_listener.dart @@ -2,11 +2,10 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; import 'package:provider/single_child_widget.dart'; -import 'bloc_provider.dart'; - /// Mixin which allows `MultiBlocListener` to infer the types /// of multiple [BlocListener]s. mixin BlocListenerSingleChildWidget on SingleChildWidget {} @@ -23,15 +22,15 @@ typedef BlocWidgetListener = void Function(BuildContext context, S state); typedef BlocListenerCondition = bool Function(S previous, S current); /// {@template bloc_listener} -/// Takes a [BlocWidgetListener] and an optional [value] and invokes -/// the [listener] in response to `state` changes in the [value]. +/// Takes a [BlocWidgetListener] and an optional [bloc] and invokes +/// the [listener] in response to `state` changes in the [bloc]. /// It should be used for functionality that needs to occur only in response to /// a `state` change such as navigation, showing a `SnackBar`, showing /// a `Dialog`, etc... /// The [listener] is guaranteed to only be called once for each `state` change /// unlike the `builder` in `BlocBuilder`. /// -/// If the [value] parameter is omitted, [BlocListener] will automatically +/// If the [bloc] parameter is omitted, [BlocListener] will automatically /// perform a lookup using [BlocProvider] and the current `BuildContext`. /// /// ```dart @@ -42,7 +41,7 @@ typedef BlocListenerCondition = bool Function(S previous, S current); /// child: Container(), /// ) /// ``` -/// Only specify the [value] if you wish to provide a bloc/cubit that is otherwise +/// Only specify the [bloc] if you wish to provide a [bloc] that is otherwise /// not accessible via [BlocProvider] and the current `BuildContext`. /// /// ```dart @@ -59,11 +58,11 @@ typedef BlocListenerCondition = bool Function(S previous, S current); /// {@template bloc_listener_listen_when} /// An optional [listenWhen] can be implemented for more granular control /// over when [listener] is called. -/// [listenWhen] will be invoked on each `state` change. +/// [listenWhen] will be invoked on each [bloc] `state` change. /// [listenWhen] takes the previous `state` and current `state` and must /// return a [bool] which determines whether or not the [listener] function /// will be invoked. -/// The previous `state` will be initialized to the `state` of the bloc/cubit +/// The previous `state` will be initialized to the `state` of the [bloc] /// when the [BlocListener] is initialized. /// [listenWhen] is optional and if omitted, it will default to `true`. /// @@ -80,38 +79,38 @@ typedef BlocListenerCondition = bool Function(S previous, S current); /// ) /// ``` /// {@endtemplate} -class BlocListener, S> extends BlocListenerBase +class BlocListener, S> extends BlocListenerBase with BlocListenerSingleChildWidget { /// {@macro bloc_listener} const BlocListener({ Key? key, required BlocWidgetListener listener, - T? value, + B? bloc, BlocListenerCondition? listenWhen, Widget? child, }) : super( key: key, child: child, listener: listener, - value: value, + bloc: bloc, listenWhen: listenWhen, ); } /// {@template bloc_listener_base} -/// Base class for widgets that listen to state changes in a specified bloc/cubit. +/// Base class for widgets that listen to state changes in a specified [bloc]. /// /// A [BlocListenerBase] is stateful and maintains the state subscription. /// The type of the state and what happens with each state change /// is defined by sub-classes. /// {@endtemplate} -abstract class BlocListenerBase, S> +abstract class BlocListenerBase, S> extends SingleChildStatefulWidget { /// {@macro bloc_listener_base} const BlocListenerBase({ Key? key, required this.listener, - this.value, + this.bloc, this.child, this.listenWhen, }) : super(key: key, child: child); @@ -120,9 +119,9 @@ abstract class BlocListenerBase, S> /// [BlocListenerBase]. final Widget? child; - /// The bloc/cubit which will be listened to. - /// Whenever the `state` changes, [listener] will be invoked. - final T? value; + /// The [bloc] whose `state` will be listened to. + /// Whenever the [bloc]'s `state` changes, [listener] will be invoked. + final B? bloc; /// The [BlocWidgetListener] which will be called on every `state` change. /// This [listener] should be used for any code which needs to execute @@ -133,29 +132,29 @@ abstract class BlocListenerBase, S> final BlocListenerCondition? listenWhen; @override - SingleChildState> createState() => - _BlocListenerBaseState(); + SingleChildState> createState() => + _BlocListenerBaseState(); } -class _BlocListenerBaseState, S> - extends SingleChildState> { +class _BlocListenerBaseState, S> + extends SingleChildState> { StreamSubscription? _subscription; + late B _bloc; late S _previousState; - late T _bloc; @override void initState() { super.initState(); - _bloc = widget.value ?? context.read(); + _bloc = widget.bloc ?? context.read(); _previousState = _bloc.state; _subscribe(); } @override - void didUpdateWidget(BlocListenerBase oldWidget) { + void didUpdateWidget(BlocListenerBase oldWidget) { super.didUpdateWidget(oldWidget); - final oldBloc = oldWidget.value ?? context.read(); - final currentBloc = widget.value ?? oldBloc; + final oldBloc = oldWidget.bloc ?? context.read(); + final currentBloc = widget.bloc ?? oldBloc; if (oldBloc != currentBloc) { if (_subscription != null) { _unsubscribe(); diff --git a/packages/flutter_bloc/lib/src/bloc_provider.dart b/packages/flutter_bloc/lib/src/bloc_provider.dart index 3b758bc056a..4471dcee739 100644 --- a/packages/flutter_bloc/lib/src/bloc_provider.dart +++ b/packages/flutter_bloc/lib/src/bloc_provider.dart @@ -1,6 +1,5 @@ -import 'package:flutter/widgets.dart'; - import 'package:bloc/bloc.dart'; +import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; import 'package:provider/single_child_widget.dart'; @@ -35,8 +34,8 @@ mixin BlocProviderSingleChildWidget on SingleChildWidget {} /// ``` /// /// {@endtemplate} -class BlocProvider> extends SingleChildStatelessWidget - with BlocProviderSingleChildWidget { +class BlocProvider> + extends SingleChildStatelessWidget with BlocProviderSingleChildWidget { /// {@macro bloc_provider} BlocProvider({ Key? key, @@ -110,7 +109,7 @@ class BlocProvider> extends SingleChildStatelessWidget /// ```dart /// BlocProvider.of(context); /// ``` - static T of>( + static T of>( BuildContext context, { bool listen = false, }) { @@ -120,7 +119,7 @@ class BlocProvider> extends SingleChildStatelessWidget if (e.valueType != T) rethrow; throw FlutterError( ''' - BlocProvider.of() called with a context that does not contain a Bloc/Cubit of type $T. + BlocProvider.of() called with a context that does not contain a Bloc of type $T. No ancestor could be found starting from the context that was passed to BlocProvider.of<$T>(). This can happen if the context you used comes from a widget above the BlocProvider. @@ -143,8 +142,8 @@ class BlocProvider> extends SingleChildStatelessWidget } static VoidCallback _startListening( - InheritedContext e, - Cubit value, + InheritedContext e, + Bloc value, ) { final subscription = value.listen( (dynamic _) => e.markNeedsNotifyDependents(), diff --git a/packages/flutter_bloc/lib/src/multi_bloc_listener.dart b/packages/flutter_bloc/lib/src/multi_bloc_listener.dart index af66a806589..694ebb28269 100644 --- a/packages/flutter_bloc/lib/src/multi_bloc_listener.dart +++ b/packages/flutter_bloc/lib/src/multi_bloc_listener.dart @@ -1,8 +1,7 @@ import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/src/bloc_listener.dart'; import 'package:provider/provider.dart'; -import 'bloc_listener.dart'; - /// {@template multi_bloc_listener} /// Merges multiple [BlocListener] widgets into one widget tree. /// diff --git a/packages/flutter_bloc/lib/src/multi_bloc_provider.dart b/packages/flutter_bloc/lib/src/multi_bloc_provider.dart index e8df106c605..694faf91015 100644 --- a/packages/flutter_bloc/lib/src/multi_bloc_provider.dart +++ b/packages/flutter_bloc/lib/src/multi_bloc_provider.dart @@ -1,8 +1,7 @@ import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/src/bloc_provider.dart'; import 'package:provider/provider.dart'; -import 'bloc_provider.dart'; - /// {@template multi_bloc_provider} /// Merges multiple [BlocProvider] widgets into one widget tree. /// diff --git a/packages/flutter_bloc/lib/src/multi_repository_provider.dart b/packages/flutter_bloc/lib/src/multi_repository_provider.dart index 59583f75bf7..737174f07f4 100644 --- a/packages/flutter_bloc/lib/src/multi_repository_provider.dart +++ b/packages/flutter_bloc/lib/src/multi_repository_provider.dart @@ -1,8 +1,7 @@ import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/src/repository_provider.dart'; import 'package:provider/provider.dart'; -import 'repository_provider.dart'; - /// {@template multi_repository_provider} /// Merges multiple [RepositoryProvider] widgets into one widget tree. /// diff --git a/packages/flutter_bloc/pubspec.yaml b/packages/flutter_bloc/pubspec.yaml index 1d52a3d6d99..88af26ecea7 100644 --- a/packages/flutter_bloc/pubspec.yaml +++ b/packages/flutter_bloc/pubspec.yaml @@ -12,8 +12,12 @@ environment: dependencies: flutter: sdk: flutter - bloc: ^7.0.0-nullsafety.0 - provider: ^5.0.0-nullsafety.2 + bloc: ">=7.0.0-nullsafety.0 <7.0.0" + provider: ">=5.0.0-nullsafety.2 <5.0.0" + +dependency_overrides: + bloc: + path: ../bloc dev_dependencies: flutter_test: diff --git a/packages/flutter_bloc/test/bloc_builder_test.dart b/packages/flutter_bloc/test/bloc_builder_test.dart index e6e594d045f..481623ec1a6 100644 --- a/packages/flutter_bloc/test/bloc_builder_test.dart +++ b/packages/flutter_bloc/test/bloc_builder_test.dart @@ -35,7 +35,7 @@ class MyThemeAppState extends State { @override Widget build(BuildContext context) { return BlocBuilder, ThemeData>( - value: _themeCubit, + bloc: _themeCubit, builder: ((context, theme) { _onBuild(); return MaterialApp( @@ -93,7 +93,7 @@ class MyCounterAppState extends State { body: Column( children: [ BlocBuilder( - value: _cubit, + bloc: _cubit, buildWhen: (previousState, state) { return (previousState + state) % 3 == 0; }, @@ -105,7 +105,7 @@ class MyCounterAppState extends State { }, ), BlocBuilder( - value: _cubit, + bloc: _cubit, builder: (context, count) { return Text( '$count', @@ -405,7 +405,7 @@ void main() { final counterCubit = CounterCubit(); await tester.pumpWidget( BlocBuilder( - value: counterCubit, + bloc: counterCubit, buildWhen: (previous, state) { if (state % 2 == 0) { buildWhenPreviousState.add(previous); @@ -440,7 +440,7 @@ void main() { textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (context, setState) => BlocBuilder( - value: counterCubit, + bloc: counterCubit, buildWhen: (previous, state) => state % 2 == 0, builder: (_, state) { states.add(state); diff --git a/packages/flutter_bloc/test/bloc_consumer_test.dart b/packages/flutter_bloc/test/bloc_consumer_test.dart index d07f93443b1..9349517a96c 100644 --- a/packages/flutter_bloc/test/bloc_consumer_test.dart +++ b/packages/flutter_bloc/test/bloc_consumer_test.dart @@ -21,7 +21,7 @@ void main() { MaterialApp( home: Scaffold( body: BlocConsumer( - value: counterCubit, + bloc: counterCubit, builder: (context, state) { return Text('State: $state'); }, @@ -45,7 +45,7 @@ void main() { MaterialApp( home: Scaffold( body: BlocConsumer( - value: counterCubit, + bloc: counterCubit, builder: (context, state) { return Text('State: $state'); }, @@ -75,7 +75,7 @@ void main() { child: MaterialApp( home: Scaffold( body: BlocConsumer( - value: counterCubit, + bloc: counterCubit, builder: (context, state) { return Text('State: $state'); }, @@ -100,7 +100,7 @@ void main() { MaterialApp( home: Scaffold( body: BlocConsumer( - value: counterCubit, + bloc: counterCubit, builder: (context, state) { return Text('State: $state'); }, @@ -128,7 +128,7 @@ void main() { MaterialApp( home: Scaffold( body: BlocConsumer( - value: counterCubit, + bloc: counterCubit, buildWhen: (previous, current) => (previous + current) % 3 == 0, builder: (context, state) { builderStates.add(state); @@ -216,7 +216,7 @@ void main() { body: StatefulBuilder( builder: (context, setState) { return BlocConsumer( - value: counterCubit, + bloc: counterCubit, builder: (context, state) { builderStates.add(state); return TextButton( @@ -263,7 +263,7 @@ void main() { MaterialApp( home: Scaffold( body: BlocConsumer( - value: counterCubit, + bloc: counterCubit, builder: (context, state) { builderStates.add(state); return Text('State: $state'); @@ -307,7 +307,7 @@ void main() { final counterCubit = CounterCubit(); await tester.pumpWidget( BlocConsumer( - value: counterCubit, + bloc: counterCubit, listenWhen: (previous, current) { if (current % 3 == 0) { listenWhenPreviousState.add(previous); diff --git a/packages/flutter_bloc/test/bloc_listener_test.dart b/packages/flutter_bloc/test/bloc_listener_test.dart index aeefdb3bea5..62644016f88 100644 --- a/packages/flutter_bloc/test/bloc_listener_test.dart +++ b/packages/flutter_bloc/test/bloc_listener_test.dart @@ -38,7 +38,7 @@ class _MyAppState extends State { return MaterialApp( home: Scaffold( body: BlocListener( - value: _counterCubit, + bloc: _counterCubit, listener: (context, state) { widget.onListenerCalled?.call(context, state); }, @@ -74,7 +74,7 @@ void main() { const targetKey = Key('cubit_listener_container'); await tester.pumpWidget( BlocListener( - value: CounterCubit(), + bloc: CounterCubit(), listener: (_, __) {}, child: const SizedBox(key: targetKey), ), @@ -88,7 +88,7 @@ void main() { const expectedStates = [1]; await tester.pumpWidget( BlocListener( - value: counterCubit, + bloc: counterCubit, listener: (_, state) { states.add(state); }, @@ -106,7 +106,7 @@ void main() { const expectedStates = [1, 2]; await tester.pumpWidget( BlocListener( - value: counterCubit, + bloc: counterCubit, listener: (_, state) { states.add(state); }, @@ -202,7 +202,7 @@ void main() { const expectedStates = [1]; await tester.pumpWidget( BlocListener( - value: counterCubit, + bloc: counterCubit, listenWhen: (previous, state) { listenWhenCallCount++; latestPreviousState = previous; @@ -231,7 +231,7 @@ void main() { const expectedStates = [2]; await tester.pumpWidget( BlocListener( - value: counterCubit, + bloc: counterCubit, listenWhen: (previous, state) { listenWhenCallCount++; if ((previous + state) % 3 == 0) { @@ -261,7 +261,7 @@ void main() { final counterCubit = CounterCubit(); await tester.pumpWidget( BlocListener( - value: counterCubit, + bloc: counterCubit, listenWhen: (previous, current) { if (current % 3 == 0) { listenWhenPreviousState.add(previous); @@ -323,7 +323,7 @@ void main() { const expectedStates = [1, 2]; await tester.pumpWidget( BlocListener( - value: counterCubit, + bloc: counterCubit, listenWhen: (previous, state) { listenWhenCallCount++; latestPreviousState = previous; @@ -353,7 +353,7 @@ void main() { const expectedStates = []; await tester.pumpWidget( BlocListener( - value: counterCubit, + bloc: counterCubit, listenWhen: (_, __) => false, listener: (_, state) => states.add(state), child: const SizedBox(), @@ -373,7 +373,7 @@ void main() { const expectedStates = [1]; await tester.pumpWidget( BlocListener( - value: counterCubit, + bloc: counterCubit, listenWhen: (_, __) => true, listener: (_, state) => states.add(state), child: const SizedBox(), @@ -393,7 +393,7 @@ void main() { const expectedStates = []; await tester.pumpWidget( BlocListener( - value: counterCubit, + bloc: counterCubit, listenWhen: (_, __) => false, listener: (_, state) => states.add(state), child: const SizedBox(), @@ -419,7 +419,7 @@ void main() { const expectedStates = [1, 2, 3, 4]; await tester.pumpWidget( BlocListener( - value: counterCubit, + bloc: counterCubit, listenWhen: (_, __) => true, listener: (_, state) => states.add(state), child: const SizedBox(), diff --git a/packages/flutter_bloc/test/bloc_provider_test.dart b/packages/flutter_bloc/test/bloc_provider_test.dart index 007c810261b..6bc9cf9f467 100644 --- a/packages/flutter_bloc/test/bloc_provider_test.dart +++ b/packages/flutter_bloc/test/bloc_provider_test.dart @@ -130,7 +130,7 @@ class CounterPage extends StatelessWidget { return Scaffold( body: BlocBuilder( - value: counterCubit, + bloc: counterCubit, builder: (context, count) { onBuild?.call(); return Text('$count', key: const Key('counter_text')); @@ -371,7 +371,7 @@ void main() { await tester.pumpWidget(const MyAppNoProvider(home: CounterPage())); final dynamic exception = tester.takeException(); final expectedMessage = ''' - BlocProvider.of() called with a context that does not contain a Bloc/Cubit of type CounterCubit. + BlocProvider.of() called with a context that does not contain a Bloc of type CounterCubit. No ancestor could be found starting from the context that was passed to BlocProvider.of(). This can happen if the context you used comes from a widget above the BlocProvider. diff --git a/packages/flutter_bloc/test/multi_bloc_listener_test.dart b/packages/flutter_bloc/test/multi_bloc_listener_test.dart index 9fafb9f84cb..3a3a87a56ac 100644 --- a/packages/flutter_bloc/test/multi_bloc_listener_test.dart +++ b/packages/flutter_bloc/test/multi_bloc_listener_test.dart @@ -24,11 +24,11 @@ void main() { MultiBlocListener( listeners: [ BlocListener( - value: counterCubitA, + bloc: counterCubitA, listener: (context, state) => statesA.add(state), ), BlocListener( - value: counterCubitB, + bloc: counterCubitB, listener: (context, state) => statesB.add(state), ), ], @@ -64,11 +64,11 @@ void main() { MultiBlocListener( listeners: [ BlocListener( - value: counterCubitA, + bloc: counterCubitA, listener: (BuildContext context, int state) => statesA.add(state), ), BlocListener( - value: counterCubitB, + bloc: counterCubitB, listener: (BuildContext context, int state) => statesB.add(state), ), ], diff --git a/packages/flutter_bloc/test/multi_bloc_provider_test.dart b/packages/flutter_bloc/test/multi_bloc_provider_test.dart index 5ad791d7f7b..276799c95a6 100644 --- a/packages/flutter_bloc/test/multi_bloc_provider_test.dart +++ b/packages/flutter_bloc/test/multi_bloc_provider_test.dart @@ -93,7 +93,7 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder( - value: BlocProvider.of(context), + bloc: BlocProvider.of(context), builder: (_, theme) { return MaterialApp(home: CounterPage(), theme: theme); }, @@ -108,7 +108,7 @@ class CounterPage extends StatelessWidget { return Scaffold( body: BlocBuilder( - value: counterCubit, + bloc: counterCubit, builder: (context, count) { return Center( child: Text('$count', key: const Key('counter_text')),