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

Wrong state in builder when yielding too fast #1682

Closed JackBackstack closed 4 years ago

JackBackstack commented 4 years ago

First of all: thanks for this amazing library :)

More importantly: I think the builder doesn't take the state that was considered in the buildWhen method when the states are yielding too fast. I would expect to get the state in the builder method that I checked in the buildWhen method and not the latest state.

BlocBuilder Code

return BlocBuilder<GroupsBloc, GroupsState>(
  buildWhen: (previous, current) {
    debugPrint("buildWhen(): groupState=${current.runtimeType}");
    return (current is GroupsStateCreateSuccess);
  },
  builder: (context, groupState) {
    debugPrint("build(): groupState=${groupState.runtimeType}");
    return Container();
}

Output

15:26:04.462 - buildWhen(): groupState=GroupsStateCreateInProgress
15:26:04.852 - buildWhen(): groupState=GroupsStateCreateSuccess
15:26:04.866 - buildWhen(): groupState=GroupsStateDataChangedInProgress
15:26:05.053 - build(): groupState=GroupsStateDataChangedInProgress
15:26:04.779 - buildWhen(): groupState=GroupsStateDataChangedSuccess

Or do you have any other suggestion for this issue?

felangel commented 4 years ago

Hi @JackBackstack 👋 Thanks for opening an issue and for the positive feedback! Can you please provide a link to a sample app which illustrates the issue? In general, if you yield too quickly build will not be called because Flutter renders at 60fps but I would love to take a closer look and ensure it's not an issue in the library 👍

JackBackstack commented 4 years ago

Happy to help you. This small app shows that only the last event triggers the build function. Can't tell if this is Flutter or your Framework.

Single file code

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: [
        BlocProvider<SomeBloc>(create: (BuildContext context) => SomeBloc()),
      ],
      child: MaterialApp(
        title: "Fast yielding bloc",
        home: SomeWidget(),
      ),
    );
  }
}

/// WIDGET
class SomeWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<SomeBloc, SomeState>(
      buildWhen: (previous, current) {
        debugPrint("buildWhen(): state=${current.runtimeType}");
        return true;
      },
      builder: (context, groupState) {
        debugPrint("build(): state=${groupState.runtimeType}");
        return Scaffold(
          floatingActionButton: FloatingActionButton(
            onPressed: () => generateSomeEvents(context),
          ),
          body: Center(child: Text("Fast yielding bloc")),
        );
      },
    );
  }

  generateSomeEvents(BuildContext context) async {
    for (int i = 0; i < 10; i++) {
      if (i.isEven) {
        debugPrint("generateSomeEvents(): event value=$i");
        BlocProvider.of<SomeBloc>(context).add(SomeEventSomethingOne("even value $i"));
      } else {
        debugPrint("generateSomeEvents(): event value=$i");
        BlocProvider.of<SomeBloc>(context).add(SomeEventSomethingTwo("odd value $i"));
      }
    }
  }
}

/// BLOC
class SomeBloc extends Bloc<SomeEvent, SomeState> {
  // constructor
  SomeBloc() : super(SomeStateInitial(null));

  // mapping
  @override
  Stream<SomeState> mapEventToState(SomeEvent event) async* {
    debugPrint("mapEventToState(): event=${event.runtimeType}");
    if (event is SomeEventSomethingOne) {
      yield SomeStateSomethingOne(event.eventValue);
    } else if (event is SomeEventSomethingTwo) {
      yield SomeStateSomethingTwo(event.eventValue);
    }
  }
}

/// STATE
abstract class SomeState extends Equatable {
  final String stateValue;
  const SomeState(this.stateValue);
  @override
  List<Object> get props => [stateValue];
}

class SomeStateInitial extends SomeState {
  SomeStateInitial(String stateValue) : super(stateValue);
}

class SomeStateSomethingOne extends SomeState {
  SomeStateSomethingOne(String stateValue) : super(stateValue);
}

class SomeStateSomethingTwo extends SomeState {
  SomeStateSomethingTwo(String stateValue) : super(stateValue);
}

/// EVENT
abstract class SomeEvent extends Equatable {
  final String eventValue;
  SomeEvent(this.eventValue);
  @override
  List<Object> get props => [eventValue];
}

class SomeEventSomethingOne extends SomeEvent {
  SomeEventSomethingOne(String eventValue) : super(eventValue);
}

class SomeEventSomethingTwo extends SomeEvent {
  SomeEventSomethingTwo(String eventValue) : super(eventValue);
}

Output

I/flutter (22734): generateSomeEvents(): event value=0
I/flutter (22734): generateSomeEvents(): event value=1
I/flutter (22734): generateSomeEvents(): event value=2
I/flutter (22734): generateSomeEvents(): event value=3
I/flutter (22734): generateSomeEvents(): event value=4
I/flutter (22734): generateSomeEvents(): event value=5
I/flutter (22734): generateSomeEvents(): event value=6
I/flutter (22734): generateSomeEvents(): event value=7
I/flutter (22734): generateSomeEvents(): event value=8
I/flutter (22734): generateSomeEvents(): event value=9
I/flutter (22734): mapEventToState(): event=SomeEventSomethingOne
I/flutter (22734): buildWhen(): state=SomeStateSomethingOne
I/flutter (22734): mapEventToState(): event=SomeEventSomethingTwo
I/flutter (22734): buildWhen(): state=SomeStateSomethingTwo
I/flutter (22734): mapEventToState(): event=SomeEventSomethingOne
I/flutter (22734): buildWhen(): state=SomeStateSomethingOne
I/flutter (22734): mapEventToState(): event=SomeEventSomethingTwo
I/flutter (22734): buildWhen(): state=SomeStateSomethingTwo
I/flutter (22734): mapEventToState(): event=SomeEventSomethingOne
I/flutter (22734): buildWhen(): state=SomeStateSomethingOne
I/flutter (22734): mapEventToState(): event=SomeEventSomethingTwo
I/flutter (22734): buildWhen(): state=SomeStateSomethingTwo
I/flutter (22734): mapEventToState(): event=SomeEventSomethingOne
I/flutter (22734): buildWhen(): state=SomeStateSomethingOne
I/flutter (22734): mapEventToState(): event=SomeEventSomethingTwo
I/flutter (22734): buildWhen(): state=SomeStateSomethingTwo
I/flutter (22734): mapEventToState(): event=SomeEventSomethingOne
I/flutter (22734): buildWhen(): state=SomeStateSomethingOne
I/flutter (22734): mapEventToState(): event=SomeEventSomethingTwo
I/flutter (22734): buildWhen(): state=SomeStateSomethingTwo
I/flutter (22734): build(): state=SomeStateSomethingTwo

Nevertheless, do you might have any idea for me how to rely on the build function?

zeusbaba commented 4 years ago

I can confirm that I have similar issue and it happens only with v6.0.3 I have downgraded to v6.0.2 and issue disappears.

felangel commented 4 years ago

@zeusbaba can you please provide a sample app which illustrates the issue? Thanks! 🙏

felangel commented 4 years ago

@zeusbaba @JackBackstack can you try using the following branch and let me know if it addresses the issue? https://github.com/felangel/bloc/pull/1684

felangel commented 4 years ago

@JackBackstack and @zeusbaba I tried the snippet above on both 6.0.3 and 6.0.2 and saw the exact same output. What is the problem you're trying to solve? I can reproduce the same behavior using setState -- if I call setState back to back faster than flutter can rebuild you will experience the same behavior.

JackBackstack commented 4 years ago

@felangel This gives the exact output for me. I also can't confirm it was working with any version before.

The problem I want to solve on a higher level: I have a bloc that manages a list of objects which are stored in Firebase's Firestore. So I have an event to create an object and I have an event when backend data changed. Unfortunately the data changed state is fired so fast that the event for successfully creating an object is not visible in the build method. Which I need because this has the ID of the created object.

JackBackstack commented 4 years ago

@felangel clarification after checking with my real project: In this demo snippet it did not change anything. In my real project in fact it changed the state that was passed to the build function. It's the state I wanted in the buildWhen function. That's working as expected. Thanks a lot for this fix! Edit: this seems only to be true for the first build function. If multiple BlocBuilder listening to this state only one of them gets the the desired saved state. All others will get the current state.

Which raises another question. How reliable is this kind of architecture in general? When Flutter skips some build functions without giving us the chance to be aware of it? I assume it's nothing you can do about and many other state managements have similar problems?

zeusbaba commented 4 years ago

let me try to summarize my case;
i'm using

buildWhen: (prev, current) {
      return (current is Authenticated || current is Unauthenticated);
    }, builder: (context, state) {
      CommonUtils.logger.d("main.builder state: $state");
....

to make sure that ONLY those two states triggers rebuild.
However, when used v6.0.3 other states were triggering rebuild as well, and this confused builder method as it expected only one of the defined states, see buildWhen.

when i downgraded to v6.0.2, this issue disappeared.

felangel commented 4 years ago

@zeusbaba @JackBackstack thanks for the feedback! I'm going to have #1684 merged and published to fix the potential incorrect build values when buildWhen and listenWhen are provided.

Edit: this seems only to be true for the first build function. If multiple BlocBuilder listening to this state only one of them gets the the desired saved state. All others will get the current state.

Which raises another question. How reliable is this kind of architecture in general? When Flutter skips some build functions without giving us the chance to be aware of it? I assume it's nothing you can do about and many other state managements have similar problems?

Can you provide a sample app to illustrate this issue? I'm not sure I fully understand what you're trying to accomplish, thanks!

JackBackstack commented 4 years ago

@felangel I'm not sure if I manage to create a sample app in the the next days but this is what I'm trying to archieve.

The problem I want to solve on a higher level: I have a bloc that manages a list of objects which are stored in Firebase's Firestore. So I have an event to create an object and I have an event when backend data changed. Unfortunately the data changed state is fired so fast that the event for successfully creating an object is not visible in the build method. Which I need because this has the ID of the created object.

felangel commented 4 years ago

Unfortunately the data changed state is fired so fast that the event for successfully creating an object is not visible in the build method.

@jackbackstack what do you mean by this? All events will be processed by the bloc and will emit states. The issue is states can be emitted more frequently than the UI can re-render but that shouldn't really be a problem and it isn't specific to bloc.

thalissone commented 3 years ago

I have same problem.

@override
  Widget build(BuildContext context) {
    return BlocProvider(
        create: (context) => controller,
        child: BlocConsumer<GroupFeedViewModel, GroupFeedViewSate>(
          listenWhen: (previous, current) {
            print("listenWhen: previous: $previous / current: $current");
            return current is! SuccessState;
          },
          listener: (context, state) {
            if (state is LoadingState) {
              print("listener: LoadingState: ${state.loading}");
              context.showHideLoader(state.loading);
            } else if (state is ErrorState) {
              print("listener: ErrorState");
              _builderError();
            }
          },
          buildWhen: (previous, current) {
            print("builderWhen: previous: $previous / current: $current");
            return current is SuccessState;
          },
          builder: (context, state) {
            print("builder: $state");
            if (state is SuccessState) {
              return _buildSuccessState();
            }
            return SizedBox.shrink();
          },
        ));
  }

Log:

builder: Instance of 'LoadingState'

I make first call in init:

  void initState() {
    super.initState();
    controller.init(widget.groupId);
  }

My condition not accept state different than SuccessState, but when open screen and make first call, builder is calling. This is very fast and is valid for the first request, the others work as expected.

bloc: ^6.1.1

Any idea?

felangel commented 3 years ago

Hi @thalissone please refer to the documentation:

An optional buildWhen can be implemented for more granular control over how often BlocBuilder rebuilds. buildWhen should only be used for performance optimizations as it provides no security about the state passed to the builder function.

https://pub.dev/documentation/flutter_bloc/latest/flutter_bloc/BlocBuilder-class.html

This is working as expected and your builder should be able to handle every state -- buildWhen is just an optimization. Hope that helps 👍