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