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

Storage was accessed before it was initialized [HydratedBloc] #3244

Closed MichelMelhem closed 2 years ago

MichelMelhem commented 2 years ago

Hello, I recently updated the HydratedBloc lib from 7.0.0 to 8.0.0. With this new version, the new way of initializing the storage is by using HydratedBlocOverrides.runZoned. But i'm facing issues with underlying app behaviors not working the same while using this new API. In fact fact i'm consistently getting the exception

Storage was accessed before it was initialized.
Please ensure that storage has been initialized.

While hot reloading my app from the vscode DevTool. This is problematic as it block me in developing my app, each time i hot reload it i get an exception popping.

After some investigation i think that i found was was broken in the new version of Hydrated Bloc. My findings are that in version 7.0.0 of hydrated bloc you could use BlocProvider this way without any issues :

   CounterBloc bloc = CounterBloc();
    return BlocProvider<CounterBloc>(
      create: (_) => bloc,
      child: CounterView(),
    );

In the new version 8.0.0 we are forced to do :

    return BlocProvider<CounterBloc>(
      create: (_) => CounterBloc(),
      child: CounterView(),
    );

If you use the first syntax, each time you reload the app using hydrated storage 8.0.0 you will get the StorageNotFound error.

Here you will be able to find a modified version of the main.dart of the Hydrated Bloc example project that reproduces the bug : https://pastebin.com/RLjUJVXq

Why this bug is problematic ?

With this bug you can't do something like this anymore :

Widget build(BuildContext context) {
    _authBloc = AuthBloc();
    _userBloc = UserBloc(_authBloc);
    return MaterialApp(
        home: MultiBlocProvider(providers: [
      BlocProvider<AuthBloc>(create: (BuildContext context) => _authBloc),
      BlocProvider<UserBloc>(create: (BuildContext context) => _userBloc),
    ], child: Root()));
  }

Flutter doctor output:

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 2.10.2, on macOS 12.2.1 21D62 darwin-arm, locale en-FR)
[✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 13.2.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2021.1)
[✓] Android Studio (version 2020.3)
[✓] VS Code (version 1.64.2)
[✓] Connected device (2 available)
[✓] HTTP Host Availability

• No issues found!

Thank you for your help, Michel M

felangel commented 2 years ago

Hi @MichelMelhem 👋 Thanks for opening an issue!

This is due to a bug in Flutter https://github.com/flutter/flutter/issues/93676. It would be awesome if you could leave a 👍 on the issue.

Sorry for the inconvenience!

MichelMelhem commented 2 years ago

Hello @felangel , Thanks for your answer ! As it is a flutter related issue would you consider adding back, even as a deprecated function, the old way of initializing the storage ? In fact I updated all my blocs to be compatible with version 8 of the library with the new syntax so it is not possible for me to revert back my app.

MichelMelhem commented 2 years ago

Hi @felangel , any updates on this issue ?

KanybekMomukeyev commented 2 years ago

Absolutely same here, any update on this issue or temporary solution how to avoid/fix it?

macik1423 commented 2 years ago

I am also waiting for any solution.

aaassseee commented 2 years ago

Try out the temporary solution which is suggested by @felangel. You can go through the example code on hydrated bloc in here. Or just add flutter_services_binding to pubspec.yaml, and replace WidgetsFlutterBinding.ensureInitialized(); with FlutterServicesBinding.ensureInitialized();. This solution works for me and I am currently testing it.

felangel commented 2 years ago

The flutter_services_binding was a solution but it’s only temporary (changes in flutter have made it not feasible on beta and master channels). I’m currently working on an alternative solution, sorry for the inconvenience!

felangel commented 2 years ago

You can use the 9.0.0-dev version of hydrated_bloc (via the createStorage api) now:

For example:

HydratedBlocOverrides.runZoned(
    () => runApp(WeatherApp(weatherRepository: WeatherRepository())),
    blocObserver: WeatherBlocObserver(),
    createStorage: () async {
      WidgetsFlutterBinding.ensureInitialized();
      return HydratedStorage.build(
        storageDirectory: kIsWeb
            ? HydratedStorage.webStorageDirectory
            : await getTemporaryDirectory(),
      );
    },
  );
}

It's important not to call WidgetsFluitterBinding.ensureInitialized outside of the HydratedBlocOverrides zone.

Closing for now but feel free to comment with any questions and I'm happy to take a closer look.

rahulrmishra commented 2 years ago

Hey @felangel

I am using firebase, hive and service locators, and the hydrated bloc. So I'm not sure how to initialize other services and create storage together. Any combination that I tried threw errors.

This is my code before using hydrated bloc

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  await initialiseServices();
  if (Platform.isAndroid) {
    await AndroidInAppWebViewController.setWebContentsDebuggingEnabled(true);
  }
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
  FlutterError.onError = (FlutterErrorDetails details) {
    debugPrint("=================== CAUGHT FLUTTER ERROR $details");
  };
  runZonedGuarded(() {
    BlocOverrides.runZoned(
      () => runApp(const App()),
      blocObserver: SimpleBlocObserver(),
    );
  }, FirebaseCrashlytics.instance.recordError);
}
felangel commented 2 years ago

@rahulrmishra can you share a link to a minimal reproduction sample?

minhdanh commented 2 years ago

Hi @felangel, I'm having the similar problem like @rahulrmishra. I have this error if I try to initialize Firebase before using WidgetsFlutterBinding.ensureInitialized();

E/flutter ( 8283): [ERROR:flutter/lib/ui/ui_dart_state.cc(198)] Unhandled Exception: Binding has not yet been initialized.
E/flutter ( 8283): The "instance" getter on the ServicesBinding binding mixin is only available once that binding has been initialized.
E/flutter ( 8283): Typically, this is done by calling "WidgetsFlutterBinding.ensureInitialized()" or "runApp()" (the latter calls the former). Typically this call is done in the "void main()" method. The "ensureInitialized" method is idempotent; calling it multiple times is not harmful. After calling that method, the "instance" getter will return the binding.

I have created a sample here: https://github.com/minhdanh/flutter-hydrated-bloc-firebase/blob/master/lib/main.dart

aaassseee commented 2 years ago

@minhdanh You also needs to call WidgetsFlutterBinding.ensureInitialized(); before await Firebase.initializeApp(); So you call WidgetsFlutterBinding.ensureInitialized(); twice in total.

minhdanh commented 2 years ago

Thank you @aaassseee.

@felangel I use your example with a minor change and could see that instead of using:

    return BlocProvider(
      create: (_) => BrightnessCubit(),
      child: AppView(),
    );

when I use:

    final brightnessCubit = BrightnessCubit();

    return BlocProvider.value(
      value: brightnessCubit,
      child: AppView(),
    );

(I use BlocProvider.value to reuse brightnessCubit somewhere else)

I'll have the error Storage was accessed before it was initialized when the code is reloaded. Why this happens and what can I do about this? I updated my sample repo in case you want to have a look.

Best regards

aaassseee commented 2 years ago

@minhdanh I think the zone get rebuild when hot reload. Because you are initing storage inside the create storage function. so at that moment the storage getting create when zone rebuild. I suggest you init the hydrated storage before zone creation.

minhdanh commented 2 years ago

Hi @aaassseee, I made the change as you suggested but it's still same error for me:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final storage = await HydratedStorage.build(
    storageDirectory: kIsWeb
        ? HydratedStorage.webStorageDirectory
        : await getTemporaryDirectory(),
  );

  HydratedBlocOverrides.runZoned(
    () => runApp(App()),
    storage: storage,
  );
}

As mentioned in my previous comment, I think there's something with BlocProvider.value that causing this error.

minhdanh commented 2 years ago

I think it's not a problem with BlocProvider.value but with the way the bloc is created outside of BlocProvider(). If I add:

+ final brightnessCubit = BrightnessCubit();

return BlocProvider(
      create: (_) => BrightnessCubit(),
      child: AppView(),
    );

I'll also have the error when hot reloading. Still investigating

aaassseee commented 2 years ago

@minhdanh May I ask what is the use case with init cubit outside of BlocProvider? I think this is anti pattern with bloc design spec.

minhdanh commented 2 years ago

@aaassseee What I'm trying to do is to reuse a bloc I declared (bloc 1), and pass it to another bloc (bloc 2) so that in bloc 2 I can listen for the events of bloc 1 and react accordingly. Please see following example:

      final _themeSettingsBloc = ThemeSettingsBloc();

      return MultiBlocProvider(
        providers: [
          BlocProvider.value(value: _themeSettingsBloc),
          BlocProvider<UserSettingsBloc>(
            lazy: false,
            create: (BuildContext context) => UserSettingsBloc(
              themeSettingsBloc: _themeSettingsBloc,
            ),
          ),
        ],
        child: const AppView(),
      ),

Is there any problem with this approach? :sweat_smile:

aaassseee commented 2 years ago

@minhdanh You can actually use context.read to get the ThemeSettingsBloc

minhdanh commented 2 years ago

@aaassseee Thank you so much. I changed my code like you suggested:

      return MultiBlocProvider(
        providers: [
          BlocProvider(create: (_) => ThemeSettingsBloc()),
          BlocProvider<UserSettingsBloc>(
            lazy: false,
            create: (BuildContext context) => UserSettingsBloc(
              themeSettingsBloc: context.read<ThemeSettingsBloc>(),
            ),
          ),
        ],
        child: const AppView(),
      ),

Now I don't have the error when reloading any more.

Best regards!

aaassseee commented 2 years ago

@minhdanh Happy to help

farisbasha commented 2 years ago

wha

@minhdanh Happy to help

What about when using get_it injection for bloc... i still have this annoying storage error🥲🥲

aaassseee commented 2 years ago

@farisbasha Any example code?

farisbasha commented 2 years ago

@farisbasha Any example code?

HydratedBlocOverrides.runZoned(
        () async => runApp(await builder()),
        blocObserver: AppBlocObserver(),
        createStorage: () async {
          WidgetsFlutterBinding.ensureInitialized();
          await SystemChrome.setPreferredOrientations([
            DeviceOrientation.portraitDown,
            DeviceOrientation.portraitUp,
          ]);
          getIt.registerSingleton<AppRouter>(AppRouter());
          configureDependencies(env);
          return HydratedStorage.build(
            storageDirectory: kIsWeb
                ? HydratedStorage.webStorageDirectory
                : await getApplicationDocumentsDirectory(),
          );
        },
      );

And Bloc Provider Look like

MultiBlocProvider(
      providers: [
        BlocProvider<CoreBloc>(
          lazy: false,
          create: (context) => getIt<CoreBloc>(),
        ),
        BlocProvider<AuthBloc>(
          lazy: false,
          create: (ctx) => getIt<AuthBloc>(),
        ),

      ],

Both CoreBloc and AuthBloc are HydatedBloc

This error occur whenever i hot relode

Storage was accessed before it was initialized.
I/flutter (13213): Please ensure that storage has been initialized.
I/flutter (13213):
I/flutter (13213): For example:
I/flutter (13213):
I/flutter (13213): HydratedBlocOverrides.runZoned(
I/flutter (13213):   () => runApp(MyApp()),
I/flutter (13213):   createStorage: () => HydratedStorage.build(...),
I/flutter (13213): );
aaassseee commented 2 years ago

@farisbasha It feels anti pattern to me. Can you show me more code about where you construct the bloc with get_it? I didn't see where you init the bloc, for example CoreBloc. Also, if you show me a runnable example would help me a lot to identify the problem.

jabari-pulliam commented 2 years ago

For anyone interested, I had the same problem as @farisbasha and I found that the solution is just to call the getIt injector (I'm using injectable) inside the body of runZoned so that the storage field on HydratedBlocOverrides is set prior to the blocs being built by the injector.

Ex:

Future<void> main() async {
  HydratedBlocOverrides.runZoned(
    () async {
      await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
      configureDependencies("dev");
      runApp(MyApp(init: getIt.allReady));
    },
    storage: await _buildStorage(),
  );
}

Future<HydratedStorage> _buildStorage() async {
  WidgetsFlutterBinding.ensureInitialized();
  final storage = await HydratedStorage.build(
    storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(),
  );
  return storage;
}
felangel commented 2 years ago

I highly recommend migrating to hydrated_bloc 9.0.0-dev.3 which moved away from the zone-based api