Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

ReplayBloc function onTransition() handles event _Undo instead of user defined event #4028

Open
simphotonics opened this issue Dec 15, 2023 · 5 comments
Assignees
Labels
bug Something isn't working question Further information is requested waiting for response Waiting for follow up

Comments

@simphotonics
Copy link

Description
While trying out the class ReplayBloc I encountered a problem when using the method onTransition. Instead of handling the user defined event Rewind, an internal event _Undo is handled. (The handler of Rewind calls undo(), see function _rewind() below.)

Steps To Reproduce

import 'package:replay_bloc/replay_bloc.dart';
import 'package:equatable/equatable.dart';

/// Bloc state
final class CounterState extends Equatable {
  const CounterState(this.i);
  final int i;

  @override
  List<Object> get props => [i];
}

final class CounterInitial extends CounterState {
  const CounterInitial() : super(0);
}

/// Bloc events
sealed class CounterEvent extends ReplayEvent {}

final class Increment extends CounterEvent {}

final class Decrement extends CounterEvent {}

final class Rewind extends CounterEvent{}



/// Bloc logic
class CounterBloc extends ReplayBloc<CounterEvent, CounterState> {
  CounterBloc() : super(CounterInitial()) {
    on<CounterEvent>((event, emit) => switch (event) {
          Increment() => emit(CounterState(state.i + 1)),
          Decrement() => emit(CounterState(state.i - 1)),
          Rewind() => emit(_rewind()),
        });
  }

  CounterState _rewind() {
    undo();
    return state;
  }

  @override
  void onTransition(
      covariant Transition<ReplayEvent, CounterState> transition) {
    super.onTransition(transition);
    print(transition.event.runtimeType);
    
    if (transition.event is Rewind) {
      print('  Doing something during event Rewind. '); // <-------- Never reached.
    }
  }
}

/// Executable
void main(List<String> args) {
  final bloc = CounterBloc();

  bloc.add(Increment());
  bloc.add(Decrement());
  bloc.add(Rewind());
}

Expected Behavior

$ dart main.dart
Increment
Decrement
Rewind
  Doing something during event Rewind.

Actual Behavior

$ dart main.dart
Increment
Decrement
Undo

Additional Context
The fact that _Undo is private makes it more difficult to filter events in onTransition.

@simphotonics simphotonics added the bug Something isn't working label Dec 15, 2023
@felangel
Copy link
Owner

Hi @simphotonics 👋
Thanks for opening an issue!

Can you elaborate on why you’re creating a separate rewind event when you can just call undo on the bloc directly from the widget tree?

@felangel felangel added question Further information is requested waiting for response Waiting for follow up labels Dec 15, 2023
@felangel felangel self-assigned this Dec 15, 2023
@simphotonics
Copy link
Author

simphotonics commented Dec 15, 2023

Hi @felangel. Thanks for taking the time to have a look.

In the concrete example I am working on the events are AwardPointTeam1, AwardPointTeam2, and ReplayLastPoint:

part of 'score_bloc.dart';

@immutable
sealed class ScoreEvent extends ReplayEvent {
  const ScoreEvent();
  @override
  String toString() => '$runtimeType';
}

@immutable
sealed class AwardPoint extends ScoreEvent {
  const AwardPoint();
}

@immutable
class AwardPointTeam1 extends AwardPoint {
  const AwardPointTeam1();
}

@immutable
class AwardPointTeam2 extends ScoreEvent {
  const AwardPointTeam2();
}

@immutable
class ReplayLastPoint extends ScoreEvent {
  const ReplayLastPoint();
}

and the bloc state represents a tennis score:

part of 'score_bloc.dart';

@immutable
final class ScoreState extends Equatable {
  const ScoreState({
    required this.server,
    this.points = const (0, 0),
    this.pointsWon = const (0, 0),
    this.games = const [(0, 0)],
    this.sets = const (0, 0),
    this.winner,
  });

  final (int, int) points;
  final (int, int) pointsWon;
  final List<(int, int)> games;
  final (int, int) sets;
  final TeamSelector server;
  final TeamSelector? winner;

  bool get hasWinner => winner == null ? false : true;

  @override
  List<Object?> get props => [points, pointsWon, games, sets, winner];
}

During a ReplayLastPoint event some logic is required to change the scoring method if necessary.

For example at games: 6:0, 6:7, 0:0, and points 0:0 the scoring method is that of a standard tennis game in the third set.
After the event ReplayLastPoint at score: 6:0, 6:6, and tiebreak points e.g. 8:9, the scoring method has to be changed to that of a set tiebreaker. I placed this logic into onTransition since it has access to the current and the next state.

@override
  void onTransition(covariant Transition<ReplayEvent, ScoreState> transition) {
    super.onTransition(transition);

    if (transition.event is AwardPoint) {
      return;
    }

    // Set the correct set configuration after an undo.
    if (transition.nextState.sets != decidingSetAtScore &&
        transition.currentState.sets == decidingSetAtScore) {
      _setConfig = matchConfig.setConfig.standard;
    }
    // Set the correct game configuration after an undo.
    _gameConfig =
        (transition.nextState.games.last == _setConfig.tiebreakAtScore)
            ? _setConfig.gameConfig.tiebreak
            : _setConfig.gameConfig.standard;
  }

@simphotonics
Copy link
Author

Just checking to see if any further details are required.

@felangel
Copy link
Owner

It'd be super helpful if you could share a link to a GitHub repo or DartPad which we can run locally, thanks! 🙏

@simphotonics
Copy link
Author

simphotonics commented Jan 13, 2025

Just created the repository: replay_bloc_demo as requested.

Doing that, I think I have found out what is going on. While handling the event ReplayPoint, I am calling undo() in order to access the previous state. This implicitly adds the event _Undo and since I am then emitting the same state, the event ReplayPoint is skipped:

import 'package:replay_bloc/replay_bloc.dart';

import 'score_event.dart';
import 'score_state.dart';

/// Bloc logic
class ScoreBloc extends ReplayBloc<MatchEvent, ScoreState> {
  ScoreBloc() : super(ScoreInitial()) {
    on<MatchEvent>((event, emit) => switch (event) {
          AwardPointTeam1() => emit(
              ScoreState(state.scoreTeam1 + _pointValue, state.scoreTeam2),
            ),
          AwardPointTeam2() => emit(
              ScoreState(state.scoreTeam1, state.scoreTeam2 + _pointValue),
            ),
          ReplayPoint() => emit(_replayPoint()),
        });
  }

  int _pointValue = 1;

  ScoreState _replayPoint() {
    undo();
    return state;
    // return ScoreState(state.scoreTeam1, 99);
  }

  @override
  void onTransition(covariant Transition<ReplayEvent, ScoreState> transition) {
    super.onTransition(transition);

    if (transition.event is ReplayPoint) { // Can't use _Undo here since the class is private). 
      if ((transition.nextState.scoreTeam1 - transition.nextState.scoreTeam2)
              .abs() >
          1) {
        _pointValue = 2;
      }
    }

    print('$transition _pointValue: $_pointValue');
  }
}

Running main.dart show the following transitions:

$ dart bin/main.dart 
Transition { currentState: score: (0, 0), event: Instance of 'AwardPointTeam1', nextState: score: (1, 0) } _pointValue: 1
Transition { currentState: score: (1, 0), event: Instance of 'AwardPointTeam2', nextState: score: (1, 1) } _pointValue: 1
Transition { currentState: score: (1, 1), event: Instance of 'AwardPointTeam2', nextState: score: (1, 2) } _pointValue: 1
Transition { currentState: score: (1, 2), event: Instance of 'AwardPointTeam1', nextState: score: (2, 2) } _pointValue: 1
Transition { currentState: score: (2, 2), event: Instance of 'AwardPointTeam2', nextState: score: (2, 3) } _pointValue: 1
Transition { currentState: score: (2, 3), event: Instance of 'AwardPointTeam2', nextState: score: (2, 4) } _pointValue: 1
Transition { currentState: score: (2, 4), event: Instance of 'AwardPointTeam2', nextState: score: (2, 5) } _pointValue: 1
Transition { currentState: score: (2, 5), event: Instance of 'AwardPointTeam2', nextState: score: (2, 6) } _pointValue: 1
Transition { currentState: score: (2, 6), event: Instance of 'AwardPointTeam2', nextState: score: (2, 7) } _pointValue: 1
Transition { currentState: score: (2, 7), event: Instance of 'AwardPointTeam1', nextState: score: (3, 7) } _pointValue: 1
Transition { currentState: score: (3, 7), event: Undo, nextState: score: (2, 7) } _pointValue: 1
Transition { currentState: score: (2, 7), event: Undo, nextState: score: (2, 6) } _pointValue: 1
Transition { currentState: score: (2, 6), event: Undo, nextState: score: (2, 5) } _pointValue: 1
Transition { currentState: score: (2, 5), event: Undo, nextState: score: (2, 4) } _pointValue: 1
Transition { currentState: score: (2, 4), event: Undo, nextState: score: (2, 3) } _pointValue: 1
Transition { currentState: score: (2, 3), event: Undo, nextState: score: (2, 2) } _pointValue: 1
Transition { currentState: score: (2, 2), event: Undo, nextState: score: (1, 2) } _pointValue: 1
Transition { currentState: score: (1, 2), event: Undo, nextState: score: (1, 1) } _pointValue: 1
Transition { currentState: score: (1, 1), event: Undo, nextState: score: (1, 0) } _pointValue: 1
Transition { currentState: score: (1, 0), event: Undo, nextState: score: (0, 0) } _pointValue: 1
Transition { currentState: score: (0, 0), event: Instance of 'ReplayPoint', nextState: score: (0, 0) } _pointValue: 1

What would be the best way to handle a situation when I need to set a bloc property (in this case: _pointValue) that depends on the previous state and on the fact that a certain event, like ReplayPoint, is emitted?

If I define ScoreState without EquatableMixin, then the transition with ReplayPoint is shown but undo() does not work as expected:

$ dart bin/main.dart 
Transition { currentState: score: (0, 0), event: Instance of 'AwardPointTeam1', nextState: score: (1, 0) } _pointValue: 1
Transition { currentState: score: (1, 0), event: Instance of 'AwardPointTeam2', nextState: score: (1, 1) } _pointValue: 1
Transition { currentState: score: (1, 1), event: Instance of 'AwardPointTeam2', nextState: score: (1, 2) } _pointValue: 1
Transition { currentState: score: (1, 2), event: Instance of 'AwardPointTeam2', nextState: score: (1, 3) } _pointValue: 1
Transition { currentState: score: (1, 3), event: Instance of 'AwardPointTeam2', nextState: score: (1, 4) } _pointValue: 1
Transition { currentState: score: (1, 4), event: Instance of 'AwardPointTeam1', nextState: score: (2, 4) } _pointValue: 1
Transition { currentState: score: (2, 4), event: Instance of 'AwardPointTeam2', nextState: score: (2, 5) } _pointValue: 1
Transition { currentState: score: (2, 5), event: Instance of 'AwardPointTeam1', nextState: score: (3, 5) } _pointValue: 1
Transition { currentState: score: (3, 5), event: Instance of 'AwardPointTeam2', nextState: score: (3, 6) } _pointValue: 1
Transition { currentState: score: (3, 6), event: Instance of 'AwardPointTeam2', nextState: score: (3, 7) } _pointValue: 1
Transition { currentState: score: (3, 7), event: Undo, nextState: score: (3, 6) } _pointValue: 1
Transition { currentState: score: (3, 6), event: Instance of 'ReplayPoint', nextState: score: (3, 6) } _pointValue: 2
Transition { currentState: score: (3, 6), event: Undo, nextState: score: (3, 6) } _pointValue: 2
Transition { currentState: score: (3, 6), event: Instance of 'ReplayPoint', nextState: score: (3, 6) } _pointValue: 2
Transition { currentState: score: (3, 6), event: Undo, nextState: score: (3, 6) } _pointValue: 2
Transition { currentState: score: (3, 6), event: Instance of 'ReplayPoint', nextState: score: (3, 6) } _pointValue: 2
Transition { currentState: score: (3, 6), event: Undo, nextState: score: (3, 6) } _pointValue: 2
Transition { currentState: score: (3, 6), event: Instance of 'ReplayPoint', nextState: score: (3, 6) } _pointValue: 2
Transition { currentState: score: (3, 6), event: Undo, nextState: score: (3, 6) } _pointValue: 2
Transition { currentState: score: (3, 6), event: Instance of 'ReplayPoint', nextState: score: (3, 6) } _pointValue: 2
Transition { currentState: score: (3, 6), event: Undo, nextState: score: (3, 6) } _pointValue: 2
Transition { currentState: score: (3, 6), event: Instance of 'ReplayPoint', nextState: score: (3, 6) } _pointValue: 2
Transition { currentState: score: (3, 6), event: Undo, nextState: score: (3, 6) } _pointValue: 2
Transition { currentState: score: (3, 6), event: Instance of 'ReplayPoint', nextState: score: (3, 6) } _pointValue: 2
Transition { currentState: score: (3, 6), event: Undo, nextState: score: (3, 6) } _pointValue: 2
Transition { currentState: score: (3, 6), event: Instance of 'ReplayPoint', nextState: score: (3, 6) } _pointValue: 2
Transition { currentState: score: (3, 6), event: Undo, nextState: score: (3, 6) } _pointValue: 2
Transition { currentState: score: (3, 6), event: Instance of 'ReplayPoint', nextState: score: (3, 6) } _pointValue: 2
Transition { currentState: score: (3, 6), event: Undo, nextState: score: (3, 6) } _pointValue: 2
Transition { currentState: score: (3, 6), event: Instance of 'ReplayPoint', nextState: score: (3, 6) } _pointValue: 2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working question Further information is requested waiting for response Waiting for follow up
Projects
None yet
Development

No branches or pull requests

2 participants