felangel / bloc

A predictable state management library that helps implement the BLoC design pattern
https://bloclibrary.dev
MIT License
11.8k stars 3.39k forks source link

BlocBuilder doesn't rebuild widget tree after yield state #661

Closed IgorChicherin closed 4 years ago

IgorChicherin commented 4 years ago

Greetings, I have a mystical problem with rebuilding widget tree. BlocBuilder build only InitialState and don't handle yield a new states from bloc component.

flutter_bloc: ^2.0.0

Bloc implementation

class SearchBloc extends Bloc<SearchEvent, SearchState> with SaleMapBlocMixin {
  @override
  SearchState get initialState => InitialDataSearchState();

  @override
  Stream<SearchState> transformEvents(
    Stream<SearchEvent> events,
    Stream<SearchState> Function(SearchEvent event) next,
  ) {
    return super.transformEvents(
      (events as Observable<SearchEvent>).debounceTime(
        Duration(milliseconds: 500),
      ),
      next,
    );
  }

  @override
  Stream<SearchState> mapEventToState(
    SearchEvent event,
  ) async* {
    if (event is GetSearchDataSearchEvent) {
      yield* getSearchDataGenerator(event);
    }
  }

  Stream<SearchState> getSearchDataGenerator(
      GetSearchDataSearchEvent event) async* {
    yield LoadingDataSearchState();
    QueryResult searchResult =
        await _fetchSearchResults(event.searchText, event.cameraBounds);

    if (searchResult.hasErrors)
      yield ErrorLoadingSearchState(searchResult.errors.toString());

    List<CampaignVenue> campaignVenues =
        _getCampaignVenues(searchResult.data["campaignVenues"]);

    print(campaignVenues.toString());

    if (campaignVenues.isNotEmpty){
      LocationData currentLocation = await getCurrentLocation();
      yield LoadedDataSearchState(campaignVenues, currentLocation);
    }
    else
      yield EmptyDataSearchState();
  }

  Future<QueryResult> _fetchSearchResults(
      String searchText, LatLngBounds cameraBounds) async {
    final QueryResult searchResult =
        await getResponse(QueryOptions(document: searchQuery, variables: {
      "text": "$searchText",
      "track_search": true,
      "in_bounding_box": {
        "south_west": {
          "lat": cameraBounds.southwest.latitude,
          "lng": cameraBounds.southwest.longitude
        },
        "north_east": {
          "lat": cameraBounds.northeast.latitude,
          "lng": cameraBounds.northeast.longitude
        }
      }
    }));

    return searchResult;
  }

  List<CampaignVenue> _getCampaignVenues(data) =>
      data.map<CampaignVenue>((item) => CampaignVenue.fromJson(item)).toList();
}

Page implemenatation

class SearchPage extends StatefulWidget {
  final LatLngBounds cameraBounds;

  const SearchPage({Key key, this.cameraBounds}) : super(key: key);

  @override
  _SearchPageState createState() => _SearchPageState();
}

class _SearchPageState extends State<SearchPage> {
  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    BlocProvider.of<SearchBloc>(context).close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      builder: (context) => SearchBloc(),
      child: Scaffold(body: BlocBuilder<SearchBloc, SearchState>(
        builder: (BuildContext context, SearchState state) {
          List<Widget> slivers = [
            SearchHeader(cameraBounds: widget.cameraBounds)
          ];

          if (state is InitialDataSearchState) slivers.add(_buildInitial());

          if (state is LoadingDataSearchState) slivers.add(_buildLoading());

          if (state is ErrorLoadingSearchState) slivers.add(_buildInitial());

          if (state is EmptyDataSearchState) slivers.add(_buildEmptyResult());

          if (state is LoadedDataSearchState)
            slivers = <Widget>[
              ...slivers,
              ..._buildData(state.campaignVenues, state.currentLocation)
            ];

          return CustomScrollView(
            slivers: slivers,
          );
        },
      )),
    );
  }

  Widget _buildEmptyResult() => SliverFillRemaining(
        child: Center(
          child: Container(
            child: Text("No search result"),
          ),
        ),
      );

  List<Widget> _buildData(
      List<CampaignVenue> campaignsVenues, LocationData currentLocation) {
    return <Widget>[
      _buildStickyHeaderList(1, campaignsVenues, currentLocation),
      _buildStickyHeaderList(2, campaignsVenues, currentLocation)
    ];
  }

  _buildStickyHeaderList(
      index, List<CampaignVenue> campaignsVenues, currentLocation) {
    return SliverStickyHeaderBuilder(
      builder: (context, state) => _buildAnimatedHeader(context, index, state),
      sliver: SliverList(
        delegate: SliverChildBuilderDelegate(
          (context, i) {
            return ListTile(
              title: _buildLists(index, i, campaignsVenues, currentLocation),
            );
          },
          childCount: campaignsVenues.length,
        ),
      ),
    );
  }

  Widget _buildLists(int sliverIndex, int i,
      List<CampaignVenue> campaignsVenues, currentLocation) {
    if (sliverIndex == 1)
      return CategoryDetailCampaignCard(
        currentLocation: currentLocation,
        campaignVenue: campaignsVenues[i],
      );
    else if (sliverIndex == 2)
      return CategoryDetailVenueCard(
        currentLocation: currentLocation,
        campaignVenue: campaignsVenues[i],
      );
  }

  Widget _buildAnimatedHeader(
      BuildContext context, int index, SliverStickyHeaderState state) {
    return new Container(
      height: 60.0,
      color: SaleMapThemeColors.white.withOpacity(1.0 - state.scrollPercentage),
      padding: EdgeInsets.symmetric(horizontal: 16.0),
      child: Container(
        margin: EdgeInsets.only(top: 16),
        child: Row(
          children: <Widget>[
            Text(
              index == 1 ? "Campaigns" : "Venues",
              style: TextStyle(
                  color: Colors.black, fontSize: 20, fontFamily: "Roboto"),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildInitial() => SliverFillRemaining(
        child: Center(
          child: Container(
            child: Text("Enter search data"),
          ),
        ),
      );

  Widget _buildLoading() => SliverFillRemaining(
        child: Center(
          child: Container(
            child: SaleMapCircularProgressIndicator(),
          ),
        ),
      );
}

Console Logs

flutter: BlocBuilder InitialDataSearchState
flutter: GetSearchDataSearchEvent
flutter: Transition { currentState: InitialDataSearchState, event: GetSearchDataSearchEvent, nextState: LoadingDataSearchState }
flutter: [Instance of 'CampaignVenue', Instance of 'CampaignVenue', Instance of 'CampaignVenue', Instance of 'CampaignVenue', Instance of 'CampaignVenue', Instance of 'CampaignVenue', Instance of 'CampaignVenue', Instance of 'CampaignVenue', Instance of 'CampaignVenue', Instance of 'CampaignVenue', Instance of 'CampaignVenue', Instance of 'CampaignVenue', Instance of 'CampaignVenue', Instance of 'CampaignVenue', Instance of 'CampaignVenue', Instance of 'CampaignVenue', Instance of 'CampaignVenue', Instance of 'CampaignVenue', Instance of 'CampaignVenue', Instance of 'CampaignVenue', Instance of 'CampaignVenue', Instance of 'CampaignVenue', Instance of 'CampaignVenue']
flutter: Transition { currentState: LoadingDataSearchState, event: GetSearchDataSearchEvent, nextState: LoadedDataSearchState }
flutter: GetSearchDataSearchEvent
flutter: GetSearchDataSearchEvent
flutter: Transition { currentState: LoadedDataSearchState, event: GetSearchDataSearchEvent, nextState: LoadingDataSearchState }
flutter: [Instance of 'CampaignVenue', Instance of 'CampaignVenue', Instance of 'CampaignVenue', Instance of 'CampaignVenue', Instance of 'CampaignVenue', Instance of 'CampaignVenue']
flutter: Transition { currentState: LoadingDataSearchState, event: GetSearchDataSearchEvent, nextState: LoadedDataSearchState }
flutter: GetSearchDataSearchEvent
flutter: GetSearchDataSearchEvent
flutter: GetSearchDataSearchEvent
flutter: Transition { currentState: LoadedDataSearchState, event: GetSearchDataSearchEvent, nextState: LoadingDataSearchState }
flutter: []

flutter doctor

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel unknown, v1.9.1+hotfix.4, on Mac OS X 10.15.1 19B88, locale
    en-US)

[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
[✓] Xcode - develop for iOS and macOS (Xcode 11.2)
[✓] Android Studio (version 3.5)
[!] Connected device
    ! No devices available

! Doctor found issues in 1 category.
felangel commented 4 years ago

Hi @IgorChicherin 👋 Thanks for opening an issue!

I'm guessing you're extending Equatable in your state classes and are not properly overriding the props getter. Can you please share your SearchState implementation? Thanks!

IgorChicherin commented 4 years ago

@felangel Yep, sure, here is states implementation.


abstract class SearchState extends Equatable {
  const SearchState();
}

class InitialDataSearchState extends SearchState {
  @override
  List<Object> get props => ["InitialDataSearchBarState"];
}

class LoadingDataSearchState extends SearchState {
  @override
  List<Object> get props => ["LoadingDataSearchBarState"];
}

class LoadedDataSearchState extends SearchState {
  final List<CampaignVenue> campaignVenues;
  final LocationData currentLocation;

  LoadedDataSearchState(this.campaignVenues, this.currentLocation);

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

class EmptyDataSearchState extends SearchState {
  @override
  List<Object> get props => ["EmptyDataSearchState"];

}

class ErrorLoadingSearchState extends SearchState {
  final String errorMessage;

  ErrorLoadingSearchState(this.errorMessage);

  @override
  List<Object> get props => ["ErrorLoadingSearchBarState"];
}
felangel commented 4 years ago

props should return the properties for each class otherwise Equatable cannot do the equality comparison properly.

abstract class SearchState extends Equatable {
  const SearchState();

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

class InitialDataSearchState extends SearchState {}

class LoadingDataSearchState extends SearchState {}

class LoadedDataSearchState extends SearchState {
  final List<CampaignVenue> campaignVenues;
  final LocationData currentLocation;

  const LoadedDataSearchState(this.campaignVenues, this.currentLocation);

  @override
  List<Object> get props => [campaignVenues, currentLocation];
}

class EmptyDataSearchState extends SearchState {}

class ErrorLoadingSearchState extends SearchState {
  final String errorMessage;

  const ErrorLoadingSearchState(this.errorMessage);

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

Also you need to ensure that CampaignVenue and LocationData extend Equatable and override props correctly.

Hope that helps 👍

IgorChicherin commented 4 years ago

@felangel oh, my bad, thanks alot for helping

jippy96 commented 3 years ago

Hello @felangel , i'm working on TDD clean architecture with bloc as state management, I have the same problem and tried to follow the instructions about the issue but it doesn't work I will put my bloc code just below.

`class ProductBloc extends Bloc<ProductEvent, ProductState> { final ProductRemoteDataSourceImpl productRemoteDataSourceImpl = ProductRemoteDataSourceImpl(); ProductBloc() : super(ProductInitial());

@override Stream mapEventToState( ProductEvent event, ) async* { if (event is ProductsEventLoad) { // Passage à l'état de chargement des produits yield ProducStateLoading(); // Appel du repository qui va se charger de charger les produits final products = await productRemoteDataSourceImpl.getSomeProducts(20); // Passage à l'état d'affichage des produits yield ProductStateLoaded(products: products); } else if (event is ProductsEventLoadMore) { print('more'); final products = await productRemoteDataSourceImpl.getSomeProducts(20); yield ProductStateLoaded(products: products); } else if (event is ProductEventLike) { // Enregistrement du Like en base if (state is ProductStateLoaded) { final List productsUpdated = (state as ProductStateLoaded).products.map((product) { return product.id == event.product.id ? event.product : product; }).toList(); print(productsUpdated.length); yield ProductStateLoaded(products: productsUpdated); } } } } `

bloc state code is here :

`

abstract class ProductState extends Equatable { ProductState(); }

class ProductInitial extends ProductState { @override List get props => []; }

class ProducStateLoading extends ProductState { @override List get props => []; }

class ProductStateLoaded extends ProductState { final List products;

ProductStateLoaded({@required this.products}); @override List get props => [products]; }

class ProductStateLiked extends ProductState { final Product product;

ProductStateLiked({@required this.product}); @override List get props => [product]; }

`