Repository navigation
Flutter framework does not warn when ensureInitialized is called in a different zone than runApp #94123
Description
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
[-]Add FlutterServicesBinding.ensureInitialized[/-][+][Proposal] Add FlutterServicesBinding.ensureInitialized[/+]dnfield commentedon Dec 1, 2021
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.
felangel commentedon Dec 1, 2021
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)?
dnfield commentedon Dec 1, 2021
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).
felangel commentedon Dec 1, 2021
Yeah that could work.
The main reason why I suggested this is because in my experience 99% of the time when
WidgetsFlutterBinding.ensureInitializedis used, it’s because we want to communicate with the platform. It’d be much more intuitive/descriptive to have aFlutterServicesBindingto make it clear that the services are initialized and nothing else.It would also be much simpler to transition from:
To:
As opposed to:
felangel commentedon Jan 4, 2022
@dnfield are there any updates on this? This is also affecting integration tests due to
IntegrationTestWidgetsFlutterBindingsetting up the bindings in the root zone as well.dnfield commentedon Jan 11, 2022
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
runAppcall that setter with its current zone?felangel commentedon Jan 11, 2022
No, this isn't just to get it working with hot reload (that is #93676).
This is aimed at addressing the issue of
zoneValuesnot being available in parts of the application (even without a hot reload) when you callWidgetsFlutterBinding.ensureInitialized()(orIntegrationTestWidgetsFlutterBinding.ensureInitialized())in the root zone.A simple use case is wanting to provide the temporary directory as a zoneValue to the entire flutter application:
dnfield commentedon Jan 11, 2022
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.
74 remaining items
Assert that runApp is called in the same zone as binding.ensureInitia…
yaakovschectman commentedon Mar 16, 2023
Reverted by #122830, reopening for now
Assert that runApp is called in the same zone as binding.ensureInitia…
TheGlorySaint commentedon Mar 20, 2023
I'm on the Master Branch. After I updated the flutter version, my console logs me this error:
I didn't change anything in my main.dart dart file. At the top there is the
WidgetsFlutterBinding.ensureInitialized();danagbemava-nc commentedon Mar 20, 2023
Hi @TheGlorySaint, kindly file a new issue and provide detailed info about how to reproduce the issue. Thanks
github-actions commentedon Apr 3, 2023
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 -vand a minimal reproduction of the issue.