Bloc: BlocBuilder doesn't rebuild after yield

Created on 7 Apr 2019  路  8Comments  路  Source: felangel/bloc

Describe the bug
Hi, when I run .dispatch on my bloc, the ListView in BlocBuilder stays the same. Here's the bloc and the blocBuilder:

class DungeonBloc extends Bloc<List<DungeonTile>, List<DungeonTile>> {
  int r = Random().nextInt(eventTypes.length);

  @override
  List<DungeonTile> get initialState => [
    DungeonTile(event: DungeonEvent(eventType: "loot", length: 10)),
    DungeonTile(event: DungeonEvent(eventType: "fight", length: 10)),
    DungeonTile(event: DungeonEvent(eventType: "puzzle", length: 10))
  ];

  @override
  Stream<List<DungeonTile>> mapEventToState(List<DungeonTile> event) async* {
    switch(event.length) {
      case 3:
        event.add(DungeonTile(event: DungeonEvent(eventType: eventTypes[r], length: 10)));
        yield event;
        break;
      case 4:
        event.removeAt(0);
        yield event;
        break;
    }
  }
}
BlocBuilder(
  bloc: _dungeonBloc,
  builder: (BuildContext context, List<DungeonTile> l) {
    _dungeonTiles = l;
    return ListView.builder(
      physics: NeverScrollableScrollPhysics(),
      controller: _scrollController,
      padding: EdgeInsets.all(0.0),
      scrollDirection: Axis.horizontal,
      shrinkWrap: true,
      itemCount: _dungeonTiles.length,
      itemBuilder: (BuildContext, int index) => _dungeonTiles[index],
      );
    }
)

However, if I change the yield event; in DungeonBloc to yield initial state; or to a fixed value such as yield [DungeonTile(event: DungeonEvent(eventType: "loot", length: 10))];, then the code works fine and the BlocBuilder gets rebuilt.

question

Most helpful comment

Hi @janhrastnik 馃憢

I believe the issue you鈥檙e facing is because you are mutating and yielding the same state instead of yielding a new instance of your state each time. You should use List.from to create a new list from the current event and then you can modify and yield the new list. Hope that helps 馃憤

All 8 comments

Hi @janhrastnik 馃憢

I believe the issue you鈥檙e facing is because you are mutating and yielding the same state instead of yielding a new instance of your state each time. You should use List.from to create a new list from the current event and then you can modify and yield the new list. Hope that helps 馃憤

I tried this by writing the following code, but the issue stays the same. Did you mean something like this?

class DungeonBloc extends Bloc<List<DungeonTile>, List<DungeonTile>> {
  int r = Random().nextInt(eventTypes.length);

  @override
  List<DungeonTile> get initialState => [
    DungeonTile(event: DungeonEvent(eventType: "loot", length: 10)),
    DungeonTile(event: DungeonEvent(eventType: "fight", length: 10)),
    DungeonTile(event: DungeonEvent(eventType: "puzzle", length: 10))
  ];

  @override
  Stream<List<DungeonTile>> mapEventToState(List<DungeonTile> event) async* {
    switch(event.length) {
      case 3:
        final List newList = List.from(event);
        newList.add(DungeonTile(event: DungeonEvent(eventType: eventTypes[r], length: 10)));
        yield newList;
        break;
      case 4:
        final List newList = List.from(event);
        newList.removeAt(0);
        yield newList;
        break;
    }
  }

}

Yeah that鈥檚 what I meant. Hmm if it鈥檚 still not working can you also share you implementation of DungeonTile? Thanks 馃憤

Of course. This is main.dart. I'm trying to show a ListView at the centre of the screen with 3 DungeonTile instances. When DungeonBloc gets called once, I want to add another DungeonTile to the ListView then scroll the ListView to show the last 3 DungeonTiles. Then I call DungeonBloc again to remove the first DungeonTile. Hope that clears things up a bit.

import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
import 'clickerbloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

double TILE_LENGTH;
List eventTypes = ["loot", "fight", "puzzle"];

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  final ClickerBloc _clickerBloc = ClickerBloc();
  final DungeonBloc _dungeonBloc = DungeonBloc();

  @override
  Widget build(BuildContext context) {
    SystemChrome.setEnabledSystemUIOverlays([]);
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: BlocProviderTree(
        blocProviders: <BlocProvider>[
          BlocProvider<ClickerBloc>(bloc: _clickerBloc),
          BlocProvider<DungeonBloc>(bloc: _dungeonBloc),
        ],
        child: DungeonList(),
      ),
      );
  }
}

class DungeonList extends StatefulWidget {
  @override
  DungeonListState createState() => DungeonListState();
}

class DungeonListState extends State<DungeonList> {
  ScrollController _scrollController = ScrollController();
  List<DungeonTile> _dungeonTiles = [
    DungeonTile(event: DungeonEvent(eventType: "loot", length: 10)),
    DungeonTile(event: DungeonEvent(eventType: "fight", length: 10)),
    DungeonTile(event: DungeonEvent(eventType: "puzzle", length: 10))
  ];

  _scrollToMiddle() {
    _scrollController.jumpTo(MediaQuery.of(context).size.width/4);
    print("OFFSET IS ${_scrollController.offset}");
  }

  _scrollToNextRoom(bloc) {
    _scrollToMiddle();
    print(_scrollController.offset);
    bloc.dispatch(_dungeonTiles);
    _scrollController.animateTo(
      90.0 + MediaQuery.of(context).size.width/2,
      duration: Duration(seconds: 1),
      curve: Curves.ease
    ).then((data) {
      bloc.dispatch(_dungeonTiles);
    });
  }

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToMiddle());
  }

  @override
  Widget build(BuildContext context) {
    TILE_LENGTH = MediaQuery.of(context).size.width/2;
    final ClickerBloc _clickerBloc = BlocProvider.of<ClickerBloc>(context);
    final DungeonBloc _dungeonBloc = BlocProvider.of<DungeonBloc>(context);
    return Scaffold(
      body: Column(
        children: <Widget>[
          Expanded(
            child: GestureDetector(
              onTap: () {
                _clickerBloc.dispatch(_dungeonTiles[1].event);
              },
              child: Center(
                child: Stack(
                  alignment: Alignment(-0.1, 0.0),
                  children: <Widget>[
                    ConstrainedBox(
                      constraints: BoxConstraints(
                          maxHeight: 200.0
                      ),
                      child: BlocBuilder(
                          bloc: _dungeonBloc,
                          builder: (BuildContext context, List<DungeonTile> l) {
                            print("BLOCBUILDER GETS CALLED");
                            _dungeonTiles = l;
                            return ListView.builder(
                              physics: NeverScrollableScrollPhysics(),
                              controller: _scrollController,
                              padding: EdgeInsets.all(0.0),
                              scrollDirection: Axis.horizontal,
                              shrinkWrap: true,
                              itemCount: _dungeonTiles.length,
                              itemBuilder: (BuildContext, int index) => _dungeonTiles[index],
                            );
                          }
                      ),
                    ),
                    Text("Hero")
                  ],
                ),
              ),
            ),
          ),
          BlocBuilder(
              bloc: _clickerBloc,
              builder: (BuildContext context, double progress) {
                print(progress);
                if (progress == -1) {
                  _scrollToNextRoom(_dungeonBloc);
                }
                return LinearProgressIndicator(
                  value: progress,
                );
              }
          ),
          Row(
            children: <Widget>[
              Text("Hero"),
              Text("Inventory"),
              Text("Skills")
            ],
          )
        ],
      ),
    );
  }
}

class DungeonTile extends StatelessWidget {
  DungeonTile({Key key, @required this.event}) : super(key: key);

  final DungeonEvent event;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: TILE_LENGTH,
      height: 100.0,
      decoration: BoxDecoration(
        image: DecorationImage(image: AssetImage("assets/mayclover_meadow_example.png"), fit: BoxFit.cover),
        border: new Border.all(color: Colors.blueAccent)
      ),
      alignment: Alignment(0.7, 0.0),
      child: Text(event.eventType),
    );
  }
}

class DungeonEvent {
  String eventType;
  int length;
  int progress;
  DungeonEvent({@required this.eventType, @required this.length, this.progress=0});
}

And this is the clickerbloc.dart file:

import 'main.dart';
import 'package:bloc/bloc.dart';
import 'dart:math';

class ClickerBloc extends Bloc<DungeonEvent, double> {
  double get initialState => 0.0;

  Stream<double> mapEventToState(DungeonEvent event) async* {
    print(event.eventType);
    switch(event.eventType) {
      case "fight":
        event.progress++;
        if (event.progress == event.length) {
          yield -1;
        } else {
          yield event.progress / event.length;
        }
        break;
      case "loot":
        event.progress++;
        if (event.progress == event.length) {
          yield -1;
        } else {
          yield event.progress / event.length;
        }
        break;
      case "puzzle":
        event.progress++;
        if (event.progress == event.length) {
          yield -1;
        } else {
          yield event.progress / event.length;
        }
        break;
    }
  }
}

class DungeonBloc extends Bloc<List<DungeonTile>, List<DungeonTile>> {
  int r = Random().nextInt(eventTypes.length);

  @override
  List<DungeonTile> get initialState => [
    DungeonTile(event: DungeonEvent(eventType: "loot", length: 10)),
    DungeonTile(event: DungeonEvent(eventType: "fight", length: 10)),
    DungeonTile(event: DungeonEvent(eventType: "puzzle", length: 10))
  ];

  @override
  Stream<List<DungeonTile>> mapEventToState(List<DungeonTile> event) async* {
    switch(event.length) {
      case 3:
        final List newList = List.from(event, growable: true);
        newList.add(DungeonTile(event: DungeonEvent(eventType: eventTypes[r], length: 10)));
        yield newList;
        break;
      case 4:
        final List newList = List.from(event);
        newList.removeAt(0);
        yield newList;
        break;
    }
  }

}

@janhrastnik I took a look and everything seems to be working fine for me. Check out the gist. When the event is dispatched, the BlocBuilder rebuilds with the new set of events. Hope that helps 馃憤

Closing for now but feel free to comment with additional comments/questions and I'm more than happy to continue the conversation.

Your code works for me now, thank you so much! I've also found my problem now. In my code, the modified list that I'm yielding is dynamic, but if I change it to a List, then the code works all of a sudden.

Hi @janhrastnik 馃憢

I believe the issue you鈥檙e facing is because you are mutating and yielding the same state instead of yielding a new instance of your state each time. You should use List.from to create a new list from the current event and then you can modify and yield the new list. Hope that helps 馃憤

You saved my day! where i found this in Bloc library doc?
I have problem this when build base bloc load more and when load more state not rebuild. But now resolved.

im confused in my case, it does not rebuild the widget.
this is the state's json
{id: , first_name: test, last_name: test, address: null, created_by: null, date_created: null, email: null, notes: null, phone1: 123131, phone2: null, client_state: null, lead_source: 5edd9fcbae9e1f24a8710daa}

while this is what is sent to yield. the rebuild should work right?
{id: 5ee2fecf54051805d88a4653, first_name: test, last_name: test, address: null, created_by: 5ee1aa04c6b22824744915ac, date_created: 2020-06-12T12:04:31, email: null, notes: null, phone1: 123131, phone2: null, client_state: null, lead_source: 5edd9fcbae9e1f24a8710daa}

the call is yield ClassName.fromJson() . clear difference is the 2nd json has a value for id property so it should trigger the rebuild supposedly, right?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

MahdiPishguy picture MahdiPishguy  路  3Comments

frankrod picture frankrod  路  3Comments

rsnider19 picture rsnider19  路  3Comments

wheel1992 picture wheel1992  路  3Comments

shawnchan2014 picture shawnchan2014  路  3Comments