1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | /// @docImport 'dart:convert'; |
6 | /// @docImport 'dart:ui'; |
7 | /// |
8 | /// @docImport 'package:flutter/rendering.dart'; |
9 | /// @docImport 'package:flutter/scheduler.dart'; |
10 | /// @docImport 'package:flutter/services.dart'; |
11 | /// @docImport 'package:flutter/widgets.dart'; |
12 | /// @docImport 'package:flutter_test/flutter_test.dart'; |
13 | library; |
14 | |
15 | import 'dart:async'; |
16 | import 'dart:convert' show json; |
17 | import 'dart:developer' as developer; |
18 | import 'dart:io' show exit; |
19 | import 'dart:ui' as ui show Brightness, PlatformDispatcher, SingletonFlutterWindow, window; |
20 | |
21 | // Before adding any more dart:ui imports, please read the README. |
22 | |
23 | import 'package:meta/meta.dart' ; |
24 | |
25 | import 'assertions.dart'; |
26 | import 'basic_types.dart'; |
27 | import 'constants.dart'; |
28 | import 'debug.dart'; |
29 | import 'object.dart'; |
30 | import 'platform.dart'; |
31 | import 'print.dart'; |
32 | import 'service_extensions.dart'; |
33 | import 'timeline.dart'; |
34 | |
35 | export 'dart:ui' show PlatformDispatcher, SingletonFlutterWindow, clampDouble; |
36 | |
37 | export 'basic_types.dart' show AsyncCallback, AsyncValueGetter, AsyncValueSetter; |
38 | |
39 | // Examples can assume: |
40 | // mixin BarBinding on BindingBase { } |
41 | |
42 | /// Signature for service extensions. |
43 | /// |
44 | /// The returned map must not contain the keys "type" or "method", as |
45 | /// they will be replaced before the value is sent to the client. The |
46 | /// "type" key will be set to the string `_extensionType` to indicate |
47 | /// that this is a return value from a service extension, and the |
48 | /// "method" key will be set to the full name of the method. |
49 | typedef ServiceExtensionCallback = Future<Map<String, dynamic>> Function(Map<String, String> parameters); |
50 | |
51 | /// Base class for mixins that provide singleton services. |
52 | /// |
53 | /// The Flutter engine ([dart:ui]) exposes some low-level services, |
54 | /// but these are typically not suitable for direct use, for example |
55 | /// because they only provide a single callback which an application |
56 | /// may wish to multiplex to allow multiple listeners. |
57 | /// |
58 | /// Bindings provide the glue between these low-level APIs and the |
59 | /// higher-level framework APIs. They _bind_ the two together, whence |
60 | /// the name. |
61 | /// |
62 | /// ## Implementing a binding mixin |
63 | /// |
64 | /// A library would typically create a new binding mixin to expose a |
65 | /// feature in [dart:ui]. This is rare in general, but it is something |
66 | /// that an alternative framework would do, e.g. if a framework were |
67 | /// to replace the [widgets] library with an alternative API but still |
68 | /// wished to leverage the [services] and [foundation] libraries. |
69 | /// |
70 | /// To create a binding mixin, declare a mixin `on` the [BindingBase] class |
71 | /// and whatever other bindings the concrete binding must implement for |
72 | /// this binding mixin to be useful. |
73 | /// |
74 | /// The mixin is guaranteed to only be constructed once in the |
75 | /// lifetime of the app; this is handled by [initInstances]. |
76 | /// |
77 | /// A binding mixin must at a minimum implement the following features: |
78 | /// |
79 | /// * The [initInstances] method, which must call `super.initInstances` and |
80 | /// set an `_instance` static field to `this`. |
81 | /// * An `instance` static getter, which must return that field using [checkInstance]. |
82 | /// |
83 | /// In addition, it should implement whatever singleton features the library needs. |
84 | /// |
85 | /// As a general rule, the less can be placed in the binding, the |
86 | /// better. Prefer having APIs that takes objects rather than having |
87 | /// them refer to global singletons. Bindings are best limited to |
88 | /// exposing features that literally only exist once, for example, the |
89 | /// APIs in [dart:ui]. |
90 | /// |
91 | /// {@tool snippet} |
92 | /// |
93 | /// Here is a basic example of a binding that implements these features. It relies on |
94 | /// another fictional binding called `BarBinding`. |
95 | /// |
96 | /// ```dart |
97 | /// mixin FooBinding on BindingBase, BarBinding { |
98 | /// @override |
99 | /// void initInstances() { |
100 | /// super.initInstances(); |
101 | /// _instance = this; |
102 | /// // ...binding initialization... |
103 | /// } |
104 | /// |
105 | /// static FooBinding get instance => BindingBase.checkInstance(_instance); |
106 | /// static FooBinding? _instance; |
107 | /// |
108 | /// // ...binding features... |
109 | /// } |
110 | /// ``` |
111 | /// {@end-tool} |
112 | /// |
113 | /// ## Implementing a binding class |
114 | /// |
115 | /// The top-most layer used to write the application (e.g. the Flutter |
116 | /// [widgets] library) will have a concrete class that inherits from |
117 | /// [BindingBase] and uses all the various [BindingBase] mixins (such |
118 | /// as [ServicesBinding]). The [widgets] library in Flutter introduces |
119 | /// a binding called [WidgetsFlutterBinding]. |
120 | /// |
121 | /// A binding _class_ should mix in the relevant bindings from each |
122 | /// layer that it wishes to expose, and should have an |
123 | /// `ensureInitialized` method that constructs the class if that |
124 | /// layer's mixin's `_instance` field is null. This allows the binding |
125 | /// to be overridden by developers who have more specific needs, while |
126 | /// still allowing other code to call `ensureInitialized` when a binding |
127 | /// is needed. |
128 | /// |
129 | /// {@tool snippet} |
130 | /// |
131 | /// A typical binding class is shown below. The `ensureInitialized` method's |
132 | /// return type is the library's binding mixin, rather than the concrete |
133 | /// class. |
134 | /// |
135 | /// ```dart |
136 | /// // continuing from previous example... |
137 | /// class FooLibraryBinding extends BindingBase with BarBinding, FooBinding { |
138 | /// static FooBinding ensureInitialized() { |
139 | /// if (FooBinding._instance == null) { |
140 | /// FooLibraryBinding(); |
141 | /// } |
142 | /// return FooBinding.instance; |
143 | /// } |
144 | /// } |
145 | /// ``` |
146 | /// {@end-tool} |
147 | abstract class BindingBase { |
148 | /// Default abstract constructor for bindings. |
149 | /// |
150 | /// First calls [initInstances] to have bindings initialize their |
151 | /// instance pointers and other state, then calls |
152 | /// [initServiceExtensions] to have bindings initialize their |
153 | /// VM service extensions, if any. |
154 | BindingBase() { |
155 | if (!kReleaseMode) { |
156 | FlutterTimeline.startSync('Framework initialization' ); |
157 | } |
158 | assert(() { |
159 | _debugConstructed = true; |
160 | return true; |
161 | }()); |
162 | |
163 | assert(_debugInitializedType == null, 'Binding is already initialized to $_debugInitializedType' ); |
164 | initInstances(); |
165 | assert(_debugInitializedType != null); |
166 | |
167 | assert(!_debugServiceExtensionsRegistered); |
168 | initServiceExtensions(); |
169 | assert(_debugServiceExtensionsRegistered); |
170 | |
171 | if (!kReleaseMode) { |
172 | developer.postEvent('Flutter.FrameworkInitialization' , <String, String>{}); |
173 | FlutterTimeline.finishSync(); |
174 | } |
175 | } |
176 | |
177 | bool _debugConstructed = false; |
178 | static Type? _debugInitializedType; |
179 | static bool _debugServiceExtensionsRegistered = false; |
180 | |
181 | /// Deprecated. Will be removed in a future version of Flutter. |
182 | /// |
183 | /// This property has been deprecated to prepare for Flutter's upcoming |
184 | /// support for multiple views and multiple windows. |
185 | /// |
186 | /// It represents the main view for applications where there is only one |
187 | /// view, such as applications designed for single-display mobile devices. |
188 | /// If the embedder supports multiple views, it points to the first view |
189 | /// created which is assumed to be the main view. It throws if no view has |
190 | /// been created yet or if the first view has been removed again. |
191 | /// |
192 | /// The following options exists to migrate code that relies on accessing |
193 | /// this deprecated property: |
194 | /// |
195 | /// If a [BuildContext] is available, consider looking up the current |
196 | /// [FlutterView] associated with that context via [View.of]. It gives access |
197 | /// to the same functionality as this deprecated property. However, the |
198 | /// platform-specific functionality has moved to the [PlatformDispatcher], |
199 | /// which may be accessed from the view returned by [View.of] via |
200 | /// [FlutterView.platformDispatcher]. Using [View.of] with a [BuildContext] is |
201 | /// the preferred option to migrate away from this deprecated [window] |
202 | /// property. |
203 | /// |
204 | /// If no context is available to look up a [FlutterView], the |
205 | /// [platformDispatcher] exposed by this binding can be used directly for |
206 | /// platform-specific functionality. It also maintains a list of all available |
207 | /// [FlutterView]s in [PlatformDispatcher.views] to access view-specific |
208 | /// functionality without a context. |
209 | /// |
210 | /// See also: |
211 | /// |
212 | /// * [View.of] to access view-specific functionality on the [FlutterView] |
213 | /// associated with the provided [BuildContext]. |
214 | /// * [FlutterView.platformDispatcher] to access platform-specific |
215 | /// functionality from a given [FlutterView]. |
216 | /// * [platformDispatcher] on this binding to access the [PlatformDispatcher], |
217 | /// which provides platform-specific functionality. |
218 | @Deprecated( |
219 | 'Look up the current FlutterView from the context via View.of(context) or consult the PlatformDispatcher directly instead. ' |
220 | 'Deprecated to prepare for the upcoming multi-window support. ' |
221 | 'This feature was deprecated after v3.7.0-32.0.pre.' |
222 | ) |
223 | ui.SingletonFlutterWindow get window => ui.window; |
224 | |
225 | /// The [ui.PlatformDispatcher] to which this binding is bound. |
226 | /// |
227 | /// A number of additional bindings are defined as extensions of |
228 | /// [BindingBase], e.g., [ServicesBinding], [RendererBinding], and |
229 | /// [WidgetsBinding]. Each of these bindings define behaviors that interact |
230 | /// with a [ui.PlatformDispatcher], e.g., [ServicesBinding] registers |
231 | /// listeners with the [ChannelBuffers], [RendererBinding] |
232 | /// registers [ui.PlatformDispatcher.onMetricsChanged], |
233 | /// [ui.PlatformDispatcher.onTextScaleFactorChanged], and [SemanticsBinding] |
234 | /// registers [ui.PlatformDispatcher.onSemanticsEnabledChanged], |
235 | /// [ui.PlatformDispatcher.onSemanticsActionEvent], and |
236 | /// [ui.PlatformDispatcher.onAccessibilityFeaturesChanged] handlers. |
237 | /// |
238 | /// Each of these other bindings could individually access a |
239 | /// [ui.PlatformDispatcher] statically, but that would preclude the ability to |
240 | /// test these behaviors with a fake platform dispatcher for verification |
241 | /// purposes. Therefore, [BindingBase] exposes this [ui.PlatformDispatcher] |
242 | /// for use by other bindings. A subclass of [BindingBase], such as |
243 | /// [TestWidgetsFlutterBinding], can override this accessor to return a |
244 | /// different [ui.PlatformDispatcher] implementation. |
245 | ui.PlatformDispatcher get platformDispatcher => ui.PlatformDispatcher.instance; |
246 | |
247 | /// The initialization method. Subclasses override this method to hook into |
248 | /// the platform and otherwise configure their services. Subclasses must call |
249 | /// "super.initInstances()". |
250 | /// |
251 | /// The binding is not fully initialized when this method runs (for |
252 | /// example, other binding mixins may not yet have run their |
253 | /// [initInstances] method). For this reason, code in this method |
254 | /// should avoid invoking callbacks or synchronously triggering any |
255 | /// code that would normally assume that the bindings are ready. |
256 | /// |
257 | /// {@tool snippet} |
258 | /// |
259 | /// By convention, if the service is to be provided as a singleton, |
260 | /// it should be exposed as `MixinClassName.instance`, a static |
261 | /// getter with a non-nullable return type that returns |
262 | /// `MixinClassName._instance`, a static field that is set by |
263 | /// `initInstances()`. To improve the developer experience, the |
264 | /// return value should actually be |
265 | /// `BindingBase.checkInstance(_instance)` (see [checkInstance]), as |
266 | /// in the example below. |
267 | /// |
268 | /// ```dart |
269 | /// mixin BazBinding on BindingBase { |
270 | /// @override |
271 | /// void initInstances() { |
272 | /// super.initInstances(); |
273 | /// _instance = this; |
274 | /// // ...binding initialization... |
275 | /// } |
276 | /// |
277 | /// static BazBinding get instance => BindingBase.checkInstance(_instance); |
278 | /// static BazBinding? _instance; |
279 | /// |
280 | /// // ...binding features... |
281 | /// } |
282 | /// ``` |
283 | /// {@end-tool} |
284 | @protected |
285 | @mustCallSuper |
286 | void initInstances() { |
287 | assert(_debugInitializedType == null); |
288 | assert(() { |
289 | _debugInitializedType = runtimeType; |
290 | _debugBindingZone = Zone.current; |
291 | return true; |
292 | }()); |
293 | } |
294 | |
295 | /// A method that shows a useful error message if the given binding |
296 | /// instance is not initialized. |
297 | /// |
298 | /// See [initInstances] for advice on using this method. |
299 | /// |
300 | /// This method either returns the argument or throws an exception. |
301 | /// In release mode it always returns the argument. |
302 | /// |
303 | /// The type argument `T` should be the kind of binding mixin (e.g. |
304 | /// `SchedulerBinding`) that is calling the method. It is used in |
305 | /// error messages. |
306 | @protected |
307 | static T checkInstance<T extends BindingBase>(T? instance) { |
308 | assert(() { |
309 | if (_debugInitializedType == null && instance == null) { |
310 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
311 | ErrorSummary('Binding has not yet been initialized.' ), |
312 | ErrorDescription('The "instance" getter on the $T binding mixin is only available once that binding has been initialized.' ), |
313 | ErrorHint( |
314 | 'Typically, this is done by calling "WidgetsFlutterBinding.ensureInitialized()" or "runApp()" (the ' |
315 | 'latter calls the former). Typically this call is done in the "void main()" method. The "ensureInitialized" method ' |
316 | 'is idempotent; calling it multiple times is not harmful. After calling that method, the "instance" getter will ' |
317 | 'return the binding.' , |
318 | ), |
319 | ErrorHint( |
320 | 'In a test, one can call "TestWidgetsFlutterBinding.ensureInitialized()" as the first line in the test\'s "main()" method ' |
321 | 'to initialize the binding.' , |
322 | ), |
323 | ErrorHint( |
324 | 'If $T is a custom binding mixin, there must also be a custom binding class, like WidgetsFlutterBinding, ' |
325 | 'but that mixes in the selected binding, and that is the class that must be constructed before using the "instance" getter.' , |
326 | ), |
327 | ]); |
328 | } |
329 | if (instance == null) { |
330 | assert(_debugInitializedType == null); |
331 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
332 | ErrorSummary('Binding mixin instance is null but bindings are already initialized.' ), |
333 | ErrorDescription( |
334 | 'The "instance" property of the $T binding mixin was accessed, but that binding was not initialized when ' |
335 | 'the "initInstances()" method was called.' , |
336 | ), |
337 | ErrorHint( |
338 | 'This probably indicates that the $T mixin was not mixed into the class that was used to initialize the binding. ' |
339 | 'If this is a custom binding mixin, there must also be a custom binding class, like WidgetsFlutterBinding, ' |
340 | 'but that mixes in the selected binding. If this is a test binding, check that the binding being initialized ' |
341 | 'is the same as the one into which the test binding is mixed.' , |
342 | ), |
343 | ErrorHint( |
344 | 'It is also possible that $T does not implement "initInstances()" to assign a value to "instance". See the ' |
345 | 'documentation of the BindingBase class for more details.' , |
346 | ), |
347 | ErrorHint( |
348 | 'The binding that was initialized was of the type " $_debugInitializedType". ' |
349 | ), |
350 | ]); |
351 | } |
352 | try { |
353 | if (instance._debugConstructed && _debugInitializedType == null) { |
354 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
355 | ErrorSummary('Binding initialized without calling initInstances.' ), |
356 | ErrorDescription('An instance of $T is non-null, but BindingBase.initInstances() has not yet been called.' ), |
357 | ErrorHint( |
358 | 'This could happen because a binding mixin was somehow used outside of the normal binding mechanisms, or because ' |
359 | 'the binding\'s initInstances() method did not call "super.initInstances()".' , |
360 | ), |
361 | ErrorHint( |
362 | 'This could also happen if some code was invoked that used the binding while the binding was initializing, ' |
363 | 'for example if the "initInstances" method invokes a callback. Bindings should not invoke callbacks before ' |
364 | '"initInstances" has completed.' , |
365 | ), |
366 | ]); |
367 | } |
368 | if (!instance._debugConstructed) { |
369 | // The state of _debugInitializedType doesn't matter in this failure mode. |
370 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
371 | ErrorSummary('Binding did not complete initialization.' ), |
372 | ErrorDescription('An instance of $T is non-null, but the BindingBase() constructor has not yet been called.' ), |
373 | ErrorHint( |
374 | 'This could also happen if some code was invoked that used the binding while the binding was initializing, ' |
375 | "for example if the binding's constructor itself invokes a callback. Bindings should not invoke callbacks " |
376 | 'before "initInstances" has completed.' , |
377 | ), |
378 | ]); |
379 | } |
380 | } on NoSuchMethodError { |
381 | throw FlutterError.fromParts(<DiagnosticsNode>[ |
382 | ErrorSummary('Binding does not extend BindingBase' ), |
383 | ErrorDescription('An instance of $T was created but the BindingBase constructor was not called.' ), |
384 | ErrorHint( |
385 | 'This could happen because the binding was implemented using "implements" rather than "extends" or "with". ' |
386 | 'Concrete binding classes must extend or mix in BindingBase.' , |
387 | ), |
388 | ]); |
389 | } |
390 | return true; |
391 | }()); |
392 | return instance!; |
393 | } |
394 | |
395 | /// In debug builds, the type of the current binding, if any, or else null. |
396 | /// |
397 | /// This may be useful in asserts to verify that the binding has not been initialized |
398 | /// before the point in the application code that wants to initialize the binding, or |
399 | /// to verify that the binding is the one that is expected. |
400 | /// |
401 | /// For example, if an application uses [Zone]s to report uncaught exceptions, it may |
402 | /// need to ensure that `ensureInitialized()` has not yet been invoked on any binding |
403 | /// at the point where it configures the zone and initializes the binding. |
404 | /// |
405 | /// If this returns null, the binding has not been initialized. |
406 | /// |
407 | /// If this returns a non-null value, it returns the type of the binding instance. |
408 | /// |
409 | /// To obtain the binding itself, consider the `instance` getter on the [BindingBase] |
410 | /// subclass or mixin. |
411 | /// |
412 | /// This method only returns a useful value in debug builds. In release builds, the |
413 | /// return value is always null; to improve startup performance, the type of the |
414 | /// binding is not tracked in release builds. |
415 | /// |
416 | /// See also: |
417 | /// |
418 | /// * [BindingBase], whose class documentation describes the conventions for dealing |
419 | /// with bindings. |
420 | /// * [initInstances], whose documentation details how to create a binding mixin. |
421 | static Type? debugBindingType() { |
422 | return _debugInitializedType; |
423 | } |
424 | |
425 | Zone? _debugBindingZone; |
426 | |
427 | /// Whether [debugCheckZone] should throw (true) or just report the error (false). |
428 | /// |
429 | /// Setting this to true makes it easier to catch cases where the zones are |
430 | /// misconfigured, by allowing debuggers to stop at the point of error. |
431 | /// |
432 | /// Currently this defaults to false, to avoid suddenly breaking applications |
433 | /// that are affected by this check but appear to be working today. Applications |
434 | /// are encouraged to resolve any issues that cause the [debugCheckZone] message |
435 | /// to appear, as even if they appear to be working today, they are likely to be |
436 | /// hiding hard-to-find bugs, and are more brittle (likely to collect bugs in |
437 | /// the future). |
438 | /// |
439 | /// To silence the message displayed by [debugCheckZone], ensure that the same |
440 | /// zone is used when calling `ensureInitialized()` as when calling the framework |
441 | /// in any other context (e.g. via [runApp]). |
442 | static bool debugZoneErrorsAreFatal = false; |
443 | |
444 | /// Checks that the current [Zone] is the same as that which was used |
445 | /// to initialize the binding. |
446 | /// |
447 | /// If the current zone ([Zone.current]) is not the zone that was active when |
448 | /// the binding was initialized, then this method generates a [FlutterError] |
449 | /// exception with detailed information. The exception is either thrown |
450 | /// directly, or reported via [FlutterError.reportError], depending on the |
451 | /// value of [BindingBase.debugZoneErrorsAreFatal]. |
452 | /// |
453 | /// To silence the message displayed by [debugCheckZone], ensure that the same |
454 | /// zone is used when calling `ensureInitialized()` as when calling the |
455 | /// framework in any other context (e.g. via [runApp]). For example, consider |
456 | /// keeping a reference to the zone used to initialize the binding, and using |
457 | /// [Zone.run] to use it again when calling into the framework. |
458 | /// |
459 | /// ## Usage |
460 | /// |
461 | /// The binding is considered initialized once [BindingBase.initInstances] has |
462 | /// run; if this is called before then, it will throw an [AssertionError]. |
463 | /// |
464 | /// The `entryPoint` parameter is the name of the API that is checking the |
465 | /// zones are consistent, for example, `'runApp'`. |
466 | /// |
467 | /// This function always returns true (if it does not throw). It is expected |
468 | /// to be invoked via the binding instance, e.g.: |
469 | /// |
470 | /// ```dart |
471 | /// void startup() { |
472 | /// WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized(); |
473 | /// assert(binding.debugCheckZone('startup')); |
474 | /// // ... |
475 | /// } |
476 | /// ``` |
477 | /// |
478 | /// If the binding expects to be used with multiple zones, it should override |
479 | /// this method to return true always without throwing. (For example, the |
480 | /// bindings used with [flutter_test] do this as they make heavy use of zones |
481 | /// to drive the framework with an artificial clock and to catch errors and |
482 | /// report them as test failures.) |
483 | bool debugCheckZone(String entryPoint) { |
484 | assert(() { |
485 | assert(_debugBindingZone != null, 'debugCheckZone can only be used after the binding is fully initialized.' ); |
486 | if (Zone.current != _debugBindingZone) { |
487 | final Error message = FlutterError( |
488 | 'Zone mismatch.\n' |
489 | 'The Flutter bindings were initialized in a different zone than is now being used. ' |
490 | 'This will likely cause confusion and bugs as any zone-specific configuration will ' |
491 | 'inconsistently use the configuration of the original binding initialization zone ' |
492 | 'or this zone based on hard-to-predict factors such as which zone was active when ' |
493 | 'a particular callback was set.\n' |
494 | 'It is important to use the same zone when calling `ensureInitialized` on the binding ' |
495 | 'as when calling ` $entryPoint` later.\n' |
496 | 'To make this ${ debugZoneErrorsAreFatal ? 'error non-fatal' : 'warning fatal' }, ' |
497 | 'set BindingBase.debugZoneErrorsAreFatal to ${!debugZoneErrorsAreFatal} before the ' |
498 | 'bindings are initialized (i.e. as the first statement in `void main() { }`).' , |
499 | ); |
500 | if (debugZoneErrorsAreFatal) { |
501 | throw message; |
502 | } |
503 | FlutterError.reportError(FlutterErrorDetails( |
504 | exception: message, |
505 | stack: StackTrace.current, |
506 | context: ErrorDescription('during $entryPoint' ), |
507 | )); |
508 | } |
509 | return true; |
510 | }()); |
511 | return true; |
512 | } |
513 | |
514 | /// Called when the binding is initialized, to register service |
515 | /// extensions. |
516 | /// |
517 | /// Bindings that want to expose service extensions should overload |
518 | /// this method to register them using calls to |
519 | /// [registerSignalServiceExtension], |
520 | /// [registerBoolServiceExtension], |
521 | /// [registerNumericServiceExtension], and |
522 | /// [registerServiceExtension] (in increasing order of complexity). |
523 | /// |
524 | /// Implementations of this method must call their superclass |
525 | /// implementation. |
526 | /// |
527 | /// {@macro flutter.foundation.BindingBase.registerServiceExtension} |
528 | /// |
529 | /// See also: |
530 | /// |
531 | /// * <https://github.com/dart-lang/sdk/blob/main/runtime/vm/service/service.md#rpcs-requests-and-responses> |
532 | @protected |
533 | @mustCallSuper |
534 | void initServiceExtensions() { |
535 | assert(!_debugServiceExtensionsRegistered); |
536 | |
537 | assert(() { |
538 | registerSignalServiceExtension( |
539 | name: FoundationServiceExtensions.reassemble.name, |
540 | callback: reassembleApplication, |
541 | ); |
542 | return true; |
543 | }()); |
544 | |
545 | if (!kReleaseMode) { |
546 | if (!kIsWeb) { |
547 | registerSignalServiceExtension( |
548 | name: FoundationServiceExtensions.exit.name, |
549 | callback: _exitApplication, |
550 | ); |
551 | } |
552 | // These service extensions are used in profile mode applications. |
553 | registerStringServiceExtension( |
554 | name: FoundationServiceExtensions.connectedVmServiceUri.name, |
555 | getter: () async => connectedVmServiceUri ?? '' , |
556 | setter: (String uri) async { |
557 | connectedVmServiceUri = uri; |
558 | }, |
559 | ); |
560 | registerStringServiceExtension( |
561 | name: FoundationServiceExtensions.activeDevToolsServerAddress.name, |
562 | getter: () async => activeDevToolsServerAddress ?? '' , |
563 | setter: (String serverAddress) async { |
564 | activeDevToolsServerAddress = serverAddress; |
565 | }, |
566 | ); |
567 | } |
568 | |
569 | assert(() { |
570 | registerServiceExtension( |
571 | name: FoundationServiceExtensions.platformOverride.name, |
572 | callback: (Map<String, String> parameters) async { |
573 | if (parameters.containsKey('value' )) { |
574 | final String value = parameters['value' ]!; |
575 | debugDefaultTargetPlatformOverride = null; |
576 | for (final TargetPlatform candidate in TargetPlatform.values) { |
577 | if (candidate.name == value) { |
578 | debugDefaultTargetPlatformOverride = candidate; |
579 | break; |
580 | } |
581 | } |
582 | _postExtensionStateChangedEvent( |
583 | FoundationServiceExtensions.platformOverride.name, |
584 | defaultTargetPlatform.name, |
585 | ); |
586 | await reassembleApplication(); |
587 | } |
588 | return <String, dynamic>{ |
589 | 'value' : defaultTargetPlatform.name, |
590 | }; |
591 | }, |
592 | ); |
593 | |
594 | registerServiceExtension( |
595 | name: FoundationServiceExtensions.brightnessOverride.name, |
596 | callback: (Map<String, String> parameters) async { |
597 | if (parameters.containsKey('value' )) { |
598 | debugBrightnessOverride = switch (parameters['value' ]) { |
599 | 'Brightness.light' => ui.Brightness.light, |
600 | 'Brightness.dark' => ui.Brightness.dark, |
601 | _ => null, |
602 | }; |
603 | _postExtensionStateChangedEvent( |
604 | FoundationServiceExtensions.brightnessOverride.name, |
605 | (debugBrightnessOverride ?? platformDispatcher.platformBrightness).toString(), |
606 | ); |
607 | await reassembleApplication(); |
608 | } |
609 | return <String, dynamic>{ |
610 | 'value' : (debugBrightnessOverride ?? platformDispatcher.platformBrightness).toString(), |
611 | }; |
612 | }, |
613 | ); |
614 | return true; |
615 | }()); |
616 | assert(() { |
617 | _debugServiceExtensionsRegistered = true; |
618 | return true; |
619 | }()); |
620 | } |
621 | |
622 | /// Whether [lockEvents] is currently locking events. |
623 | /// |
624 | /// Binding subclasses that fire events should check this first, and if it is |
625 | /// set, queue events instead of firing them. |
626 | /// |
627 | /// Events should be flushed when [unlocked] is called. |
628 | @protected |
629 | bool get locked => _lockCount > 0; |
630 | int _lockCount = 0; |
631 | |
632 | /// Locks the dispatching of asynchronous events and callbacks until the |
633 | /// callback's future completes. |
634 | /// |
635 | /// This causes input lag and should therefore be avoided when possible. It is |
636 | /// primarily intended for use during non-user-interactive time such as to |
637 | /// allow [reassembleApplication] to block input while it walks the tree |
638 | /// (which it partially does asynchronously). |
639 | /// |
640 | /// The [Future] returned by the `callback` argument is returned by [lockEvents]. |
641 | /// |
642 | /// The [gestures] binding wraps [PlatformDispatcher.onPointerDataPacket] in |
643 | /// logic that honors this event locking mechanism. Similarly, tasks queued |
644 | /// using [SchedulerBinding.scheduleTask] will only start when events are not |
645 | /// [locked]. |
646 | @protected |
647 | Future<void> lockEvents(Future<void> Function() callback) { |
648 | developer.TimelineTask? debugTimelineTask; |
649 | if (!kReleaseMode) { |
650 | debugTimelineTask = developer.TimelineTask()..start('Lock events' ); |
651 | } |
652 | |
653 | _lockCount += 1; |
654 | final Future<void> future = callback(); |
655 | future.whenComplete(() { |
656 | _lockCount -= 1; |
657 | if (!locked) { |
658 | if (!kReleaseMode) { |
659 | debugTimelineTask!.finish(); |
660 | } |
661 | try { |
662 | unlocked(); |
663 | } catch (error, stack) { |
664 | FlutterError.reportError(FlutterErrorDetails( |
665 | exception: error, |
666 | stack: stack, |
667 | library: 'foundation' , |
668 | context: ErrorDescription('while handling pending events' ), |
669 | )); |
670 | } |
671 | } |
672 | }); |
673 | return future; |
674 | } |
675 | |
676 | /// Called by [lockEvents] when events get unlocked. |
677 | /// |
678 | /// This should flush any events that were queued while [locked] was true. |
679 | @protected |
680 | @mustCallSuper |
681 | void unlocked() { |
682 | assert(!locked); |
683 | } |
684 | |
685 | /// Cause the entire application to redraw, e.g. after a hot reload. |
686 | /// |
687 | /// This is used by development tools when the application code has changed, |
688 | /// to cause the application to pick up any changed code. It can be triggered |
689 | /// manually by sending the `ext.flutter.reassemble` service extension signal. |
690 | /// |
691 | /// This method is very computationally expensive and should not be used in |
692 | /// production code. There is never a valid reason to cause the entire |
693 | /// application to repaint in production. All aspects of the Flutter framework |
694 | /// know how to redraw when necessary. It is only necessary in development |
695 | /// when the code is literally changed on the fly (e.g. in hot reload) or when |
696 | /// debug flags are being toggled. |
697 | /// |
698 | /// While this method runs, events are locked (e.g. pointer events are not |
699 | /// dispatched). |
700 | /// |
701 | /// Subclasses (binding classes) should override [performReassemble] to react |
702 | /// to this method being called. This method itself should not be overridden. |
703 | Future<void> reassembleApplication() { |
704 | return lockEvents(performReassemble); |
705 | } |
706 | |
707 | /// This method is called by [reassembleApplication] to actually cause the |
708 | /// application to reassemble, e.g. after a hot reload. |
709 | /// |
710 | /// Bindings are expected to use this method to re-register anything that uses |
711 | /// closures, so that they do not keep pointing to old code, and to flush any |
712 | /// caches of previously computed values, in case the new code would compute |
713 | /// them differently. For example, the rendering layer triggers the entire |
714 | /// application to repaint when this is called. |
715 | /// |
716 | /// Do not call this method directly. Instead, use [reassembleApplication]. |
717 | @mustCallSuper |
718 | @protected |
719 | Future<void> performReassemble() { |
720 | FlutterError.resetErrorCount(); |
721 | return Future<void>.value(); |
722 | } |
723 | |
724 | /// Registers a service extension method with the given name (full |
725 | /// name "ext.flutter.name"), which takes no arguments and returns |
726 | /// no value. |
727 | /// |
728 | /// Calls the `callback` callback when the service extension is called. |
729 | /// |
730 | /// {@macro flutter.foundation.BindingBase.registerServiceExtension} |
731 | @protected |
732 | void registerSignalServiceExtension({ |
733 | required String name, |
734 | required AsyncCallback callback, |
735 | }) { |
736 | registerServiceExtension( |
737 | name: name, |
738 | callback: (Map<String, String> parameters) async { |
739 | await callback(); |
740 | return <String, dynamic>{}; |
741 | }, |
742 | ); |
743 | } |
744 | |
745 | /// Registers a service extension method with the given name (full |
746 | /// name "ext.flutter.name"), which takes a single argument |
747 | /// "enabled" which can have the value "true" or the value "false" |
748 | /// or can be omitted to read the current value. (Any value other |
749 | /// than "true" is considered equivalent to "false". Other arguments |
750 | /// are ignored.) |
751 | /// |
752 | /// Calls the `getter` callback to obtain the value when |
753 | /// responding to the service extension method being called. |
754 | /// |
755 | /// Calls the `setter` callback with the new value when the |
756 | /// service extension method is called with a new value. |
757 | /// |
758 | /// {@macro flutter.foundation.BindingBase.registerServiceExtension} |
759 | @protected |
760 | void registerBoolServiceExtension({ |
761 | required String name, |
762 | required AsyncValueGetter<bool> getter, |
763 | required AsyncValueSetter<bool> setter, |
764 | }) { |
765 | registerServiceExtension( |
766 | name: name, |
767 | callback: (Map<String, String> parameters) async { |
768 | if (parameters.containsKey('enabled' )) { |
769 | await setter(parameters['enabled' ] == 'true' ); |
770 | _postExtensionStateChangedEvent(name, await getter() ? 'true' : 'false' ); |
771 | } |
772 | return <String, dynamic>{'enabled' : await getter() ? 'true' : 'false' }; |
773 | }, |
774 | ); |
775 | } |
776 | |
777 | /// Registers a service extension method with the given name (full |
778 | /// name "ext.flutter.name"), which takes a single argument with the |
779 | /// same name as the method which, if present, must have a value |
780 | /// that can be parsed by [double.parse], and can be omitted to read |
781 | /// the current value. (Other arguments are ignored.) |
782 | /// |
783 | /// Calls the `getter` callback to obtain the value when |
784 | /// responding to the service extension method being called. |
785 | /// |
786 | /// Calls the `setter` callback with the new value when the |
787 | /// service extension method is called with a new value. |
788 | /// |
789 | /// {@macro flutter.foundation.BindingBase.registerServiceExtension} |
790 | @protected |
791 | void registerNumericServiceExtension({ |
792 | required String name, |
793 | required AsyncValueGetter<double> getter, |
794 | required AsyncValueSetter<double> setter, |
795 | }) { |
796 | registerServiceExtension( |
797 | name: name, |
798 | callback: (Map<String, String> parameters) async { |
799 | if (parameters.containsKey(name)) { |
800 | await setter(double.parse(parameters[name]!)); |
801 | _postExtensionStateChangedEvent(name, (await getter()).toString()); |
802 | } |
803 | return <String, dynamic>{name: (await getter()).toString()}; |
804 | }, |
805 | ); |
806 | } |
807 | |
808 | /// Sends an event when a service extension's state is changed. |
809 | /// |
810 | /// Clients should listen for this event to stay aware of the current service |
811 | /// extension state. Any service extension that manages a state should call |
812 | /// this method on state change. |
813 | /// |
814 | /// `value` reflects the newly updated service extension value. |
815 | /// |
816 | /// This will be called automatically for service extensions registered via |
817 | /// [registerBoolServiceExtension], [registerNumericServiceExtension], or |
818 | /// [registerStringServiceExtension]. |
819 | void _postExtensionStateChangedEvent(String name, dynamic value) { |
820 | postEvent( |
821 | 'Flutter.ServiceExtensionStateChanged' , |
822 | <String, dynamic>{ |
823 | 'extension' : 'ext.flutter. $name' , |
824 | 'value' : value, |
825 | }, |
826 | ); |
827 | } |
828 | |
829 | /// All events dispatched by a [BindingBase] use this method instead of |
830 | /// calling [developer.postEvent] directly so that tests for [BindingBase] |
831 | /// can track which events were dispatched by overriding this method. |
832 | /// |
833 | /// This is unrelated to the events managed by [lockEvents]. |
834 | @protected |
835 | void postEvent(String eventKind, Map<String, dynamic> eventData) { |
836 | developer.postEvent(eventKind, eventData); |
837 | } |
838 | |
839 | /// Registers a service extension method with the given name (full name |
840 | /// "ext.flutter.name"), which optionally takes a single argument with the |
841 | /// name "value". If the argument is omitted, the value is to be read, |
842 | /// otherwise it is to be set. Returns the current value. |
843 | /// |
844 | /// Calls the `getter` callback to obtain the value when |
845 | /// responding to the service extension method being called. |
846 | /// |
847 | /// Calls the `setter` callback with the new value when the |
848 | /// service extension method is called with a new value. |
849 | /// |
850 | /// {@macro flutter.foundation.BindingBase.registerServiceExtension} |
851 | @protected |
852 | void registerStringServiceExtension({ |
853 | required String name, |
854 | required AsyncValueGetter<String> getter, |
855 | required AsyncValueSetter<String> setter, |
856 | }) { |
857 | registerServiceExtension( |
858 | name: name, |
859 | callback: (Map<String, String> parameters) async { |
860 | if (parameters.containsKey('value' )) { |
861 | await setter(parameters['value' ]!); |
862 | _postExtensionStateChangedEvent(name, await getter()); |
863 | } |
864 | return <String, dynamic>{'value' : await getter()}; |
865 | }, |
866 | ); |
867 | } |
868 | |
869 | /// Registers a service extension method with the given name (full name |
870 | /// "ext.flutter.name"). |
871 | /// |
872 | /// The given callback is called when the extension method is called. The |
873 | /// callback must return a [Future] that either eventually completes to a |
874 | /// return value in the form of a name/value map where the values can all be |
875 | /// converted to JSON using `json.encode()` (see [JsonEncoder]), or fails. In |
876 | /// case of failure, the failure is reported to the remote caller and is |
877 | /// dumped to the logs. |
878 | /// |
879 | /// The returned map will be mutated. |
880 | /// |
881 | /// {@template flutter.foundation.BindingBase.registerServiceExtension} |
882 | /// A registered service extension can only be activated if the vm-service |
883 | /// is included in the build, which only happens in debug and profile mode. |
884 | /// Although a service extension cannot be used in release mode its code may |
885 | /// still be included in the Dart snapshot and blow up binary size if it is |
886 | /// not wrapped in a guard that allows the tree shaker to remove it (see |
887 | /// sample code below). |
888 | /// |
889 | /// {@tool snippet} |
890 | /// The following code registers a service extension that is only included in |
891 | /// debug builds. |
892 | /// |
893 | /// ```dart |
894 | /// void myRegistrationFunction() { |
895 | /// assert(() { |
896 | /// // Register your service extension here. |
897 | /// return true; |
898 | /// }()); |
899 | /// } |
900 | /// ``` |
901 | /// {@end-tool} |
902 | /// |
903 | /// {@tool snippet} |
904 | /// A service extension registered with the following code snippet is |
905 | /// available in debug and profile mode. |
906 | /// |
907 | /// ```dart |
908 | /// void myOtherRegistrationFunction() { |
909 | /// // kReleaseMode is defined in the 'flutter/foundation.dart' package. |
910 | /// if (!kReleaseMode) { |
911 | /// // Register your service extension here. |
912 | /// } |
913 | /// } |
914 | /// ``` |
915 | /// {@end-tool} |
916 | /// |
917 | /// Both guards ensure that Dart's tree shaker can remove the code for the |
918 | /// service extension in release builds. |
919 | /// {@endtemplate} |
920 | @protected |
921 | void registerServiceExtension({ |
922 | required String name, |
923 | required ServiceExtensionCallback callback, |
924 | }) { |
925 | final String methodName = 'ext.flutter. $name' ; |
926 | developer.registerExtension(methodName, (String method, Map<String, String> parameters) async { |
927 | assert(method == methodName); |
928 | assert(() { |
929 | if (debugInstrumentationEnabled) { |
930 | debugPrint('service extension method received: $method( $parameters)' ); |
931 | } |
932 | return true; |
933 | }()); |
934 | |
935 | // VM service extensions are handled as "out of band" messages by the VM, |
936 | // which means they are handled at various times, generally ASAP. |
937 | // Notably, this includes being handled in the middle of microtask loops. |
938 | // While this makes sense for some service extensions (e.g. "dump current |
939 | // stack trace", which explicitly doesn't want to wait for a loop to |
940 | // complete), Flutter extensions need not be handled with such high |
941 | // priority. Further, handling them with such high priority exposes us to |
942 | // the possibility that they're handled in the middle of a frame, which |
943 | // breaks many assertions. As such, we ensure they we run the callbacks |
944 | // on the outer event loop here. |
945 | await debugInstrumentAction<void>('Wait for outer event loop' , () { |
946 | return Future<void>.delayed(Duration.zero); |
947 | }); |
948 | |
949 | late Map<String, dynamic> result; |
950 | try { |
951 | result = await callback(parameters); |
952 | } catch (exception, stack) { |
953 | FlutterError.reportError(FlutterErrorDetails( |
954 | exception: exception, |
955 | stack: stack, |
956 | context: ErrorDescription('during a service extension callback for " $method"' ), |
957 | )); |
958 | return developer.ServiceExtensionResponse.error( |
959 | developer.ServiceExtensionResponse.extensionError, |
960 | json.encode(<String, String>{ |
961 | 'exception' : exception.toString(), |
962 | 'stack' : stack.toString(), |
963 | 'method' : method, |
964 | }), |
965 | ); |
966 | } |
967 | result['type' ] = '_extensionType' ; |
968 | result['method' ] = method; |
969 | return developer.ServiceExtensionResponse.result(json.encode(result)); |
970 | }); |
971 | } |
972 | |
973 | @override |
974 | String toString() => '< ${objectRuntimeType(this, 'BindingBase' )}>' ; |
975 | } |
976 | |
977 | /// Terminate the Flutter application. |
978 | Future<void> _exitApplication() async { |
979 | exit(0); |
980 | } |
981 | |