firebase / flutterfire

🔥 A collection of Firebase plugins for Flutter apps.
https://firebase.google.com/docs/flutter/setup
BSD 3-Clause "New" or "Revised" License
8.71k stars 3.97k forks source link

[📚] FutureBuilder initialization sample is broken on Flutter 1.20 #3490

Closed domesticmouse closed 3 years ago

domesticmouse commented 4 years ago

I'm attempting to implement the Initializing Flutterfire FutureBuilder approach with Flutter 1.20 and hitting the following exception:

$ flutter --version
Flutter 1.20.3 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 216dee60c0 (7 days ago) • 2020-09-01 12:24:47 -0700
Engine • revision d1bc06f032
Tools • Dart 2.9.2
$ flutter run -d iPhone
Launching lib/main.dart on iPhone 8 in debug mode...
Running Xcode build...                                                  

 └─Compiling, linking and signing...                        13.4s
Xcode build done.                                           36.1s
Waiting for iPhone 8 to report its views...                          4ms
[VERBOSE-2:ui_dart_state.cc(166)] Unhandled Exception: ServicesBinding.defaultBinaryMessenger was accessed before the binding was initialized.
If you're running an application and need to access the binary messenger before `runApp()` has been called (for example, during plugin initialization), then you need to explicitly call the `WidgetsFlutterBinding.ensureInitialized()` first.
If you're running a test, you can call the `TestWidgetsFlutterBinding.ensureInitialized()` as the first line in your test's `main()` method to initialize the binding.
#0      defaultBinaryMessenger.<anonymous closure> (package:flutter/src/services/binary_messenger.dart:93:7)
#1      defaultBinaryMessenger (package:flutter/src/services/binary_messenger.dart:106:4)
#2      MethodChannel.binaryMessenger (package:flutter/src/services/platform_channel.dart:145:62)
#3      MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:151:35)
#4      MethodChannel.invokeMethod (package:flutter/src/services/platfor<…>
Syncing files to device iPhone 8...                                135ms

Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h Repeat this help message.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).
An Observatory debugger and profiler on iPhone 8 is available at: http://127.0.0.1:63589/nxc57idb0bs=/

Application finished.

My main.dart looks like this:

import 'package:flutter/material.dart';

import 'package:firebase_core/firebase_core.dart';

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

class App extends StatelessWidget {
  // Create the initilization Future outside of `build`:
  final Future<FirebaseApp> _initialization = Firebase.initializeApp();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutterfire Demo',
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(
          title: Text('Flutterfire Demo'),
        ),
        body: FutureBuilder(
          // Initialize FlutterFire:
          future: _initialization,
          builder: (context, snapshot) {
            // Check for errors
            if (snapshot.hasError) {
              return Center(child: Text('${snapshot.error}'));
            }

            // Once complete, show your application
            if (snapshot.connectionState != ConnectionState.done) {
              return Center(child: Text('Loading'));
            }

            // Otherwise, show something whilst waiting for initialization to complete
            return Center(child: Text('Success!'));
          },
        ),
      ),
    );
  }
}

While I can work around this by adding WidgetsFlutterBinding.ensureInitialized(); to main, that is an ugly hack. It'd make more sense to only promote the StatefulWidget approach.

domesticmouse commented 4 years ago

/cc @kroikie FYI

TahaTesser commented 4 years ago

I can reproduce the issue This solves it

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(MyApp());
}
domesticmouse commented 4 years ago

I'd prefer not to promote WidgetsFlutterBinding.ensureInitialized(); if at all possible =)

domesticmouse commented 4 years ago

An idea is to add some form of widget to the firebase_auth package that handles initialization along the lines of the following:

class FirebaseInit extends StatefulWidget {
  FirebaseInit({@required this.child});
  final Widget child;

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

class _FirebaseInitState extends State<FirebaseInit> {
  bool _initialized = false;
  String _error;

  void initializeFlutterFire() async {
    try {
      await Firebase.initializeApp();
      setState(() {
        _initialized = true;
      });
    } catch (err) {
      setState(() {
        _error = '$err';
      });
    }
  }

  @override
  void initState() {
    super.initState();
    initializeFlutterFire();
  }

  @override
  Widget build(BuildContext context) {
    if (_error != null) {
      return Center(child: Text(_error));
    }
    if (!_initialized) {
      return Center(child: CircularProgressIndicator());
    }

    return widget.child;
  }
}

The above is just a sketch, and I'm sure that we could harden it (like, say, logging the error message instead of splatting it to screen).

domesticmouse commented 4 years ago

Hey @ditman, am I being over zealous by trying to avoid WidgetsFlutterBinding.ensureInitialized();?

ditman commented 4 years ago

Hey @ditman, am I being over zealous by trying to avoid WidgetsFlutterBinding.ensureInitialized();?

@domesticmouse not sure, but I had similar doubts earlier :) I checked the docs and it seems it can be used exactly for this purpose:

You only need to call this method if you need the binding to be initialized before calling runApp.

(I normally had only seen this used within tests)

It'd be awesome to figure out what "asynchronous" thing your Future is losing its race against and the FutureBuilder case Just Workedâ„¢, I've used a similar approach to make a deferred-loaded app before (note how the ensureInitialized was also added there :/).

domesticmouse commented 4 years ago

I remember WidgetsFlutterBinding.ensureInitialized(); becoming required in various places in about the 1.17 timeframe for various usecases. I'm happy to roll back my stance and just add the ensure initialized call to the sample in the documentation to prevent people hitting it and getting confused at the error.

I'm unsure who to chat with about debugging why this is required. I'm guessing it is something deep in the FFI bindings =)

goderbauer commented 4 years ago

What reservations do you have against calling WidgetsFlutterBinding.ensureInitialized()? It's exactly what runApp does internally as well, there really shouldn't be any harm in doing so.

ditman commented 4 years ago

@goderbauer personally, I had only seen ensureInitialized used in tests before, never had seen a "normal" app calling it directly (but I didn't find anything in the docs advising against it).

domesticmouse commented 4 years ago

Okiday, we just had a meeting on this and calling ensureInitialized in main before runApp is totally ok to do. So if we can get the sample updated as @TahaTesser suggested in https://github.com/FirebaseExtended/flutterfire/issues/3490#issuecomment-688816602 that'd be fantastic =)

ditman commented 4 years ago

I suspect this is the affected file: https://github.com/FirebaseExtended/flutterfire/blame/master/docs/overview.mdx#L67

Ehesp commented 4 years ago

I'm still a bit confused here. Wouldn't this effect other plugins which call a native method immediately? What is FlutterFire doing which causes this issue specifically (when you don't add WidgetsFlutterBinding.ensureInitialized();)?