3
votes

I'm building a tic-tak-toe app and I decided to learn BLoC for Flutter along. I hava a problem with the BlocBuilder widget.

As I think about it. Every time Cubit/Bloc that the bloc builder widget listens to emits new state the bloc builder goes through this routine:

  1. Call buildWhen callback passing previous state as the previous parameter and the newly emitted state as the current parameter.

  2. If the buildWhen callback returned true then rebuild.

  3. During rebuilding call the builder callback passing given context as context parameter and the newly emitted state as state parameter. This callback returns the widget that we return.

So the conclusion is that the current parameter of the buildWhen call is always equal to the state parameter of the builder call. But in practice it's different:

BlocBuilder<GameCubit, GameState>(
      buildWhen: (previous, current) => current is SetSlotSignGameState && (current as SetSlotSignGameState).slotPosition == widget.pos,
      builder: (context, state) {
        var sign = (state as SetSlotSignGameState).sign;
        // Widget creation goes here...
      },
    );

In the builder callback, it throws:

The following _CastError was thrown building BlocBuilder<GameCubit, GameState>(dirty, state: _BlocBuilderBaseState<GameCubit, GameState>#dc100): type 'GameState' is not a subtype of type 'SetSlotSignGameState' in type cast The relevant error-causing widget was: BlocBuilder<GameCubit, GameState>

The method where I emit the states that is in the GameCubit class:

// [pos] is the position of the slot clicked
void setSlotSign(Vec2<int> pos) {

    // Some code

    emit(SetSlotSignGameState(/* Parameter representing the sign that is being placed in the slot*/, pos));

    // Some code

    emit(TurnChangeGameState());
}

Briefly about types of states. SetSlotSignGameState is emitted when a user taps on a slot in the tic-tac-toe grid and the slot is empty. So this state means that we need to change sign in some slot. TurnChangeGameState is emitted when we need to give the turn to the next player.

Temporary solution. For now I fixed it by saving the state from buildWhen callback in a private field of the widget's state and then using it from the builder. BlocListener also has this problem but there I can just move the check from listenWhen callback into listen callback. The disadvantage of this solution is that it's very inelegant and inconvenient.

2
In your code you have a syntax error: buildWhen: (previous, current) => current is SetSlotSignGameState && (current as SetSlotSignGameState).slotPosition == widget.pos) <-- THIS BRACKET HAS NO PAIR,. That was by mystake ?jorjdaniel
jorjdaniel, yes, that's a mistake I did copying the code from my code editor, original code in the project does not have this mistake. I'm sorry for confusing you with it. I will edit the question removing the typo.user11655900

2 Answers

3
votes

buildWhen is bypassed (not even called) on initial state OR when Flutter requests a rebuild.

I have created a small "test" to emphasize that:

import 'package:bloc/bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

void main() {
  runApp(BlocTestApp());
}

class BlocTestApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BlocProvider<TestCubit>(
        // Create the TestCubit and call test() event right away
        create: (context) => TestCubit()..test(),
        child: BlocBuilder<TestCubit, String>(
          buildWhen: (previous, current) {
            print("Call buildWhen(previous: $previous, current: $current)");
            return false;
          },
          builder: (context, state) {
            print("Build $state");
            return Text(state);
          },
        ),
      ),
    );
  }
}

class TestCubit extends Cubit<String> {
  TestCubit() : super("Initial State");

  void test() {
    Future.delayed(Duration(seconds: 2), () {
      emit("Test State");
    });
  }
}

OUTPUT:

I/flutter (13854): Build Initial State
I/flutter (13854): Call buildWhen(previous: Initial State, current: Test State)

As can be seen from output the initial state is built right away without calling buildWhen. Only when the state changes buildWhen is examined.


Other References

This behavior is also outlined here by the creator of Flutter Bloc library (@felangel):

This is expected behavior because there are two reasons for a BlocBuilder to rebuild:

  1. The bloc state changed
  2. Flutter marked the widget as needing to be rebuilt.

buildWhen will prevent builds triggered by 1 but not by 2. In this case, when the language changes, the whole widget tree is likely being rebuilt which is why the BlocBuilder is rebuilt despite buildWhen.


Possible solution

In your situation, based on the little code you revealed, is better to store the entire Tic-Tac-Toe configuration in the state and use BLOC events to alter it. In this way you do not need that buildWhen condition.

OR make the check inside the builder function if the logic let you do that (this is the most common used solutions with BLOC).

To respond to you question (if not clear so far :D): Sadly, you can not rely on buildWhen to filter the state types sent to builder function.

0
votes

Could you please check if SetSlotSignGameState extends the abstract class GameState