Bloc: Bloc strange behavior with ..add method after recent flutter upgrade command

Created on 16 Feb 2020  ยท  11Comments  ยท  Source: felangel/bloc

Describe the bug
Hi, felangel
Thanks for this great package.
I used flutter upgrade command today. Now my environment is:

Flutter 1.14.6 โ€ข channel beta โ€ข https://github.com/flutter/flutter.git
Framework โ€ข revision fabeb2a16f (3 weeks ago) โ€ข 2020-01-28 07:56:51 -0800
Engine โ€ข revision c4229bfbba
Tools โ€ข Dart 2.8.0 (build 2.8.0-dev.5.0 fc3af737c7)

I don't change any code. The Bloc behavior becomes strange.

 runApp(BlocProvider<SomeBloc>(
    create: (context) => SomeBloc()..add(InitEvent()),
    child: MyApp(),
  ));

I use ..add to fire an event when creating the bloc. It works well previously. An InitEvent is received by the mapEventToState method immediately. After upgrade, no InitEvent is received when the app starts. However, if I press a button which adds another event, the InitEvent and another event are received at the same time.

I set up a simple project to reproduce it.

The output in my terminal:

// Output after I press shift+R
Performing hot restart...

Restarted application in 2,725ms.
I/flutter (10716): ---> My App Init

// Output after I click the button
D/ViewRootImpl(10716): ViewPostImeInputStage processPointer 0
D/ViewRootImpl(10716): ViewPostImeInputStage processPointer 1
I/flutter (10716): bloc event: Instance of 'SomeBloc' Instance of 'InitEvent'
I/flutter (10716): bloc event: Instance of 'SomeBloc' Instance of 'SecondEvent'
I/flutter (10716): ---> init event
I/flutter (10716): bloc transition: Instance of 'SomeBloc' Transition { currentState: Instance of 'SomeInitial', event: Instance of 'InitEvent', nextState: NextState }
I/flutter (10716): ---> second event
I/flutter (10716): bloc transition: Instance of 'SomeBloc' Transition { currentState: NextState, event: Instance of 'SecondEvent', nextState: NextState }

// Output after I click the button again
D/ViewRootImpl(10716): ViewPostImeInputStage processPointer 0
D/ViewRootImpl(10716): ViewPostImeInputStage processPointer 1
I/flutter (10716): bloc event: Instance of 'SomeBloc' Instance of 'SecondEvent'
I/flutter (10716): ---> second event
I/flutter (10716): bloc transition: Instance of 'SomeBloc' Transition { currentState: NextState, event: Instance of 'SecondEvent', nextState: NextState }

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

class DebugBlocDelegate extends BlocDelegate {
  @override
  void onEvent(Bloc bloc, Object event) {
    super.onEvent(bloc, event);
    print('bloc event: $bloc $event');
  }

  @override
  void onError(Bloc bloc, Object error, StackTrace stacktrace) {
    super.onError(bloc, error, stacktrace);
    print('bloc error: $bloc $error');
  }

  @override
  void onTransition(Bloc bloc, Transition transition) {
    super.onTransition(bloc, transition);
    print('bloc transition: $bloc $transition');
  }
}

void main() {
  WidgetsFlutterBinding.ensureInitialized();

  BlocSupervisor.delegate = DebugBlocDelegate();
  runApp(BlocProvider<SomeBloc>(
    create: (context) => SomeBloc()..add(InitEvent()),
    child: MyApp(),
  ));
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    print('---> My App Init');
    return MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Bloc App'),
      ),
      body: Center(
        child: RaisedButton(
          child: Text('Trigger Event'),
          onPressed: () {
            BlocProvider.of<SomeBloc>(context).add(SecondEvent());
          },
        ),
      ),
    );
  }
}

@immutable
abstract class SomeEvent {}

class InitEvent extends SomeEvent {}

class SecondEvent extends SomeEvent {}

@immutable
abstract class SomeState {}

class SomeInitial extends SomeState {}

class NextState extends SomeState {
  @override
  String toString() => 'NextState';
}

class SomeBloc extends Bloc<SomeEvent, SomeState> {
  @override
  SomeState get initialState => SomeInitial();

  @override
  Stream<SomeState> mapEventToState(
    SomeEvent event,
  ) async* {
    if (event is InitEvent) {
      print('---> init event');
      yield NextState();
    } else if (event is SecondEvent) {
      print('---> second event');
      yield NextState();
    }
  }
}
question

Most helpful comment

I think lazy being true by default is the right way to go about this, since it's better to deffer as much initialization as possible to have a better startup time.
I feel like the real problem here is people not being aware that this setting exists. Once you know about its existence, you start to see it as a feature rather than inconvenience ๐Ÿ™‚

All 11 comments

I find the reason. After I set the BlocProvider parameter lazy to false, it works as usual. However, I still confused about why it worked well in the past several months.

Hi @marklureal ๐Ÿ‘‹
Thanks for opening an issue!

This was a change that came with provider v4.0.0 (with the v3.0.0 version of flutter_bloc).

Iโ€™m thinking of changing the default to be lazy: false again in the next major version.

Sorry for the inconvenience!

@felangel is it wrong tho, having it default to true (the current way I suppose)? Bloc will work all right I suppose, for all consumers. Also, it would be a kinda big difference from the provider API? I am pretty OK with disabling lazy when I need to, as I would do with provider.

@tenhobi I think to me the part that's confusing is if I do something like:

BlocProvider(
  create: (_) => MyBloc()..add(MyEvent()),
  child: MyChild(),
)

I wouldn't expect MyEvent to be added only after BlocProvider.of<MyBloc>(context) is called. I'm also curious to hear what people think about "lazy loading" for bloc because I personally don't see much benefit. If you scope things to the correct part of the widget tree then I don't see a compelling reason to have lazy loading at all. Thoughts?

FWIW I just got bitten by this.

I like to trigger a bunch of bloc events on app init/start, and I like to use cascading add calls on the create method -- much like your code example above.

I was expecting these to be triggered immediately and have all my subsequent events, triggered based on subscriptions also triggered, but they were never triggered :(

I realised I had to add lazy: false to get them to trigger at call time which seems counter intuitive to me. If I want something to be "lazy", in my mind that would be something I would elect to do and not be the default.

@ozburo sorry for the inconvenience! Unfortunately, this behavior is the default for Provider which is used internally by BlocProvider. We could make lazy default to false but it would diverge from the behavior of Provider.

I think lazy being true by default is the right way to go about this, since it's better to deffer as much initialization as possible to have a better startup time.
I feel like the real problem here is people not being aware that this setting exists. Once you know about its existence, you start to see it as a feature rather than inconvenience ๐Ÿ™‚

@ozburo sorry for the inconvenience! Unfortunately, this behavior is the default for Provider which is used internally by BlocProvider. We could make lazy default to false but it would diverge from the behavior of Provider.

I'm sorry too! I'm going to put this down to experience and be happy that I've solved another "gotcha moment".

I guess my issue is that I skipped Provider and went straight to Bloc, so I did't have any "historical reference" of the underlying technology.

I think lazy being true by default is the right way to go about this, since it's better to deffer as much initialization as possible to have a better startup time.

Arguable, from my experience, things that offer deferment, lazy-loading, caching etc are elected not default, but I'll abide.

I just need to test/play some more and figure out when exactly defered Bloc Events get triggered.

I feel like the real problem here is people not being aware that this setting exists. Once you know about its existence, you start to see it as a feature rather than inconvenience ๐Ÿ™‚

More docs are always your friend! ๐Ÿ˜„

And perhaps example(s) of what actually triggers the defered event. My guess is Builders/Consumers do, Subscriptions don't.

Cheers

This is something related to provider as @felangel already mentioned, but there might be room for improving docs, although this info is already present in the provider package.

The bloc gets created once there's a call to BlocProvider.of() or context.bloc() and events will start being processed.

The bloc gets created once there's a call to BlocProvider.of() or context.bloc() and events will start being processed.

Thank you! ๐Ÿ‘

I faced this problem today and I was afraid of what is the reason but when I read this issue I'm happy now, thank you guys.

Was this page helpful?
0 / 5 - 0 ratings