Skip to content

Flutter framework does not warn when ensureInitialized is called in a different zone than runApp #94123

Description

@felangel

Use case

If a developer wants to call runApp within a custom Zone with zoneValues and the zoneValues rely on the underlying platform, they must call WidgetsFlutterBinding.ensureInitialized() like:

final token = Object();
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final value = await _getPlatformValue();
  runZoned(
    () => runApp(MyApp()),
    zoneValues: {token: value},
  );
}

This appears to work as expected, however, because WidgetsFlutterBinding also initializes other bindings like GestureBinding the zoneValue is not accessible within onPressed callbacks:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print(Zone.current[token]); // OK
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: ElevatedButton(
            onPressed: () {
              print(Zone.current[token]); // NULL
            },
            child: Text('Click Me'),
          ),
        ),
      ),
    );
  }
}

Proposal

It would be great to have a way to initialize the ServicesBinding and SchedulerBinding independently of the remaining bindings in order to support accessing data from the underlying platform before runApp but without initializing the WidgetsBinding.instance within the root zone.

final token = Object();
void main() async {
  FlutterServicesBinding.ensureInitialized();
  final value = await _getPlatformValue();
  runZoned(
    () => runApp(MyApp()),
    zoneValues: {token: value},
  );
}
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print(Zone.current[token]); // OK
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: ElevatedButton(
            onPressed: () {
              print(Zone.current[token]); // OK
            },
            child: Text('Click Me'),
          ),
        ),
      ),
    );
  }
}

Where FlutterServicesBinding could look something like:

FlutterServicesBinding Sample
import 'dart:ui' as ui show SingletonFlutterWindow, PlatformDispatcher, window;

import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';

class FlutterServicesBinding extends _StandaloneBindingBase
    with SchedulerBinding, ServicesBinding {
  FlutterServicesBinding._();

  static FlutterServicesBinding? _instance;

  static FlutterServicesBinding ensureInitialized() {
    return _instance ??= FlutterServicesBinding._();
  }
}

abstract class _StandaloneBindingBase implements BindingBase {
  _StandaloneBindingBase() {
    initInstances();
    window.onReportTimings = null;
  }

  @override
  ui.SingletonFlutterWindow get window => ui.window;

  @override
  ui.PlatformDispatcher get platformDispatcher {
    return ui.PlatformDispatcher.instance;
  }

  @override
  @protected
  @mustCallSuper
  void initInstances() {}

  @override
  @protected
  @mustCallSuper
  void initServiceExtensions() {}

  @override
  @protected
  bool get locked => false;

  @override
  @protected
  Future<void> lockEvents(Future<void> Function() callback) async {}

  @override
  @protected
  @mustCallSuper
  void unlocked() {}

  @override
  Future<void> reassembleApplication() async {}

  @override
  @mustCallSuper
  @protected
  Future<void> performReassemble() async {}

  @override
  @protected
  void registerSignalServiceExtension({
    required String name,
    required AsyncCallback callback,
  }) {}

  @override
  @protected
  void registerBoolServiceExtension({
    required String name,
    required AsyncValueGetter<bool> getter,
    required AsyncValueSetter<bool> setter,
  }) {}

  @override
  @protected
  void registerNumericServiceExtension({
    required String name,
    required AsyncValueGetter<double> getter,
    required AsyncValueSetter<double> setter,
  }) {}

  @override
  @protected
  void postEvent(String eventKind, Map<String, dynamic> eventData) {}

  @override
  @protected
  void registerStringServiceExtension({
    required String name,
    required AsyncValueGetter<String> getter,
    required AsyncValueSetter<String> setter,
  }) {}

  @override
  @protected
  void registerServiceExtension({
    required String name,
    required ServiceExtensionCallback callback,
  }) {}

  @override
  String toString() => '<${objectRuntimeType(this, 'BindingBase')}>';
}

DartPad Demos

  • DartPad illustrating the limitation when using WidgetsFlutterBinding.ensureInitialized();

  • DartPad illustrating an example of how this can be improved via FlutterServicesBinding.ensureInitialized();

Additional Info

Published https://pub.dev/packages/flutter_services_binding as a workaround in the meantime.

Activity

  1. changed the title [-]Add FlutterServicesBinding.ensureInitialized[/-] [+][Proposal] Add FlutterServicesBinding.ensureInitialized[/+] on Nov 24, 2021
  2. added
    frameworkflutter/packages/flutter repository. See also f: labels.
    c: proposalA detailed proposal for a change to Flutter
    c: new featureNothing broken; request for a new capability
    and removed
    in triagePresently being triaged by the triage team
    on Nov 24, 2021
  3. dnfield commented on Dec 1, 2021

    @dnfield
    Contributor

    What happens if you just initialize the WidgetsFlutterBinding inside your zone?

    I think that's the right solution here. Refactoring the bindings classes to be separately initializable would be non-trivial.

  4. felangel commented on Dec 1, 2021

    @felangel
    ContributorAuthor

    What happens if you just initialize the WidgetsFlutterBinding inside your zone?

    I think that's the right solution here. Refactoring the bindings classes to be separately initializable would be non-trivial.

    That doesn’t work when the zoneValues rely on WidgetsFlutterBinding.ensureInitialized to have been called.

    For example, if I want to include the temporaryApplicationDirectory path in zoneValues, how would we provide the value to zoneValues without calling WidgetsFlutterBinding.ensureInitialized outside of the zone (see DartPad above)?

  5. dnfield commented on Dec 1, 2021

    @dnfield
    Contributor

    Ahh ok. But perhaps another way to solve this would be to add a zone to run service extension callbacks in, e.g. ensure initialized(zone: myZone).

  6. felangel commented on Dec 1, 2021

    @felangel
    ContributorAuthor

    Ahh ok. But perhaps another way to solve this would be to add a zone to run service extension callbacks in, e.g. ensure initialized(zone: myZone).

    Yeah that could work.

    The main reason why I suggested this is because in my experience 99% of the time when WidgetsFlutterBinding.ensureInitialized is used, it’s because we want to communicate with the platform. It’d be much more intuitive/descriptive to have a FlutterServicesBinding to make it clear that the services are initialized and nothing else.

    It would also be much simpler to transition from:

    void main() {
      WidgetFlutterBinding.ensureInitialized();
      runZoned(() {...}, zoneValues: {...});
    }

    To:

    void main() {
      FlutterServicesBinding.ensureInitialized();
      runZoned(() {...}, zoneValues: {...});
    }

    As opposed to:

    void main() {
      WidgetsFlutterBinding.ensureInitialized(
        Zone.current.fork(zoneValues: {...})..run(() {...}),
      );
    }
  7. felangel commented on Jan 4, 2022

    @felangel
    ContributorAuthor

    @dnfield are there any updates on this? This is also affecting integration tests due to IntegrationTestWidgetsFlutterBinding setting up the bindings in the root zone as well.

  8. dnfield commented on Jan 11, 2022

    @dnfield
    Contributor

    All of this is with the idea to make sure hot reload works in the correct zone right?

    What if we just had a setter for that, and make runApp call that setter with its current zone?

  9. felangel commented on Jan 11, 2022

    @felangel
    ContributorAuthor

    All of this is with the idea to make sure hot reload works in the correct zone right?

    What if we just had a setter for that, and make runApp call that setter with its current zone?

    No, this isn't just to get it working with hot reload (that is #93676).

    This is aimed at addressing the issue of zoneValues not being available in parts of the application (even without a hot reload) when you call WidgetsFlutterBinding.ensureInitialized() (or IntegrationTestWidgetsFlutterBinding.ensureInitialized()) in the root zone.

    A simple use case is wanting to provide the temporary directory as a zoneValue to the entire flutter application:

    import 'dart:async';
    
    import 'package:flutter/material.dart';
    import 'package:path_provider/path_provider.dart';
    
    final token = Object();
    
    Future<void> main() async {
      WidgetsFlutterBinding.ensureInitialized();
      final tempDirectory = await getTemporaryDirectory();
      runZoned(() => runApp(const MyApp()), zoneValues: {token: tempDirectory});
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Scaffold(
            body: Center(child: Text('ZoneValue: ${Zone.current[token]}')),
            floatingActionButton: Builder(
              builder: (context) {
                return FloatingActionButton(
                  onPressed: () {
                    ScaffoldMessenger.of(context)
                      ..hideCurrentSnackBar()
                      ..showSnackBar(
                        SnackBar(
                          content: Text(
                            'ZoneValue: ${Zone.current[token]}',
                          ),
                        ),
                      );
                  },
                );
              },
            ),
          ),
        );
      }
    }
  10. dnfield commented on Jan 11, 2022

    @dnfield
    Contributor

    But couldn't we similarly settle this issue for other callbacks?

    My concern here is that the bindings are already a bit of a complicated mess of singletons. If we try to split up initialization of them such that partial initialization is possible, it'll make for a confusing spaghetti API that will be difficult to reason about as your application grows. You'll have different dependencies expecting initialization to happen in conflicting orders and we'll be right back here.

  11. 74 remaining items

  12. yaakovschectman commented on Mar 16, 2023

    @yaakovschectman
    Contributor

    Reverted by #122830, reopening for now

  13. TheGlorySaint commented on Mar 20, 2023

    @TheGlorySaint

    I'm on the Master Branch. After I updated the flutter version, my console logs me this error:

    The Flutter bindings were initialized in a different zone than is now being used. This will likely cause confusion and bugs as any zone-specific configuration will inconsistently use the configuration of the original binding initialization zone or this zone based on hard-to-predict factors such as which zone was active when a particular callback was set.
    It is important to use the same zone when calling `ensureInitialized` on the binding as when calling `runApp` later.
    To make this warning fatal, set BindingBase.debugZoneErrorsAreFatal to true before the bindings are initialized (i.e. as the first statement in `void main() { }`).
    When the exception was thrown, this was the stack
    #0      BindingBase.debugCheckZone.<anonymous closure>
    binding.dart:484
    #1      BindingBase.debugCheckZone
    binding.dart:489
    #2      runApp
    binding.dart:1062
    #3      main.<anonymous closure>
    main.dart:118
    #8      main
    main.dart:117
    <asynchronous suspension>
    (elided 4 frames from dart:async)
    ════════════════════════════════════════════════════════════════════════════════
    

    I didn't change anything in my main.dart dart file. At the top there is the WidgetsFlutterBinding.ensureInitialized();

  14. danagbemava-nc commented on Mar 20, 2023

    @danagbemava-nc
    Member

    Hi @TheGlorySaint, kindly file a new issue and provide detailed info about how to reproduce the issue. Thanks

  15. github-actions commented on Apr 3, 2023

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

  16. locked as resolved and limited conversation to collaborators on Apr 3, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P1High-priority issues at the top of the work listc: new featureNothing broken; request for a new capabilityc: proposalA detailed proposal for a change to Fluttercustomer: crowdAffects or could affect many people, though not necessarily a specific customer.frameworkflutter/packages/flutter repository. See also f: labels.r: fixedIssue is closed as already fixed in a newer version

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions