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}
В функции происходят три важных действия:
- Получается и инициализируется, если необходимо, объект
binding
с типомWidgetsBinding
. - Вызывается метод
scheduleAttachRootWidget
, в который передаётся переданный в функцию виджет. - Откладывается операция отрисовки первого кадра —
scheduleWarmUpFrame
.
Давайте сфокусируемся на первом действии и разберёмся, какую роль оно играет в запуске нашего приложения.
Bindings (сервисы связи) — это некоторые связующие классы, выполняющие роль «клея» между движком и фреймворком. А если говорить формальным языком — интерфейсы обмена данными между Flutter framework и Flutter engine.
По правде говоря, это не чистая связь между кодом движка на C++ и Dart-кодом, а связь между PlatformDispatcher
из dart:ui и более высокоуровневыми слоями фреймворка, своего рода набор фасадов над PlatformDispatcher
.
Базовый класс для всех bindings — BindingBase
. Конкретные сервисы наследуются от BindingBase
. Они обязуются гарантировать единственность своего экземпляра и инициализируются только один раз (реализуют паттерн «Синглтон»). Каждый сервис отделяет в себе обработку ограниченного набора задач, связанных непосредственно с ним. В сервисе GestureBinding
, например, обрабатываются задачи, связанные со взаимодействием пользователя с экраном устройства.
Всего во Flutter девять наследников класса BindingBase
:
SchedulerBinding
;ServicesBinding
;GestureBinding
;RendererBinding
;SemanticsBinding
;PaintingBinding
;WidgetsBinding
;WidgetsFlutterBinding
;TestWidgetsFlutterBinding
.
Bindings связаны между собой определённой структурой: одни сервисы связи также являются миксинами над другими, более низкоуровневыми. Зависимости между ними можно представить следующей схемой:
Давайте взглянем на каждый сервис связи и его задачи подробнее.
SchedulerBinding
Главная задача этого сервиса — планировка задач, связанных с отрисовкой кадра. Например:
- Вызовы преходящих задач (
transientCallbacks
), которые инициирует система в методеWindow.onBeginFrame
. Например, события тикеров и контроллеров анимации. - Не связанные с рендерингом задачи (
midFrameMicrotasks
), которые должны быть выполнены между кадрами. То есть микротаски, запланированные преходящими задачами. Это может быть очистка очереди событий обработанных жестов, обработка скролла. Микротаски выполняются между подготовкой к новому кадру и его отрисовкой. - Вызовы непрерывных задач (
persistentCallbacks
), которые инициирует система в методеWindow.onDrawFrame
. В частности, это вызов методаbuild
у виджета илиlayout
у рендер-объекта. - Задачи, вызываемые после отрисовки кадра (
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
этот процесс запускается в следующих двух сценариях:
- Первый сценарий — в случае вызова метода
setState
: проверяется, не устарел лиStatefulElement
. - Второй сценарий — в случае, если
Element
подписан наProxyElement
, который отправляет уведомление об изменении его конфигурации —InheritedWidget
.
Результатом фазы аннулирования элементов является список элементов, помеченных флагом dirty
.
Для RenderObject
сценарии следующие:
- Изменения геометрии
RenderObject
(позиция, размер и т. д.). - Необходимость перерисовки (если поменялся только цвет, стиль шрифта и т. д.).
В результате фазы аннулирования получается список RenderObject
, который необходимо перерисовать.
После фазы аннулирования в ход вступает сервисSchedulerBinding
и отправляет запрос в Flutter engine на планировку следующего кадра.
После того как engine будет готов отрисовать следующий кадр, он обращается к SchedulerBinding
и вызывает метод onDrawFrame
.
На схеме ниже показано, что происходит после получения SchedulerBinding
сигнала onDrawFrame
от engine:
Scheduler делегирует вызов в сервис WidgetsBinding
, вызывая метод drawFrame
.
В первую очередь WidgetsBinding
рассматривает изменения, произошедшие в дереве элементов: вызывает уBuildOwner
метод buildScope
, в котором проходится список элементов, помеченных как dirty. У каждого элемента вызывается метод rebuild
, что, как правило, ведёт к вызову метода build
и получению нового виджета. Далее есть два варианта поведения:
- Если у элемента не инициализировано поле
child
, вызывается методinflate
, что ведёт к созданию нового элемента. - Если поле инициализировано, происходит проверка по ключу и типу: можно ли оставить существующий элемент (child). Если оставить можно, то элемент остаётся, если нет — он выбрасывается, вызывается
inflate
для получения нового элемента на местоchild
.
После обработки элементов подходит очередь рендер-объектов, которые требуют перерисовки. Сервис WidgetsBinding
по цепочке вызывает метод drawFrame
у RendererBinding
, и происходит следующее:
- у каждого
RenderObject
, помеченного dirty, вызывается методperformLayout
, который считает геометрию объекта (размер, отступы и т. д.); - происходит перерисовка
RenderObject
, у которого флагneedsPaint
принимает значение true; - полученная сцена отправляется в
RenderView
с помощью методаcompositeFrame
, затем эта сцена доставляется во Flutter engine для отрисовки; - затем происходит обновление слоя семантики.
Наконец, новый кадр появляется на экране устройства.
Совместив весь процесс отрисовки кадра, получаем следующую схему:
Знаем, было нелегко, но мы справились!
В этом параграфе мы узнали, что такое Bindings
и как они связывают Flutter engine с Flutter framework. Изучили, какие бывают типы сервисов связи и какую функцию имеет каждый из них, а также познакомились с тем, какую роль они принимают в процессе рендеринга.
В следующем параграфе мы приступим к изучению сливеров (англ. slivers) — инструментов, с помощью которых можно делать интерактивные списки элементов и разнообразить функциональность интерфейса вашего приложения.