Bindings

Запуск каждого Flutter-приложения начинается с вызова функции runApp. Давайте посмотрим на её реализацию.

1void runApp(Widget app) {
2  final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();
3  assert(binding.debugCheckZone('runApp'));
4  binding
5    ..scheduleAttachRootWidget(binding.wrapWithDefaultView(app))
6    ..scheduleWarmUpFrame();
7}

В функции происходят три важных действия:

  1. Получается и инициализируется, если необходимо, объект binding с типом WidgetsBinding.
  2. Вызывается метод scheduleAttachRootWidget, в который передаётся переданный в функцию виджет.
  3. Откладывается операция отрисовки первого кадра — scheduleWarmUpFrame.

Давайте сфокусируемся на первом действии и разберёмся, какую роль оно играет в запуске нашего приложения.

Bindings (сервисы связи) — это некоторые связующие классы, выполняющие роль «клея» между движком и фреймворком. А если говорить формальным языком — интерфейсы обмена данными между Flutter framework и Flutter engine.

По правде говоря, это не чистая связь между кодом движка на C++ и Dart-кодом, а связь между PlatformDispatcher из dart:ui и более высокоуровневыми слоями фреймворка, своего рода набор фасадов над PlatformDispatcher.

Базовый класс для всех bindings — BindingBase. Конкретные сервисы наследуются от BindingBase. Они обязуются гарантировать единственность своего экземпляра и инициализируются только один раз (реализуют паттерн «Синглтон»). Каждый сервис отделяет в себе обработку ограниченного набора задач, связанных непосредственно с ним. В сервисе GestureBinding, например, обрабатываются задачи, связанные со взаимодействием пользователя с экраном устройства.

Всего во Flutter девять наследников класса BindingBase:

Bindings связаны между собой определённой структурой: одни сервисы связи также являются миксинами над другими, более низкоуровневыми. Зависимости между ними можно представить следующей схемой:

flt

Давайте взглянем на каждый сервис связи и его задачи подробнее.

SchedulerBinding

Главная задача этого сервиса — планировка задач, связанных с отрисовкой кадра. Например:

  1. Вызовы преходящих задач (transientCallbacks), которые инициирует система в методе Window.onBeginFrame. Например, события тикеров и контроллеров анимации.
  2. Не связанные с рендерингом задачи (midFrameMicrotasks), которые должны быть выполнены между кадрами. То есть микротаски, запланированные преходящими задачами. Это может быть очистка очереди событий обработанных жестов, обработка скролла. Микротаски выполняются между подготовкой к новому кадру и его отрисовкой.
  3. Вызовы непрерывных задач (persistentCallbacks), которые инициирует система в методе Window.onDrawFrame. В частности, это вызов метода build у виджета или layout у рендер-объекта.
  4. Задачи, вызываемые после отрисовки кадра (postFrameCallbacks). Обычно это задачи, которые не могут выполниться в процессе рендеринга, например отправка семантических событий (изменение фокуса) или очистка кеша изображений.

Одновременно SchedulerBinding может работать только с одним типом задач, обработка идёт в том порядке, в котором мы перечислили их выше.

Узнать, какую задачу в данный момент обрабатывает SchedulerBinding, можно с помощью геттера schedulerPhase, который возвращает состояние SchedulerPhase.

Далее поговорим о том, где будет полезен SchedulerBinding и его методы.

addPostFrameCallback и endOfFrame

Очень часто нам нужно выполнить код после окончания рендеринга текущего кадра, когда станут доступны размеры всех виджетов: например, чтобы получить размер виджета, который не известен заранее.

Для этого мы можем воспользоваться двумя способами — методом addPostFrameCallback или геттером endOfFrame. Вот так:

1  import 'package:flutter/material.dart';
2  import 'package:flutter/scheduler.dart';
3
4  void main() {
5    runApp(const MyApp());
6  }
7
8  class MyApp extends StatelessWidget {
9    const MyApp({Key? key}) : super(key: key);
10
11    @override
12    Widget build(BuildContext context) {
13      return const MaterialApp(
14        debugShowCheckedModeBanner: false,
15        home: TextGeometryExample(),
16      );
17    }
18  }
19
20  class TextGeometryExample extends StatefulWidget {
21    const TextGeometryExample({super.key});
22
23    @override
24    State<TextGeometryExample> createState() => _TextGeometryExampleState();
25  }
26
27  class _TextGeometryExampleState extends State<TextGeometryExample> {
28    final GlobalKey textKey = GlobalKey();
29    Size size = const Size(0, 0);
30
31    @override
32    void initState() {
33      super.initState();
34
35      /// Добавляем [postFrameCallback] в [SchedulerBinding]
36      /// Он вызовется после отрисовки текущего кадра, в частности после
37      /// отрисовки текста с [GlobalKey] = [textKey].
38      ///
39      /// Размеры виджета text будут известны
40      /// в момент исполнения [_changeAnimatedContainerDimensions],
41      /// тк он вызывается ПОСЛЕ отрисовки кадра, т.е. размер текста будет посчитан
42      SchedulerBinding.instance
43          .addPostFrameCallback(_changeAnimatedContainerDimensions);
44
45      /// Аналогичного результата можно достичь используя Future api
46      /// и геттер [endOfFrame] у [SchedulerBinding].
47      ///
48      /// Future будет завершен тогда, когда завершится отрисовка текущего кадра
49      ///
50      /// Пример:
51      /// SchedulerBinding.instance.endOfFrame
52      ///     .then((_) => _changeAnimatedContainerDimensions());
53    }
54
55    void _changeAnimatedContainerDimensions(
56        [Duration? postframeCallbackDuration]) {
57      /// Получаем [RenderObject] который относится к виджету Text.
58      /// После отрисовки в нем есть информация о геометрии виджета.
59      RenderBox logoBox = textKey.currentContext!.findRenderObject() as RenderBox;
60
61      /// Получаем размер
62      size = Size(
63        logoBox.size.width + 5,
64        logoBox.size.height + 5,
65      );
66
67      /// Обновляем стейт. Изменения размера [size] спровоцирует анимацию у [AnimatedContainer]
68      setState(() {});
69    }
70
71    @override
72    Widget build(BuildContext context) {
73      /// [Stack] используется намеренно, чтобы продемонстрировать возможность
74      /// получить размер виджета, расположенного параллельно в дереве.
75      ///
76      /// Данного эффекта можно добиться и не используя [postFrameCallback]
77      return Scaffold(
78        body: Stack(
79          children: [
80            /// Контейнер с конкретным размером. Ожидается что в переменной size
81            /// будет размер текста, расположенного ниже по стеку.
82            ///
83            /// Текст в виджете ниже может быть произвольный и на момент верстки
84            /// мы не можем точно знать его размер.
85            Center(
86              child: AnimatedContainer(
87                duration: const Duration(seconds: 2),
88                curve: Curves.bounceInOut,
89                width: size.width,
90                height: size.height,
91                color: Colors.amber,
92              ),
93            ),
94            Center(
95              child: Text(
96                key: textKey,
97                'Динамический текст',
98                style: const TextStyle(fontSize: 20),
99              ),
100            ),
101          ],
102        ),
103      );
104    }
105  }

Ещё один вариант использования SchedulerBinding — возможность наблюдать за метриками отрисовки вашего приложения.

Наблюдение за производительностью

С помощью SchedulerBinding вы можете наблюдать за производительностью вашего приложения, используя следующие методы:

1// [FrameTiming] — объект с информацией о кадре
2typedef TimingsCallback = void Function(List<FrameTiming> timings);
3
4// Добавить [TimingsCallback]
5void addTimingsCallback(TimingsCallback callback)
6
7// Удалить [TimingsCallback]
8void removeTimingsCallback(TimingsCallback callback)

Здесь FrameTiming — объект, который содержит метрики отрисовки кадра, такие как:

  • длительность фазы build
  • длительность фазы отрисовки на GPU

Эти метрики собираются в батчи и отправляются примерно раз в секунду в release-режиме сборки. Разработчики Flutter утверждают, что за каждый зарегистрированный TimingsCallback использование процессора вырастет примерно на 0,01%, — это замедляет перформанс приложения, и нужно пользоваться этим функционалом с осторожностью.

Батч (англ. batch) — набор данных, собранный в группу. Это позволяет не отправлять данные часто и экономить ресурсы при их частой отправке.

Пример использования TimingsCallback:

1import 'dart:async';
2
3import 'package:flutter/material.dart';
4import 'package:flutter/scheduler.dart';
5
6void main() {
7  runApp(const MyApp());
8}
9
10/// Виджет приложения
11class MyApp extends StatelessWidget {
12  const MyApp([Key? key]) : super(key: key);
13
14  @override
15  Widget build(BuildContext context) {
16    return const MaterialApp(
17      debugShowCheckedModeBanner: false,
18      home: Scaffold(
19        body: PerformanceMeasurePage(title: 'Performance measure'),
20      ),
21    );
22  }
23}
24
25/// Страница со сбором метрики рендеринга
26class PerformanceMeasurePage extends StatefulWidget {
27  const PerformanceMeasurePage({super.key, required this.title});
28
29  final String title;
30
31  @override
32  State<PerformanceMeasurePage> createState() => _PerformanceMeasurePageState();
33}
34
35class _PerformanceMeasurePageState extends State<PerformanceMeasurePage> {
36  /// Показывать или нет [CircularProgressIndicator]
37  /// [CircularProgressIndicator] это постоянная анимация
38  /// Здесь он используется чтобы намеренно создать нагрузку на рендеринг
39  bool showProgressIndicator = false;
40
41  /// [StreamController] для событий TimingsCallback.
42  late final StreamController<FrameTiming> _frameTimingsStreamController;
43
44  @override
45  void initState() {
46    super.initState();
47
48    /// Инициализация [StreamController]
49    _frameTimingsStreamController = StreamController<FrameTiming>.broadcast();
50
51    /// Регистрируем [TimingsCallback] в [SchedulerBinding]
52    /// Лучше не использовать анонимную функцию, тк есть риск потерять на нее ссылку и не удалить обработчик
53    /// Поэтому используется внутренний метод, ссылка на который для объекта состояния не поменяется
54    SchedulerBinding.instance.addTimingsCallback(_onTimingsCallback);
55  }
56
57  @override
58  void dispose() {
59    /// Удаляем [TimingsCallback] чтобы не создавать дополнительную нагрузку на CPU
60    SchedulerBinding.instance.removeTimingsCallback(_onTimingsCallback);
61    _frameTimingsStreamController.close();
62    super.dispose();
63  }
64
65  @override
66  Widget build(BuildContext context) {
67    return Scaffold(
68      appBar: AppBar(
69        title: Text(widget.title),
70      ),
71      body: Center(
72        child: Column(
73          mainAxisAlignment: MainAxisAlignment.start,
74          crossAxisAlignment: CrossAxisAlignment.center,
75          children: <Widget>[
76            const Spacer(flex: 3),
77            NonAnimatedButton(
78              onTap: _onCallSetstateButtonTapped,
79              text: 'Вызвать setState',
80            ),
81            const SizedBox(height: 20),
82            NonAnimatedButton(
83              onTap: _onShowAnimationButtonTapped,
84              text: 'Показать CircularProgressIndicator',
85            ),
86            const SizedBox(height: 20),
87            if (showProgressIndicator) const CircularProgressIndicator(),
88            const Spacer(),
89            AverageFrameStats(
90              frameTiming: _frameTimingsStreamController.stream,
91            ),
92            const Spacer(flex: 3)
93          ],
94        ),
95      ),
96    );
97  }
98
99  void _onCallSetstateButtonTapped() {
100    setState(() {});
101  }
102
103  void _onShowAnimationButtonTapped() {
104    setState(() {
105      showProgressIndicator = !showProgressIndicator;
106    });
107  }
108
109  void _onTimingsCallback(List<FrameTiming> timings) {
110    for (final timing in timings) {
111      // Добавление в streamController через add для примера
112      // В реальном приложении лучше предусмотреть механизм тротлинга
113      // элементов [FrameTiming] перед вызовом метода add, если есть необходимость использовать Stream Api
114      // Может вызывать джанки т.к. потенциально создает много микро-задач (microtasks)
115      _frameTimingsStreamController.add(timing);
116    }
117    _reportTimings(timings);
118  }
119
120  void _reportTimings(List<FrameTiming> timings) {
121    // Можно отправлять аналитику про длительность кадров
122    // Например, используя AppMetrica
123    //
124    // Обычно требуется какая-то минимальная пред-обработка:
125    // Отправлять огромное количество метрики про каждый кадр не рационально,
126    // Лучше собрать какую-то статистику сессии на устройстве и отправить ее
127  }
128}
129
130/// Кнопка без анимаций.
131/// Стандартные кнопки из библиотеки Material используют [InkWell]
132/// Это создает лишнюю нагрузку на рендеринг, а мы хотим посмотреть как 
133/// работает [TimingsCallback] без лишнего шума
134class NonAnimatedButton extends StatelessWidget {
135  final VoidCallback onTap;
136  final String text;
137  const NonAnimatedButton({
138    required this.onTap,
139    required this.text,
140    Key? key,
141  }) : super(key: key);
142
143  @override
144  Widget build(BuildContext context) {
145    return GestureDetector(
146      onTap: onTap,
147      child: DecoratedBox(
148        decoration: const BoxDecoration(
149          color: Colors.lightBlue,
150        ),
151        child: SizedBox(
152          width: 130,
153          height: 56,
154          child: Center(child: Text(text)),
155        ),
156      ),
157    );
158  }
159}
160
161/// Виджет со сбором статистики про фреймы.
162/// Получает на вход поток данных [FrameTiming] и агрегирует их в статистику
163class AverageFrameStats extends StatefulWidget {
164  final Stream<FrameTiming> frameTiming;
165  const AverageFrameStats({required this.frameTiming, Key? key})
166      : super(key: key);
167
168  @override
169  State<AverageFrameStats> createState() => _AverageFrameStatsState();
170}
171
172class _AverageFrameStatsState extends State<AverageFrameStats> {
173  StreamSubscription<FrameTiming>? _framesStreamSub;
174
175  int currentFrame = 0;
176  int maxBuildDurationMs = 0;
177  int maxRasterDurationMs = 0;
178
179  @override
180  void initState() {
181    _framesStreamSub = widget.frameTiming.listen(_onFrameEvent);
182    super.initState();
183  }
184
185  @override
186  void didUpdateWidget(covariant AverageFrameStats oldWidget) {
187    if (oldWidget.frameTiming != widget.frameTiming) {
188      _framesStreamSub?.cancel();
189      _framesStreamSub = widget.frameTiming.listen(_onFrameEvent);
190    }
191    super.didUpdateWidget(oldWidget);
192  }
193
194  @override
195  void dispose() {
196    _framesStreamSub?.cancel();
197    super.dispose();
198  }
199
200  @override
201  Widget build(BuildContext context) {
202    return DecoratedBox(
203      decoration: BoxDecoration(
204        border: Border.all(
205          color: Colors.grey,
206          width: 2,
207        ),
208      ),
209      child: Padding(
210        padding: const EdgeInsets.all(20.0),
211        child: Column(
212          children: [
213            Text('Номер текущего кадра $currentFrame'),
214            const SizedBox(height: 10),
215            Text(
216              'Макс. продолжительность сборки кадра в UI $maxBuildDurationMs мс',
217            ),
218            const SizedBox(height: 10),
219            Text(
220              'Макс. продолжительность растеризации $maxRasterDurationMs мс',
221            )
222          ],
223        ),
224      ),
225    );
226  }
227  
228  void _onFrameEvent(FrameTiming timing) {
229    currentFrame = timing.frameNumber;
230    if (timing.buildDuration.inMilliseconds > maxBuildDurationMs) {
231      maxBuildDurationMs = timing.buildDuration.inMilliseconds;
232    }
233    if (timing.rasterDuration.inMilliseconds > maxRasterDurationMs) {
234      maxRasterDurationMs = timing.rasterDuration.inMilliseconds;
235    }
236    setState(() {});
237  }
238}

Сборка мусора

С помощью сервиса SchedulerBinding можно управлять стратегией работы сборщика мусора.

1PerformanceModeRequestHandle? requestPerformanceMode(DartPerformanceMode mode)

Метод просит его перейти в определённый DartPerformanceMode. Существуют четыре режима работы:

  • balanced — стандартный режим работы, идеально оптимизированный для Flutter;
  • latency — снижение времени задержек за счёт увеличения накладных расходов на память; не рекомендуется находиться в этом режиме длительное время;
  • throughput — увеличение пропускной способности за счёт увеличения задержек на обработку;
  • memory — оптимизация для работы в условиях низкой доступной памяти, работает чаще с большим объёмом данных, что понижает перформанс.

На выходе вы получаете nullable-объект PerformanceModeRequestHandle, который используется для вывода из установленного DartPerformanceMode: нужно вызвать метод dispose для освобождения ресурсов. Если возвращается null, значит, в данный момент какой-то другой код запросил режим работы другого типа.

Используйте requestPerformanceMode для оптимизации только в том случае, если проблемы производительности приложения возникают из-за сборщика мусора. Помните: это крайняя мера, если другие оптимизации не помогли.

И ещё один совет: всегда замеряйте метрики производительности до и после изменений. Подробнее о производительности приложения — в параграфе Профилирование: Flutter DevTools.

ServicesBinding

Вот за что он отвечает:

  • Прослушивание и перенаправление платформенных сообщений в BinaryMessenger, сервис, к которому по умолчанию привязываются платформенные каналы: каналы методов (MethodChannel) и событий (EventChannel). При получении очередного сообщения BinaryMessenger перенаправляет его в соответствующий платформенный канал. BinaryMessenger умеет не только получать, но и отправлять сообщения в платформу. Подробнее о нём вы можете почитать в параграфе Channels.
  • Сбор и регистрация лицензий пакетов, которые были в приложении в качестве зависимостей. Лицензии пакетов зашиваются в приложении во время его сборки инструментами Flutter.
  • Сохранение ссылки на токен главного изолята. Он может использоваться, если необходимо общаться через платформенные каналы из сторонних изолятов. Подробнее об этом вы прочитаете в параграфе Advanced изоляты и зоны, асинхронное и параллельное программирование.
  • Обработка системных событий, которые идут от платформы. Например, запрос на выход из приложения, жизненный цикл приложения, событие out of memory, нажатия клавиатуры и др.
  • Создание RestorationManager — это сущность, которая отвечает за восстановление состояния приложения. Про него подробно рассказывали в лекции про persistence Школы мобильной разработки Яндекса.

GestureBinding

Главная обязанность сервиса GestureBinding — это обработка взаимодействия пользователя с экраном устройства, то есть обработка жестов.

Получаемые на вход данные о нажатиях доставляются конкретным потребителям этих событий (кнопки, области со скроллом и т. д.). Процесс распознавания адресата для события называется hitTest, результат распознавания — hitTestResult.

GestureBinding умеет кешировать hitTestResult для большей эффективности. Помимо обработки событий со стороны устройства, GestureBinding также открывает возможность посылать «ложные» события нажатий, что используется в TestWidgetsFlutterBinding.
Подробнее про hitTest вы можете прочитать в параграфе RenderObject.

RendererBinding

Этот сервис — связующее между деревом RenderObject и Flutter engine. У него две основные обязанности:

  • Прослушивание событий от engine для информирования об изменении настроек устройства, которые могут затрагивать семантический слой или как-то влиять на визуальное представление вашего приложения (например, тёмная тема или размер текста).
  • Передача во Flutter engine изменений на экране с помощью Layer tree. Подробнее — в параграфе RenderObject.

Для того чтобы передавать изменения в engine, этот binding отвечает за управление PipelineOwner и инициализацию RenderView.

PipelineOwner — это такой объект, который знает, какой RenderObject должен среагировать в ответ на изменения layout. Он же и управляет этой реакцией.

SemanticsBinding

Связывает engine и слой семантики. Отвечает за всё необходимое для accessibility приложения, чтобы им могли пользоваться люди с ограниченными возможностями здоровья:

  • упрощение или отключение анимации;
  • управление обновлениями семантики и доставка этих событий в SemanticsNode, для этого используется SemanticsOwner;
  • обработка и доставка SemanticsAction в нужный SemanticsNode.

Подробнее про accessibility вы можете прочитать в параграфе Accessibility.

PaintingBinding

Binding для связи с библиотекой painting, вот за что он отвечает:

  • механизм кеширования и вытеснения из кеша (cache eviction) изображений;
  • прогрев шейдеров (подробнее о том, зачем нужен прогрев шейдеров, можно почитать в параграфе Профилирование);
  • уведомления об изменении шрифтов в системе и их предоставление;
  • предоставление кодеков для декодирования изображений.

Вытеснение из кеша (cache eviction) — это процесс удаления данных из кеша компьютерной системы для освобождения места под новые данные. Кеш используется для временного хранения часто используемых данных и для быстрого доступа к ним. Однако кеш имеет ограниченный размер, и, когда он заполняется, новые данные не могут быть добавлены без удаления старых.

WidgetsBinding

Связывает engine и виджеты. У него две основные задачи:

  • управление процессом перестроения структуры дерева элементов (для этого используется BuildOwner);
  • вызов рендера в ответ на изменения структуры дерева.

Помимо этого, он объединяет функционал других сервисов связи и переадресовывает его в слушателей — виджеты с миксином WidgetsBindingObserver. Например, изменения состояния приложения AppLifeCycleState, которые изначально попадают в ServicesBinding, перехватываются и отправляются в WidgetsBindingObserver. Для того чтобы наблюдать за платформенными событиями, вам нужно примиксовать WidgetsBindingObserver в свой виджет.

Пример:

1import 'package:flutter/material.dart';
2
3void main() => runApp(const WidgetBindingObserverExampleApp());
4
5class WidgetBindingObserverExampleApp extends StatelessWidget {
6  const WidgetBindingObserverExampleApp({super.key});
7
8  @override
9  Widget build(BuildContext context) {
10    return MaterialApp(
11      home: Scaffold(
12        appBar: AppBar(title: const Text('App lifecycle observer')),
13        body: const WidgetBindingsObserverSample(),
14      ),
15    );
16  }
17}
18
19class WidgetBindingsObserverSample extends StatefulWidget {
20  const WidgetBindingsObserverSample({super.key});
21
22  @override
23  State<WidgetBindingsObserverSample> createState() =>
24      _WidgetBindingsObserverSampleState();
25}
26
27class _WidgetBindingsObserverSampleState
28    extends State<WidgetBindingsObserverSample> with WidgetsBindingObserver {
29  final List<AppLifecycleState> _stateHistoryList = <AppLifecycleState>[];
30
31  @override
32  void initState() {
33    super.initState();
34    WidgetsBinding.instance.addObserver(this);
35    if (WidgetsBinding.instance.lifecycleState != null) {
36      _stateHistoryList.add(WidgetsBinding.instance.lifecycleState!);
37    }
38  }
39
40  @override
41  void didChangeAppLifecycleState(AppLifecycleState state) {
42    setState(() {
43      _stateHistoryList.add(state);
44    });
45  }
46
47  @override
48  void dispose() {
49    WidgetsBinding.instance.removeObserver(this);
50    super.dispose();
51  }
52
53  @override
54  Widget build(BuildContext context) {
55    if (_stateHistoryList.isNotEmpty) {
56      return Padding(
57        padding: const EdgeInsets.all(20.0),
58        child: Center(
59          child: ListView.builder(
60            itemCount: _stateHistoryList.length,
61            itemBuilder: (BuildContext context, int index) {
62              return AppLifecycleStateWidget(
63                text: _stateHistoryList[index].toString(),
64              );
65            },
66          ),
67        ),
68      );
69    }
70
71    return const Center(child: Text('Нет событий didChangeAppLifecycle'));
72  }
73}
74
75class AppLifecycleStateWidget extends StatelessWidget {
76  final String text;
77  const AppLifecycleStateWidget({
78    required this.text,
79    Key? key,
80  }) : super(key: key);
81
82  @override
83  Widget build(BuildContext context) {
84    return Padding(
85      padding: const EdgeInsets.all(10.0),
86      child: DecoratedBox(
87        decoration: BoxDecoration(
88          border: Border.all(color: Colors.yellow, width: 2),
89        ),
90        child: Center(
91          child: Text(
92            text,
93            style: const TextStyle(
94              fontWeight: FontWeight.bold,
95            ),
96          ),
97        ),
98      ),
99    );
100  }
101}

Обратите внимание, что в методе dispose вызывается WidgetsBinding.instance.removeObserver(this) для освобождения памяти.

С помощью такого механизма работает виджет MediaQuery — он наблюдает за событием didChangeMetrics и сообщает подписчикам InheritedWidget про обновление.

WidgetsFlutterBinding

Этот сервис связи хоть и наследуется от BindingBase, но не отделяет в себе какую-то конкретную логику общения с engine. Его главная роль — инициализация всех сервисов связи, необходимых фреймворку для корректной работы.

TestWidgetsFlutterBinding

Содержит функционал, полезный при написании интеграционных тестов.

Используется библиотекой flutter_test. Как и WidgetsFlutterBinding, отвечает за инициализацию основных сервисов связи. Так же зависит от TestDefaultBinaryMessengerBinding, который переопределяет defaultBinaryMessenger на TestDefaultBinaryMessenger. Он имеет доступ к данным, отправленным со стороны плагинов, что полезно для тестовых фреймворков, мониторинга и синхронизации с сообщениями платформы.

Рендеринг и bindings

Давайте вспомним с вами несколько фактов об устройстве вёрстки во Flutter, о которых рассказывалось в параграфе Elements:

  • виджет — неизменяемая конфигурация для Element;
  • из виджетов получается дерево элементов, элемент содержит ссылку на виджет, который его создал;
  • элементы связаны друг с другом как parent и child;
  • элемент может содержать RenderObject.

Для того чтобы обновить картинку на устройстве, Element и RenderObject в начале проходят фазу аннулирования.

Аннулирование (англ. invalidate) — это проверка, что элементы или рендер-объекты не устарели. Например, при получении новой конфигурации элемент может ей не соответствовать, и тогда требуется обновление дерева элементов.

Для Element этот процесс запускается в следующих двух сценариях:

  1. Первый сценарий — в случае вызова метода setState: проверяется, не устарел ли StatefulElement.
  2. Второй сценарий — в случае, если Element подписан на ProxyElement, который отправляет уведомление об изменении его конфигурации — InheritedWidget.

Результатом фазы аннулирования элементов является список элементов, помеченных флагом dirty.

Для RenderObject сценарии следующие:

  1. Изменения геометрии RenderObject (позиция, размер и т. д.).
  2. Необходимость перерисовки (если поменялся только цвет, стиль шрифта и т. д.).

В результате фазы аннулирования получается список RenderObject, который необходимо перерисовать.

После фазы аннулирования в ход вступает сервисSchedulerBinding и отправляет запрос в Flutter engine на планировку следующего кадра.

После того как engine будет готов отрисовать следующий кадр, он обращается к SchedulerBinding и вызывает метод onDrawFrame.

На схеме ниже показано, что происходит после получения SchedulerBinding сигнала onDrawFrame от engine:

flt

Scheduler делегирует вызов в сервис WidgetsBinding, вызывая метод drawFrame.

В первую очередь WidgetsBinding рассматривает изменения, произошедшие в дереве элементов: вызывает уBuildOwner метод buildScope, в котором проходится список элементов, помеченных как dirty. У каждого элемента вызывается метод rebuild, что, как правило, ведёт к вызову метода build и получению нового виджета. Далее есть два варианта поведения:

  1. Если у элемента не инициализировано поле child, вызывается метод inflate, что ведёт к созданию нового элемента.
  2. Если поле инициализировано, происходит проверка по ключу и типу: можно ли оставить существующий элемент (child). Если оставить можно, то элемент остаётся, если нет — он выбрасывается, вызывается inflate для получения нового элемента на место child.

После обработки элементов подходит очередь рендер-объектов, которые требуют перерисовки. Сервис WidgetsBinding по цепочке вызывает метод drawFrame у RendererBinding, и происходит следующее:

  • у каждого RenderObject, помеченного dirty, вызывается метод performLayout, который считает геометрию объекта (размер, отступы и т. д.);
  • происходит перерисовка RenderObject, у которого флаг needsPaint принимает значение true;
  • полученная сцена отправляется в RenderView с помощью метода compositeFrame, затем эта сцена доставляется во Flutter engine для отрисовки;
  • затем происходит обновление слоя семантики.

Наконец, новый кадр появляется на экране устройства.

flt

Совместив весь процесс отрисовки кадра, получаем следующую схему:

flt

Знаем, было нелегко, но мы справились!

В этом параграфе мы узнали, что такое Bindings и как они связывают Flutter engine с Flutter framework. Изучили, какие бывают типы сервисов связи и какую функцию имеет каждый из них, а также познакомились с тем, какую роль они принимают в процессе рендеринга.

В следующем параграфе мы приступим к изучению сливеров (англ. slivers) — инструментов, с помощью которых можно делать интерактивные списки элементов и разнообразить функциональность интерфейса вашего приложения.

Отмечайте параграфы как прочитанные чтобы видеть свой прогресс обучения

Вступайте в сообщество хендбука

Здесь можно найти единомышленников, экспертов и просто интересных собеседников. А ещё — получить помощь или поделиться знаниями.
Вступить
Сообщить об ошибке
Предыдущий параграф3.1. Elements: подробный разбор
Следующий параграф3.3. Slivers (скоро)