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

State synchronization problem in the bloc and widget #844

Closed
konstantin-doncov opened this issue Feb 4, 2020 · 17 comments
Closed

State synchronization problem in the bloc and widget #844

konstantin-doncov opened this issue Feb 4, 2020 · 17 comments
Assignees
Labels
question Further information is requested

Comments

@konstantin-doncov
Copy link

I'm trying to implement these swipeable cards(Gesture detector and Alignment type) with bloc. Now each card has only one number for testing.
Each card deck has 10 cards and when deck has less than three cards, I emulate loading more new cards from server in the game_bloc.dart. When I just emulate it without delay - all fine, but when I use await Future.delayed() for emulating server delay there is a problem: I swipe top card(e.g. with number 7) before new pagination, during animation middle card(number 8) going to the top position with correct number, but when animation is finished I get new top card with the number 7 instead of number 8.
So, I think the problem in the bloc state synchronization in the bloc class and page class, because I didn't get this problem without delay, get this problem with delay 100ms, and again didn't get this with delay 1000ms.
I’ve been trying to fix this for a long time, so your help is very necessary, any advice on how to do it correctly will be very valuable!
P.S. Most of the code is related to animation, so don't be scared of a lot of it :)
Regards.

game_state.dart:

abstract class GameState extends Equatable {
  bool isNeedToAnimate;
  int deletingCard;

  final List<int> userGameViewDeck;

  GameState(this.isNeedToAnimate, this.deletingCard, this.userGameViewDeck);

  @override
  List<Object> get props => [isNeedToAnimate, deletingCard, userGameViewDeck];
}

class GameDeckLoadedState extends GameState {
  GameDeckLoadedState(bool isNeedToAnimate, int deletingCard, List<int> userGameViewDeck) : super(isNeedToAnimate, deletingCard, userGameViewDeck);
}

game_bloc.dart:

 @override
  Stream<GameState> mapEventToState(
    GameEvent event,
  ) async* {
    if(event is DownloadGameDeckEvent){
      yield GameDeckLoadingState();

      final result = List.generate(10, (i) => i);

      yield GameDecklLoadedState(false, null, result);
    }
    else if(event is UploadGameResponseByPairIdEvent){

      int deletingCard = state.userGameViewDeck.removeAt(0);
      yield GameDeckLoadedState(true, deletingCard, state.userGameViewDeck);


      if(state.userGameViewDeck.length < 3){

        final result = List.generate(10, (i) => i + state.userGameViewDeck.last + 1);

        result.insertAll(0, state.userGameViewDeck);
        await Future.delayed(Duration(milliseconds: 100)); //HERE IS THE PROBLEM

        yield GameDeckLoadedState(state.isNeedToAnimate, state.deletingCard, result);
      }

    }
  }

game_page.dart:

List<Alignment> cardsAlign = [ new Alignment(0.0, 1.0), new Alignment(0.0, 0.8), new Alignment(0.0, 0.0) ];
List<Size> cardsSize = new List(3);

class GamePage extends StatefulWidget
{

  @override
  _GamePageState createState() => new _GamePageState();
}

class _GamePageState extends State<GamePage> with SingleTickerProviderStateMixin
{

  GameBloc _gameBloc;

  AnimationController _controller;

  final Alignment defaultFrontCardAlign = new Alignment(0.0, 0.0);
  Alignment frontCardAlign;
  double frontCardRot = 0.0;

  @override
  Widget build(BuildContext context) {

    cardsSize[0] = new Size(MediaQuery.of(context).size.width * 0.9, MediaQuery.of(context).size.height * 0.85);
    cardsSize[1] = new Size(MediaQuery.of(context).size.width * 0.85, MediaQuery.of(context).size.height * 0.80);
    cardsSize[2] = new Size(MediaQuery.of(context).size.width * 0.8, MediaQuery.of(context).size.height * 0.75);

    return Scaffold(
        appBar: AppBar(
          title: Text('Cards'),
        ),
        body: Column(
          children: <Widget>[
            buildBody(),
          ],
        )
    );
  }


  @override
  void initState()
  {
    super.initState();

    frontCardAlign = cardsAlign[2];

    // Init the animation controller
    _controller = new AnimationController(duration: new Duration(milliseconds: 700), vsync: this);
    _controller.addListener(() => setState(() {}));

    _gameBloc = BlocProvider.of<GameBloc>(context);
    _gameBloc.add(DownloadGameDeckEvent());
  }


  Widget buildBody() {
    return BlocBuilder<GameBloc, GameState>(

        builder: (context, state,) {
        if(state is GameDeckLoadedState){

            if(state.isNeedToAnimate) {
              state.isNeedToAnimate = false;

              WidgetsBinding.instance.addPostFrameCallback((_) {
                animateSwipeCards(state);
              });
            }

            return buildGame(state);
          }
          else {
            return Container();
          }
        }
    );
  }

  Widget buildGame(GameState state)
  {

    List<int> visibleCards = List();

    int i = 0;

    if(state.deletingCard != null)
      visibleCards.add(state.deletingCard);
    else if(state.userGameViewDeck.length >= 1)
      visibleCards.add(state.userGameViewDeck[i++]);

    if(state.userGameViewDeck.length >= 2)
      visibleCards.add(state.userGameViewDeck[i++]);
    if(state.userGameViewDeck.length >= 3)
      visibleCards.add(state.userGameViewDeck[i++]);



    return new Expanded
      (
        child: new Stack
          (
          children: <Widget>
          [
            if(visibleCards.length >= 3)
              backCard(visibleCards[2]),
            if(visibleCards.length >= 2)
              middleCard(visibleCards[1]),
            if(visibleCards.length >= 1)
              frontCard(visibleCards[0]),
            // Prevent swiping if the cards are animating
            _controller.status != AnimationStatus.forward ? new SizedBox.expand
              (
                child: new GestureDetector
                  (
                  // While dragging the first card
                  onPanUpdate: (DragUpdateDetails details)
                  {
                    // Add what the user swiped in the last frame to the alignment of the card
                    setState(()
                    {
                      // 20 is the "speed" at which moves the card
                      frontCardAlign = new Alignment
                        (
                          frontCardAlign.x + 20 * details.delta.dx / MediaQuery.of(context).size.width,
                          0
                      );

                      frontCardRot = frontCardAlign.x*1.5; // * rotation speed;
                    });
                  },
                  // When releasing the first card
                  onPanEnd: (_)
                  {
                    // If the front card was swiped far enough to count as swiped
                    if(frontCardAlign.x > 5.0)
                    {
                      _gameBloc.add(GameResponseEvent());
                    }
                    else if(frontCardAlign.x < -5.0){
                      _gameBloc.add(GameResponseEvent());
                    }
                    else
                    {
                      // Return to the initial rotation and alignment
                      setState(()
                      {
                        frontCardAlign = defaultFrontCardAlign;
                        frontCardRot = 0.0;
                      });
                    }
                  },
                )
            ) : new Container(),
          ],
        )
    );
  }

  void animateSwipeCards(GameDeckLoadedState state)
  {

    _controller.stop();
    _controller.value = 0.0;
    _controller.forward();

    void updateViewsAfterAnimation(AnimationStatus status){
      if(status == AnimationStatus.completed) {
        state.deletingCard = null;

        frontCardAlign = defaultFrontCardAlign;
        frontCardRot = 0.0;

        _controller.removeStatusListener(updateViewsAfterAnimation);
      }
    }

    _controller.addStatusListener(updateViewsAfterAnimation);
  }

  Widget backCard(int userGameView)
  {
    return new Align
      (
      alignment: _controller.status == AnimationStatus.forward ? CardsAnimation.backCardAlignmentAnim(_controller).value : cardsAlign[0],
      child: new SizedBox.fromSize
        (
          size: _controller.status == AnimationStatus.forward ? CardsAnimation.backCardSizeAnim(_controller).value : cardsSize[2],
          child: ProfileInGameWidget(userGameView: userGameView)
      ),
    );
  }

  Widget middleCard(int userGameView)
  {
    return new Align
      (
      alignment: _controller.status == AnimationStatus.forward ? CardsAnimation.middleCardAlignmentAnim(_controller).value : cardsAlign[1],
      child: new SizedBox.fromSize
        (
          size: _controller.status == AnimationStatus.forward ? CardsAnimation.middleCardSizeAnim(_controller).value : cardsSize[1],
          child: ProfileInGameWidget(userGameView: userGameView)
      ),
    );
  }

  Widget frontCard(int userGameView)
  {
    return new Align
      (
        alignment: _controller.status == AnimationStatus.forward ? CardsAnimation.frontCardDisappearAlignmentAnim(_controller, frontCardAlign).value : frontCardAlign,
        child: new Transform.rotate
          (
          angle: (pi / 180.0) * frontCardRot,
          child: new SizedBox.fromSize
            (
              size: cardsSize[0],
              child: ProfileInGameWidget(userGameView: userGameView)
          ),
        )
    );
  }
}

class CardsAnimation
{
  static Animation<Alignment> backCardAlignmentAnim(AnimationController parent)
  {
    return new AlignmentTween
      (
        begin: cardsAlign[0],
        end: cardsAlign[1]
    ).animate
      (
        new CurvedAnimation
          (
            parent: parent,
            curve: new Interval(0.4, 0.7, curve: Curves.easeIn)
        )
    );
  }

  static Animation<Size> backCardSizeAnim(AnimationController parent)
  {
    return new SizeTween
      (
        begin: cardsSize[2],
        end: cardsSize[1]
    ).animate
      (
        new CurvedAnimation
          (
            parent: parent,
            curve: new Interval(0.4, 0.7, curve: Curves.easeIn)
        )
    );
  }

  static Animation<Alignment> middleCardAlignmentAnim(AnimationController parent)
  {
    return new AlignmentTween
      (
        begin: cardsAlign[1],
        end: cardsAlign[2]
    ).animate
      (
        new CurvedAnimation
          (
            parent: parent,
            curve: new Interval(0.2, 0.5, curve: Curves.easeIn)
        )
    );
  }

  static Animation<Size> middleCardSizeAnim(AnimationController parent)
  {
    return new SizeTween
      (
        begin: cardsSize[1],
        end: cardsSize[0]
    ).animate
      (
        new CurvedAnimation
          (
            parent: parent,
            curve: new Interval(0.2, 0.5, curve: Curves.easeIn)
        )
    );
  }

  static Animation<Alignment> frontCardDisappearAlignmentAnim(AnimationController parent, Alignment beginAlign)
  {
    return new AlignmentTween
      (
        begin: beginAlign,
        end: new Alignment(beginAlign.x > 0 ? beginAlign.x + 30.0 : beginAlign.x - 30.0, 0.0) // Has swiped to the left or right?
    ).animate
      (
        new CurvedAnimation
          (
            parent: parent,
            curve: new Interval(0.0, 0.5, curve: Curves.easeIn)
        )
    );
  }
}

@konstantin-doncov konstantin-doncov changed the title Bloc synchronization problem in the bloc and widget State synchronization problem in the bloc and widget Feb 4, 2020
@felangel
Copy link
Owner

felangel commented Feb 4, 2020

Hi @don-prog 👋
Thanks for opening an issue!

Are you able to share a link to a sample app which illustrates the problem you're having? It would be much easier for me to help, thanks!

@felangel felangel self-assigned this Feb 4, 2020
@felangel felangel added question Further information is requested waiting for response Waiting for follow up labels Feb 4, 2020
@konstantin-doncov
Copy link
Author

@felangel yes, of course! Here it is: https://github.com/don-prog/flutter_cards_bloc.
Also, please note that I think the main problem here is my bad architecture due to animation and state synchronization problems which I achieved before. So, if you have any suggestion about this specific problem or this page in general - I want to hear that. :)

@felangel
Copy link
Owner

felangel commented Feb 5, 2020

Thanks @don-prog for providing the sample! I'll take a look later today 👍

@felangel felangel removed the waiting for response Waiting for follow up label Feb 5, 2020
@felangel
Copy link
Owner

felangel commented Feb 6, 2020

I took a quick look and I made a gist to illustrate how I would structure the bloc and widgets. Hope that helps!

Closing for now but feel free to comment with additional questions and I'm happy to continue the conversation 👍

@felangel felangel closed this as completed Feb 6, 2020
@konstantin-doncov
Copy link
Author

@felangel thank you very much! But what is TinderSwipe in your gist? Can you share all the code please? It seems now gist is incomplete.

@felangel
Copy link
Owner

felangel commented Feb 7, 2020

@don-prog I didn't implement it because it's not relevant to bloc. You should just be able to adapt your implementation to conform to that API.

@konstantin-doncov
Copy link
Author

@felangel ok, understand. And what should I do with spent cards? Should I delete them in the TinderSwipe widget or not delete them at all during the swipe (and if so, when to delete)?

@felangel
Copy link
Owner

felangel commented Feb 7, 2020

You can probably just remove them from the bloc state after the swipe 👍

@konstantin-doncov
Copy link
Author

@felangel you mean I can remove them in the bloc or in the TinderSwipe widget?

@felangel
Copy link
Owner

felangel commented Feb 7, 2020

You should remove them in the bloc state which should update the TinderSwipe widget

@konstantin-doncov
Copy link
Author

@felangel Please, forgive me my inquisitiveness, but I really want to correctly understand you. :)
Let's look closely. TinderSwipe doesn't have state reference, only cards, so I can't touch state in it as you said, but bloc in the your gist is also doesn't remove cards after swipe. So, where should I remove cards: in the GameBloc or in the TinderSwipe?
I ask so clearly, because the last time the problem was in the related issue.

@felangel
Copy link
Owner

felangel commented Feb 7, 2020

No problem and sorry for being unclear. I meant GameBloc can remove the cards from the GameBlocState in

if (event.action == GameCardAction.delete) {
  // do something
}

Since TinderSwipe is returned by BlocBuilder when the cards are removed from the state, TinderSwipe will be rebuilt with the updated list of cards and can re-render accordingly.

Hope that helps 👍

@konstantin-doncov
Copy link
Author

@felangel I understand you, thanks! Today I'll try your suggestions a bit later.

@konstantin-doncov
Copy link
Author

konstantin-doncov commented Feb 7, 2020

@felangel unfortunately, I think the problem is not solved.
I need to keep card which is in the process of removing(when card goes beyond the screen), because animation requires this instance. So I keep it in the special var deletingCard in the state. When animation is finished it resets this var to null.
But here is a problem. When new cards are loaded in the bloc I add them in the state and copy previous deletingCard var in the new state which I will yield:

yield GameDeckLoadedState(state.isNeedToAnimate, state.deletingCard, result);

But there is a delay between sending and receiving the state, so during the sending the deletingCard variable is not equal to null(for example, but it's not always so) and we copy it to the new state, and when receiving the state the animation is already completed and the variable of old state is reset to null in the widget, but in the new state it is not so and the variable is not null.

As I understand, the animation is constantly rebuilding the widget while it removes the card.
So the situation is also possible when the last update of the widget occurs during the removal of the card, and during this comes a new state with a variable not equal to zero. The animation ended and the variable was reset, but a new state with a non-null variable returned this card back to the deck.

That is why the delay, which I specifically added, added and removed the problem with the different values.

If you want more details I think it will be better for you to check my original code.

This is directly related to the behavior of the bloc and I still can’t solve it, so I ask you for help.

Regards!

@konstantin-doncov
Copy link
Author

@felangel I understand that you don't have enough time, so in my previous comment I tried to describe my problem more specifically. If you don't understand something in my comment, please ask questions.
Please, can you help me?

@felangel
Copy link
Owner

@don-prog are you able to join the bloc discord and message me so we can set up a time to do a live code share? Thanks!

@konstantin-doncov
Copy link
Author

@felangel If you want, we can do this, but I shared the full problem project and described my problem. English isn't my native language. and I will descibe the same things and show the same code, but a little slower :D

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants