Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions packages/bloc/lib/src/bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,15 @@ abstract class Bloc<Event, State> extends BlocBase<State>
/// Updates the state of the bloc to the provided [state].
/// A bloc's state should only be updated by `emitting` a new `state`
/// from an [EventHandler] in response to an incoming event.
///
/// Set [force] to `true` to emit the [state] even if it is equal to the
/// current [state].
/// {@endtemplate}
@visibleForTesting
@override
void emit(State state) => super.emit(state);
void emit(State state, {bool force = false}) {
super.emit(state, force: force);
}

/// Register event handler for an event of type `E`.
/// There should only ever be one event handler per event type `E`.
Expand Down Expand Up @@ -195,17 +200,17 @@ abstract class Bloc<Event, State> extends BlocBase<State>
final subscription = (transformer ?? _eventTransformer)(
_eventController.stream.where((event) => event is E).cast<E>(),
(dynamic event) {
void onEmit(State state) {
void onEmit(State state, {bool force = false}) {
if (super.isClosed) return;
if (this.state == state && _emitted) return;
if (!force && this.state == state && _emitted) return;
onTransition(
Transition(
currentState: this.state,
event: event as E,
nextState: state,
),
);
emit(state);
emit(state, force: force);
}

final emitter = _Emitter(onEmit);
Expand Down
17 changes: 14 additions & 3 deletions packages/bloc/lib/src/bloc_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ abstract class Closable {
// ignore: one_member_abstracts
abstract class Emittable<State extends Object?> {
/// Emits a new [state].
void emit(State state);
///
/// If [force] is `true`, the [state] is emitted even if it is equal
/// to the current state.
void emit(State state, {bool force = false});
}

/// A generic destination for errors.
Expand Down Expand Up @@ -90,16 +93,24 @@ abstract class BlocBase<State>
/// emitting a state which is equal to the initial state is allowed as long
/// as it is the first thing emitted by the instance.
///
/// Set [force] to `true` to emit the [state] even if it is equal to the
/// current [state]. This will notify listeners and trigger [onChange]
/// just like any other state change.
///
/// ```dart
/// emit(state, force: true);
/// ```
///
/// * Throws a [StateError] if the bloc is closed.
@protected
@visibleForTesting
@override
void emit(State state) {
void emit(State state, {bool force = false}) {
try {
if (_stateController.isClosed) {
throw StateError('Cannot emit new states after calling close');
}
if (state == _state && _emitted) return;
if (!force && state == _state && _emitted) return;
onChange(Change<State>(currentState: this.state, nextState: state));
_state = state;
_stateController.add(_state);
Expand Down
17 changes: 13 additions & 4 deletions packages/bloc/lib/src/emitter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,22 @@ abstract class Emitter<State> {
bool get isDone;

/// Emits the provided [state].
void call(State state);
///
/// Set [force] to `true` to emit the [state] even if it is equal to the
/// current state.
///
/// ```dart
/// on<MyEvent>((event, emit) {
/// emit(state, force: true);
/// });
/// ```
void call(State state, {bool force = false});
}

class _Emitter<State> implements Emitter<State> {
_Emitter(this._emit);

final void Function(State state) _emit;
final void Function(State state, {bool force}) _emit;
final _completer = Completer<void>();
final _disposables = <FutureOr<void> Function()>[];

Expand Down Expand Up @@ -109,7 +118,7 @@ class _Emitter<State> implements Emitter<State> {
}

@override
void call(State state) {
void call(State state, {bool force = false}) {
assert(
!_isCompleted,
'''
Expand All @@ -132,7 +141,7 @@ ensure the event handler has not completed.
});
''',
);
if (!_isCanceled) _emit(state);
if (!_isCanceled) _emit(state, force: force);
}

@override
Expand Down
57 changes: 57 additions & 0 deletions packages/bloc/test/bloc_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,63 @@ void main() {
..add('event')
..close();
});

test('emits repeated states when force is true', () {
final seededBloc = SeededBloc(
seed: 0,
states: [1, 2, 1, 1],
force: true,
);
final expectedStates = [1, 2, 1, 1, emitsDone];

expectLater(seededBloc.stream, emitsInOrder(expectedStates));

seededBloc
..add('event')
..close();
});

test('emits the seed state when force is true', () {
final seededBloc = SeededBloc(seed: 0, states: [0, 0], force: true);
final expectedStates = [0, 0, emitsDone];

expectLater(seededBloc.stream, emitsInOrder(expectedStates));

seededBloc
..add('event')
..close();
});

test('emits duplicate states across events when force is true', () {
final seededBloc = SeededBloc(seed: 0, states: [1], force: true);
final expectedStates = [1, 1, 1, emitsDone];

expectLater(seededBloc.stream, emitsInOrder(expectedStates));

seededBloc
..add('eventA')
..add('eventB')
..add('eventC')
..close();
});

test('notifies onTransition for each forced emit', () async {
final transitions = <Transition<String, int>>[];
final seededBloc = SeededBloc(
seed: 0,
states: [1, 1],
force: true,
onTransitionCallback: transitions.add,
)..add('event');

await tick();
await seededBloc.close();

expect(transitions, const [
Transition(currentState: 0, event: 'event', nextState: 1),
Transition(currentState: 1, event: 'event', nextState: 1),
]);
});
});

group('StreamBloc', () {
Expand Down
19 changes: 17 additions & 2 deletions packages/bloc/test/blocs/seeded/seeded_bloc.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
import 'package:bloc/bloc.dart';

class SeededBloc extends Bloc<String, int> {
SeededBloc({required this.seed, required this.states}) : super(seed) {
SeededBloc({
required this.seed,
required this.states,
this.force = false,
this.onTransitionCallback,
}) : super(seed) {
on<String>((event, emit) {
states.forEach(emit.call);
for (final state in states) {
emit(state, force: force);
}
});
}

final List<int> states;
final int seed;
final bool force;
final void Function(Transition<String, int> transition)? onTransitionCallback;

@override
void onTransition(Transition<String, int> transition) {
super.onTransition(transition);
onTransitionCallback?.call(transition);
}
}
70 changes: 70 additions & 0 deletions packages/bloc/test/cubit_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,20 @@ void main() {
),
).called(1);
});

test('is called for each forced emit of an identical state', () async {
final cubit = SeededCubit(initialState: 0)
..emitStateForced(1)
..emitStateForced(1);
await cubit.close();
verify(
// ignore: invalid_use_of_protected_member
() => observer.onChange(
cubit,
const Change<int>(currentState: 1, nextState: 1),
),
).called(1);
});
});

group('emit', () {
Expand Down Expand Up @@ -216,6 +230,62 @@ void main() {
await subscription.cancel();
expect(states, [1, 2, 3]);
});

test('emits duplicate states when force is true', () async {
final states = <int>[];
final cubit = SeededCubit(initialState: 0);
final subscription = cubit.stream.listen(states.add);
cubit
..emitStateForced(1)
..emitStateForced(1)
..emitStateForced(2)
..emitStateForced(2);
await cubit.close();
await subscription.cancel();
expect(states, [1, 1, 2, 2]);
});

test('emits the current state when force is true', () async {
final states = <int>[];
final cubit = SeededCubit(initialState: 0);
final subscription = cubit.stream.listen(states.add);
cubit
..emitStateForced(0)
..emitStateForced(0);
await cubit.close();
await subscription.cancel();
expect(states, [0, 0]);
expect(cubit.state, equals(0));
});

test('force does not affect subsequent emits', () async {
final states = <int>[];
final cubit = SeededCubit(initialState: 0);
final subscription = cubit.stream.listen(states.add);
cubit
..emitStateForced(1)
..emitState(1)
..emitState(2);
await cubit.close();
await subscription.cancel();
expect(states, [1, 2]);
});

test('throws StateError when force is true and cubit is closed',
() async {
final cubit = SeededCubit(initialState: 0);
await cubit.close();
expect(
() => cubit.emitStateForced(0),
throwsA(
isA<StateError>().having(
(e) => e.message,
'message',
'Cannot emit new states after calling close',
),
),
);
});
});

group('listen', () {
Expand Down
2 changes: 2 additions & 0 deletions packages/bloc/test/cubits/seeded_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ class SeededCubit<T> extends Cubit<T> {
SeededCubit({required T initialState}) : super(initialState);

void emitState(T state) => emit(state);

void emitStateForced(T state) => emit(state, force: true);
}
8 changes: 4 additions & 4 deletions packages/replay_bloc/lib/src/replay_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ mixin ReplayBlocMixin<Event extends ReplayEvent, State> on Bloc<Event, State> {
}

@override
void emit(State state) {
void emit(State state, {bool force = false}) {
_changeStack.add(
_Change<State>(
this.state,
Expand All @@ -110,7 +110,7 @@ mixin ReplayBlocMixin<Event extends ReplayEvent, State> on Bloc<Event, State> {
),
);
// ignore: invalid_use_of_visible_for_testing_member
super.emit(state);
super.emit(state, force: force);
},
(val) {
final event = _Undo();
Expand All @@ -123,12 +123,12 @@ mixin ReplayBlocMixin<Event extends ReplayEvent, State> on Bloc<Event, State> {
),
);
// ignore: invalid_use_of_visible_for_testing_member
super.emit(val);
super.emit(val, force: force);
},
),
);
// ignore: invalid_use_of_visible_for_testing_member
super.emit(state);
super.emit(state, force: force);
}

/// Undo the last change.
Expand Down
8 changes: 4 additions & 4 deletions packages/replay_bloc/lib/src/replay_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,16 @@ mixin ReplayCubitMixin<State> on Cubit<State> {
set limit(int limit) => _changeStack.limit = limit;

@override
void emit(State state) {
void emit(State state, {bool force = false}) {
_changeStack.add(
_Change<State>(
this.state,
state,
() => super.emit(state),
(val) => super.emit(val),
() => super.emit(state, force: force),
(val) => super.emit(val, force: force),
),
);
super.emit(state);
super.emit(state, force: force);
}

/// Undo the last change.
Expand Down