Irbisa · Шпаргалка

Внутреннее устройство Flutter

Три дерева, RenderObject, дерево Element, BuildOwner, конвейер рендеринга

Senior-разработчик · 22 вопроса

  1. 01

    Что такое bindings во Flutter (WidgetsFlutterBinding, SchedulerBinding, RendererBinding, GestureBinding) и за что отвечает каждый?

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

  2. 02

    Объясните три дерева Flutter: Widget, Element, RenderObject. Зачем три дерева, а не одно?

    Виджеты служат дешёвыми неизменяемыми описаниями, которые постоянно пересоздаются; Element представляют собой долгоживущие экземпляры, которые держат состояние и позицию; RenderObject выполняют дорогую раскладку и отрисовку.

  3. 03

    Как Flutter рисует интерфейс без нативных OEM-виджетов и что делают фреймворк, движок и embedder?

    Flutter никогда не просит у платформы кнопку или текстовое поле: он сам рисует каждый пиксель своим рендерером на поверхности, которую даёт платформа.

  4. 04

    При переключении свитча ниже Counter каждый раз сбрасывается в ноль. Почему он теряет State и как его сохранить?

    @overrideWidget build(BuildContext context) {  const counter = Counter(); // StatefulWidget that keeps its own count  return Column(    children: [      Switch(        value: _padded,        onChanged: (v) => setState(() => _padded = v),      ),      _padded          ? const Padding(padding: EdgeInsets.all(16), child: counter)          : counter,    ],  );}

    Потому что в этом слоте меняется тип виджета.

  5. 05

    Какой жизненный цикл у Element (mount, update, deactivate, activate, unmount) и когда на самом деле вызывается State.dispose?

    Element монтируется при первом появлении виджета, обновляется на месте, пока тип и key те же, деактивируется при удалении из дерева и размонтируется в конце кадра, если его не забрал обратно GlobalKey.

  6. 06

    Как работает пайплайн отрисовки во Flutter и что происходит между setState и пикселями на экране?

    setState лишь помечает Element грязным и запрашивает кадр.

    void main() {  WidgetsFlutterBinding.ensureInitialized();  // Real frame timings from the engine, also in release builds  SchedulerBinding.instance.addTimingsCallback((timings) {    for (final t in timings) {      if (t.totalSpan > const Duration(milliseconds: 16)) {…
  7. 07

    Чем отличаются transient-, persistent- и post-frame-колбэки и когда использовать addPostFrameCallback?

    Когда приходит vsync, SchedulerBinding выполняет три вида колбэков в фиксированном порядке: сначала transient, затем persistent, которые строят кадр, и после его завершения post-frame.

    class _HeaderState extends State<Header> {  final _key = GlobalKey();  double? _height;   @override  void initState() {…
  8. 08

    Оборачивание ребёнка Column в Positioned бросает "Incorrect use of ParentDataWidget". Что на самом деле пытался сделать Positioned?

    Positioned ничего не рисует.

    class TrayParentData extends ContainerBoxParentData<RenderBox> {  double weight = 1;} class Weighted extends ParentDataWidget<TrayParentData> {  const Weighted({super.key, required this.weight, required super.child});…
  9. 09

    Этот сеттер вызывается при каждом ребилде через updateRenderObject, и в таймлайне layout идёт каждый кадр. Что с ним не так?

    class RenderBadge extends RenderBox {  RenderBadge({required Color color}) : _color = color;   Color _color;  set color(Color value) {    _color = value;    markNeedsLayout();  }   @override  void performLayout() => size = constraints.constrain(const Size.square(24));   @override  void paint(PaintingContext context, Offset offset) => context.canvas      .drawCircle(offset + size.center(Offset.zero), 12, Paint()..color = _color);}

    Две вещи: markNeedsLayout там, где хватило бы markNeedsPaint, и нет проверки на равенство.

  10. 10

    Найдите баг: StatusDot сохраняет первый цвет, даже когда родитель перестраивает его с новым.

    class StatusDot extends LeafRenderObjectWidget {  const StatusDot({super.key, required this.color});  final Color color;  @override  RenderStatusDot createRenderObject(BuildContext context) => RenderStatusDot(color);}class RenderStatusDot extends RenderBox {  RenderStatusDot(this._color);  Color _color;  set color(Color v) { if (v == _color) return; _color = v; markNeedsPaint(); }  @override void performLayout() => size = constraints.constrain(const Size.square(12));  @override void paint(PaintingContext context, Offset offset) => context.canvas.drawCircle(offset + size.center(Offset.zero), 6, Paint()..color = _color);}

    StatusDot не переопределяет updateRenderObject, поэтому новый color так и не доходит до render object.

  11. 11

    Как работает hit testing во Flutter и почему ребёнок, вылезающий за границы родителя, не нажимается?

    При касании Flutter обходит render-дерево от корня и спрашивает каждый RenderBox, попадает ли в него точка; все, кто ответил «да», собираются в HitTestResult (от самого глубокого к корню), и событие рассылается по этому пути.

    // A round button: hits in the square corners fall through to whatever is behindclass RenderRoundButton extends RenderProxyBox {  @override  bool hitTest(BoxHitTestResult result, {required Offset position}) {    final center = size.center(Offset.zero);    if ((position - center).distance > size.shortestSide / 2) return false;…
  12. 12

    Вы открыли непрозрачный маршрут поверх страницы, где работает AnimationController. Продолжает ли он тикать под ним?

    Нет. Overlay строит всё, что скрыто за непрозрачным маршрутом, внутри TickerMode(enabled: false), а заглушённый тикер перестаёт вызывать свой колбэк, поэтому анимация замирает до тех пор, пока маршрут не закроют.

    @overridevoid initState() {  super.initState();  // wrong: if an opaque route covers this page first, the future waits  // until the user comes back, because the ticker is muted, not finished  //   _controller.forward().then((_) => _startTimer());…
  13. 13

    Для чего нужен debugFillProperties и как сделать, чтобы свой виджет показывал свои поля во Flutter Inspector?

    debugFillProperties служит хуком Diagnosticable, который перечисляет свойства объекта для отладочных инструментов.

    class PriceTag extends StatelessWidget {  const PriceTag({super.key, required this.amount, this.currency = 'USD', this.onSale = false});   final double amount;  final String currency;  final bool onSale;…
  14. 14

    Почему Flutter может позволить себе перестраивать тысячи виджетов за кадр и когда это перестаёт быть правдой?

    Потому что ребилд виджетов лишь создаёт маленькие неизменяемые объекты и сравнивает их; дорогие Element и RenderObject переиспользуются и трогаются только тогда, когда что-то действительно изменилось.

    AnimatedBuilder(  animation: _controller,  // The same child instance comes back on every tick, so updateChild  // sees identical(old, new) and skips the whole chart  child: const ExpensiveChart(),  builder: (context, child) => Transform.rotate(…
  15. 15

    Родитель и один из его потомков оба вызвали setState до одного и того же кадра. Сколько раз выполнится build потомка?

    Один раз. BuildOwner сортирует список грязных Element по глубине, поэтому родитель перестраивается первым, и грязный флаг потомка снимается в том же проходе.

  16. 16

    При переходе между маршрутами падает "Duplicate GlobalKeys detected in widget tree". Что на самом деле делает GlobalKey под капотом?

    GlobalKey работает как запись в реестре: один ключ указывает ровно на один живой Element.

    class _GalleryState extends State<Gallery> {  // Created once, owned by one State  final _playerKey = GlobalKey<VideoPlayerState>();  bool _fullscreen = false;   @override…
  17. 17

    Что такое BuildOwner и PipelineOwner и зачем работа разделена между ними?

    Это два менеджера кадра: BuildOwner проводит дерево Element через фазу build, PipelineOwner проводит дерево RenderObject через layout, compositing bits, paint и семантику.

  18. 18

    Render object вызвал markNeedsLayout. Как далеко вверх по дереву поднимается эта пометка и что её останавливает?

    Она поднимается до ближайшей relayout boundary и там останавливается.

    class RenderStripe extends RenderBox with RenderObjectWithChildMixin<RenderBox> {  RenderStripe(this._thickness);   double _thickness;  set thickness(double v) {    if (v == _thickness) return;…
  19. 19

    Найдите баг: после изменения текста внутри RenderCaption размер не обновляется, а в debug сыплется assert.

    class RenderCaption extends RenderShiftedBox {  RenderCaption() : super(null);  static const double padding = 8;   @override  void performLayout() {    final child = this.child!;    child.layout(constraints.deflate(const EdgeInsets.all(padding)));    (child.parentData! as BoxParentData).offset = const Offset(padding, padding);    size = constraints.constrain(      Size(child.size.width + padding * 2, child.size.height + padding * 2),    );  }}

    performLayout читает child.size, но раскладывает ребёнка без parentUsesSize: true.

  20. 20

    Как работает композиция (compositing) во Flutter и когда render object получает собственный слой?

    Paint не создаёт битмап: он записывает команды рисования в дерево слоёв, а композиция превращает это дерево в сцену для raster-потока.

  21. 21

    Почему builder у LayoutBuilder не может вызвать setState у предка и как он вообще строит виджеты посреди раскладки?

    Потому что builder выполняется не в фазе build.

    // wrong: the ancestor's build is over, layout is running right nowLayoutBuilder(builder: (context, constraints) {  setState(() => _width = constraints.maxWidth); // asserts  return Content(width: _width);});…
  22. 22

    Как написать собственный RenderBox и когда это правильный инструмент вместо композиции виджетов или CustomPainter?

    Вы пишете RenderObjectWidget, который создаёт и обновляет RenderBox, а бокс сам реализует раскладку, отрисовку и hit testing.

    class Inset extends SingleChildRenderObjectWidget {  const Inset({super.key, required this.gap, super.child});  final double gap;  @override  RenderObject createRenderObject(BuildContext context) => RenderInset(gap);  @override…