Skip to content

❓A list of questions and answers for a technical interview for the position of a Flutter developer

License

Notifications You must be signed in to change notification settings

p0dyakov/flutter_interview

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

84 Commits
 
 
 
 
 
 

Repository files navigation

Flutter Interview ENGLISH VERSION

Репозитории: Flutter Interview, Flutter Roadmap, Flutter Acrticles, Flutter Best Packages, Flutter Tools

Общие

ООП

Объектно-ориентированное программирование — это парадигма программирования, основанная на представлении программы в виде совокупности объектов, каждый из которых является экземпляром определенного класса, а классы образуют иерархию наследования.

  • Абстракция Моделирование взаимодействий сущностей в виде абстрактных классов и интерфейсов для представления системы
  • Инкапсуляция Объединение данных и методов, работающих с ними, в классе
  • Наследование Создания новых классов на основе существующих
  • Полиморфизм Использование объектов с одинаковым интерфейсом без информации о типе и внутренней структуре объекта

Подробнее


SOLID

  • Single Responsibility Principle (Принцип единственной ответственности) Класс должен отвечать только за что-то одно.
  • Open-Closed Principle (Принцип открытости-закрытости) Программные сущности должны быть открыты для расширения, но закрыты для модификации.
  • Liskov Substitution Principle (Принцип подстановки Барбары Лисков) Наследующий класс должен дополнять, а не замещать поведение базового класса.
  • Interface Segregation Principle (Принцип разделения интерфейса) Клиенты не должны имплементировать логику, которую они не используют.
  • Dependency Inversion Principle (Принцип инверсии зависимостей) Модули верхних уровней не должны зависеть от модулей нижних уровней. Классы и верхних, и нижних уровней должны зависеть от одних и тех же абстракций (при чём абстракции не должны знать о деталях).

Git Flow

Фича

  1. Начало новой фичи. Создаём новую ветку feature/future_name из develop
  2. Завершение фичи. Сливаем feature/future_name в develop, удаляем feature/future_name
  3. Начало релиза. Создаём ветку релиза release/vX.X.X, ответляя от ветки develop
  4. Завершение релиза. Ветка релиза release/vX.X.X сливается в master, релиз помечается тегом, ветка релиза сливается в develop, ветка релиза удаляется

Фикс

  1. Начало исправления. От ветки master создаём hotfix/fix_name
  2. Завершение исправления. Из ветки hotfix/fix_name исправление сливается в develop и master, ветка фикса удаляется

Структуры данных

Структуры данных нужны для хранения данных в подходящем виде

  • Массивы
  • Стеки (LIFO - последний вошёл, первый вышел)
  • Очереди (FIFO - первый вошёл, первый вышел)
  • Связные списки
  • Деревья
  • Графы
  • Хеш-таблицы

Подробнее


Императивное и декларативное программирование

  • Императивный стиль - описываем, как добиться желаемого результата
  • Декларативный стиль - описываем, какой именно результат нам нужен

Стек и куча

  • Стек — это область оперативной памяти, в которой хранятся временные данные, таких как локальные переменные и адреса возврата функций. Объем памяти, выделенный под стек, ограничен. Стек работает в порядке LIFO
  • Куча — это область оперативной памяти, в которой хранятся данные, созданные во время выполнения программы. Куча используется для динамического выделения памяти для объектов, которые могут изменять размер во время выполнения программы. Размер кучи задаётся при запуске приложения, но, в отличие от стека, он ограничен лишь физически. Выделение памяти в куче происходит медленнее, чем в стеке.

DAO, DTO, VO, BO

  • DAO (Data Access Object, объект доступа к данным) — абстрактный интерфейс к какому-либо типу базы данных или иному механизму хранения
  • DTO (Data Transfer Object, объект переноса данных) - это объект для передачи данных (объектов без поведения) между слоями
  • VO (Value Object, объект-значение) ⎼ это объект без специальных методов, имеющий набор свойств (полей) примитивных типов данных или тоже Value object
  • BO (Business Object, объект бизнеса) - это объект, который представляют некую сущность из определенного «домена», то есть отрасли, для которой разработано приложение

DI и Service Locator

  • DI - передача зависимостей класса через параметры конструктора
  • Service Locator - синглтон / класс с набором статических методов

Доступ к Service Locator может производиться из любого место в коде. В этом заключается его основной минус

Dart

final и const

  • final вычисляется в runtime-е. Константно только значение экземпляра. При использовании экземпляра final в памяти выделяется новая область памяти, даже если значение объекта будет идентично.
  • const вычисляется во время компиляции. Константно не только значение, но и сам экземпляр. При использовании const переменной новая область памяти не выделяется, а используется ссылка на уже существующий экземпляр

JIT и AOT

  • Just-in-time (JIT) компиляция - это вид компиляции, который выполняется непосредственно во время работы программы, что существенно ускоряет цикл разработки. Но стоит учитывать, что программа может притормаживать и выполняться медленнее
  • Ahead-of-time (AOT) компиляция - это вид компиляции, который полностью выполняется перед запуском программы. Код на Dart преобразуется в нативный машинный код, который затем упаковывается в бинарный файл с расширением .so для Android или .dylib для iOS. AOT Занимает больше времени, чем JIT, но в результате программа работает куда быстрее.

Hot Restart и Hot Reload

  • Hot Reload загружает изменения в Dart VM и ребилдит дерево виджетов, сохраняя состояние. Не перезапускает main() и initState()
  • Hot Restart загружает изменения в Dart VM и перезагружает всё приложение. Перезапускает main() и initState(). Состояние не сохраняется

HashCode

Хэш-код - геттер, у любого объекта, который возвращает int. Нужен при сохранении объекта в map или set. Хэш-коды должны быть одинаковыми для объектов, которые равны друг другу в соответствии с оператором ==
int get hashCode => Object.hash(runtimeType, ..., ...);


Extension

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


Mixin

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

Миксины в Dart определяются ключевым словом mixin. Они могут содержать методы, поля и геттеры/сеттеры, но не могут иметь конструкторов. Вместо этого, миксины инициализируются автоматически, когда они применяются к классу. Для использования миксинов применяется оператор with

Если у миксинов будет метод с одинаковым названием, то останется реализация, которая указана в последнем миксине. Так как миксины будут переопределять этот метод


Sound Null Safety

Sound Null Safety – это дополнение к языку Dart, которое усиливает систему типов, отделяя типы, допускающие значение Null, от типов, не допускающих значения Null. Это позволяет разработчикам предотвращать ошибки, связанные с Null.


Система типов

С появлением null safety в Dart, иерархия классов и интерфейсов была изменена для учета новых требований по безопасности типов. Вот основные изменения:
  1. Добавление non-nullable типов:

    • Non-nullable типы обозначают, что значение не может быть null.
    • Все существующие типы были разделены на non-nullable и nullable версии. Например, int стал int (non-nullable) и int? (nullable)
  2. Новый корень иерархии - "Object?":

    • Введен новый корневой класс Object?, который может быть null. В предыдущих версиях Dart, корневым классом был Object
  3. Изменения в иерархии ошибок:

    • Введен новый класс NullThrownError, который представляет собой ошибку, возникающую при попытке выбросить null исключение
  4. late и required:

    • Введены ключевые слова late и required для обозначения переменных, которые могут быть инициализированы позднее и обязательно должны быть проинициализированы при объявлении, соответственно.

Late

Late - это ключевое слово в dart, которое позволяет объявить non-nullable переменную и при этом не установить для нее значение. Значение инициализируется только тогда, когда мы к нему обращаемся


Generics

Generics - это параметризованные типы. Они позволяют программе уйти от жесткой привязки к определенным типам, определить функционал так, чтобы он мог использовать данные любых типов и обеспечить их безопасность. Так же обобщения снижают повторяемость кода, дают вам возможность предоставить единый интерфейс и реализацию для многих типов.


Dart VM

Dart VM (Dart virtual machine) - среда выполнения Dart

Компоненты:

  • Среда исполнения
  • Сборщик мусора
  • Основные библиотеки и нативные методы
  • Система отладка
  • Профилировщик
  • Симулятор ARM архитектуры

Зоны

Зона - это механизм, который позволяет управлять и обрабатывать ошибки и другие события, происходящие в определенных областях кода.

  1. Защита вашего приложения от завершения из-за необработанного исключения
  2. Ассоциирование данных, известных как zone-local values, с отдельными зонами
  3. Переопределение ограниченного набора методов, таких как print() и scheduleMicrotask(), внутри части или всего кода
  4. Выполнение операции каждый раз, когда код входит или выходит из зоны. Эти операции могут включать в себя запуск или остановку таймера или сохранение stacktrace-а

Типы ошибок

Exception - это общий класс для исключений, которые обычно возникают из-за ошибок в программе, и их можно обработать и восстановиться от них:

  • DeferredLoadException
  • FormatException
  • IntegerDivisionByZeroException (помечен как устаревший)
  • IOException
  • FileSystemException
  • PathNotFoundException
  • HttpException
  • RedirectException
  • ProcessException
  • SignalException
  • SocketException
  • StdinException
  • StdoutException
  • TlsException
  • CertificateException
  • HandshakeException
  • WebSocketException
  • IsolateSpawnException
  • TimeoutException
  • NullRejectionException

Error - это класс для ошибок, которые обычно не могут быть восстановлены, и они указывают на серьезные проблемы в программе или системе:

  • OSError
  • ArgumentError
  • IndexError
  • RangeError
  • AssertionError
  • AsyncError
  • ConcurrentModificationError
  • JsonUnsupportedObjectError
  • JsonCyclicError
  • NoSuchMethodError
  • OutOfMemoryError
  • RemoteError
  • StackOverflowError
  • StateError
  • TypeError
  • UnimplementedError
  • UnsupportedError

Правила именования

  • Переменные и константы - lowerCamelCase
  • Классы, миксины, enum-ы - UpperCamelCase
  • Файлы - snake_case

Never

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


Covariant

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


Аннотации

Аннотации — это синтаксические метаданные, которые могут быть добавлены к коду. Другими словами, это возможность добавить дополнительную информацию к любому компоненту кода, например, к классу или методу. Аннотации всегда начинаются с символа @ (@override, @required). Любой класс может служить аннотацией, если в нем определен const конструктор.


int8, uint8, int16, uint16...

Спецификатор Общий эквивалент Байты Минимальное значение Максимальное значение
int8 signed char 1 -128 127
uint8 unsigned char 1 0 255
int16 short 2 -32,768 32,767
uint16 unsigned short 2 0 65,535
int32 long 4 -2,147,483,648 2,147,483,647
uint32 unsigned long 4 0 4,294,967,295
int64 long long 8 -9,223,372,036,854,775,808 9,223,372,036,854,775,807
uint64 unsigned long long 8 0 18,446,744,073,709,551,615

Future

Future - это обёртка над результатом выполнения асинхронной операции. Код Future НЕ выполняется параллельно, а выполняется в последовательности, определяемой Event Loop.
Состояния Future:

  • Uncompleted - операция не завершена
  • Completed with Result - операция завершена успешно
  • Completed with Error - операция завершена с ошибкой

Конструкторы Future

  • Future(FutureOr<T> computation()): создает объект future, который с помощью метода Timer.run запускает функцию computation асинхронно и возвращает ее результат.
  • FutureOr<T>: указывает, что функция computation должна возвращать либо объект Future либо объект типа T. Например, чтобы получить объект Future, функция computation должна возвращать либо объект Future, либо объект int
  • Future.delayed(Duration duration, [FutureOr<T> computation()]): создает объект Future, который запускается после временной задержки, указанной через первый параметр Duration. Второй необязательный параметр указывает на функцию, которая запускается после этой задержки.
  • Future.error(Object error, [StackTrace stackTrace]): создает объект Future, который содержит информацию о возникшей ошибке.
  • Future.microtask(FutureOr<T> computation()): создает объект Future, который с помощью функции scheduleMicrotask запускает функцию computation асинхронно и возвращает ее результат.
  • Future.sync(FutureOr<T> computation()): создает объект Future, который содержит результат немедленно вызываемой функции computation.
  • Future.value([FutureOr<T> value]): создает объект Future, который содержит значение value.

Await под капотом

Под капотом await перемещает весь последующий код в then у Future, которую мы дожидаемся


Event Loop

Event Loop - вечный цикл, выполняющий все поступающие в изолят задачи. В нём есть две FIFO очереди задач:

Очередь MicroTask
Используется для очень коротких действий, которые должны быть выполнены асинхронно, сразу после завершения какой-либо инструкции перед тем, как передать управление обратно Event Loop. Очередь MicroTask имеет приоритет перед очередью Event

Очередь Event
Используется для планирования операций, которые получают результат от внешних событий (операции ввода/вывода, жесты, рисование, таймеры, потоки)


Completer

Completer позволяет поставлять Future, отправлять событие о выполнении или событие об ошибке. Это может быть полезно, когда нужно сделать цепочку Future и вернуть результат.


Stream

Stream - это последовательность асинхронных событий. Stream сообщает вам, что есть событие и когда оно будет готово

  • Single subscription - это вид потока, при котором может быть только один подписчик.
  • Broadcast - это вид потока, при котором может быть много подписчиков. При этом Broadcast стримы отдают свои данные вне зависимости от того, подписан ли кто-нибудь на них или нет. Подписчики стрима получают события только с момента подписки, а не с момента старта жизни стрима

Генераторы (sync* / async*)

Генератор это ключевое слово, которое позволяет создавать последовательность значений с помощью yield

  • sync* - это синхронный генератор. Возвращает Iterable
  • async* - это aсинхронный генератор. Возвращает Stream

Многопоточность в Dart и Flutter

Dart — однопоточный язык программирования. Он исполняет одновременно одну инструкцию. Но при этом мы можем запустить код в отдельном поток с помощью Isolate


Isolate

Isolate - это легковесный процесс (поток исполнения), который выполняется параллельно с другими потоками и процессами в приложении. Каждый Isolate в Dart имеет свой собственный экземпляр виртуальной машины Dart, собственную память и управляется с помощью своего Event Loop.


Compute

Compute - это функция, которая создаёт изолят и запускает переданный код.


Проблемы многопоточности

  • Deadlock — каждый из потоков ожидают событий, которые могут предоставить другие потоки
  • Race conditions — проявление недетерминизма исполнителя программы при различном относительном порядке исполнения команд в различных потоках
  • Lock Contention — основное время потока проводится не в исполнении полезной работы, а в ожидании блокированного другим потоком ресурса
  • Live Lock — поток захватывает ресурс, но после того, как убедится, что завершить работу не может, освобождает ресурс, аннулируя результаты

Flutter

Stateless и Stateful виджеты

  • StatelessWidget - это виджет, который не имеет состояния, в процессе работы приложения не изменяет своих свойств. Они могут изменяться лишь посредством внешних событий, которые возникают в родительских виджетах
  • StatefulWidget - это виджет, который хранит состояние, в процессе работы приложения он может его изменять динамически с помощью setState().

Жизненный цикл Stateful виджета

  1. createState() вызывается единожды и создает изменяемое состояние для этого виджета в заданном месте в дереве
  2. mounted is true
  3. initState() вызывается единожды при инициализации
  4. didChangeDependencies() вызывается единожды после инициализации и далее при уведомлениях от Inhherited-виджетов вверху по дереву, от которых зависит виджет
  5. build() вызывается каждый раз при перерисовке
  6. didUpdateWidget(Widget oldWidget) вызывается каждый раз при обновлении конфигурации виджета
  7. setState() вызывается императивно для перерисовки
  8. deactivate() вызывается, когда ранее активный элемент перемещается в список неактивных элементов, при этом удаляясь из дерева
  9. dispose() вызывается, когда этот объект удаляется из дерева навсегда
  10. mounted is false

BuildContext

BuildContext - это интерфейс, который имплементирует Element.

BuildContext может быть полезен, когда нужно:

  • Получить ссылку на объект RenderObject, соответствующий виджету (или, если виджет не является Renderer, то виджету-потомку)
  • Получить размер RenderObject
  • Обратиться к дереву и получить ближайший родительский InheritedWidget. Это используется фактически всеми виджетами, которые обычно реализуют метод of (например, MediaQuery.of(context), Theme.of(context) и т.д.)

InheritedWidget

InheritedWidget — это виджет, который предоставляет своим потомкам возможность взаимодействовать с данными, хранящимися в нём. Решает проблему с передачей данных через конструкторы. Может уведомлять виджетов внизу по дереву об изменениях в собственных данных, тем самым провоцируя их перерисовку.
Для получения Inherited виджета необходимо вызвать context.dependOnInheritedWidgetOfExactType<T extends InheritedWidget>() в didChangeDependencies()

Сложность у операции получения InheritedWidget - O(1). Такая скорость достигается за счёт того, что Inherited виджеты хранятся в виде хэш-таблицы в Element


Деревья

  • Widget Tree состоит из Widget, которые используются для описания пользовательского интерфейса
  • Element Tree состоит из Element, которые управляют жизненым циклом виджета и связывают виджеты и объекты рендеринга.
  • Render Tree состоит из RenderObject, которые используются для определения размеров, положения, геометрии, определения зон экрана, на которые могут повлиять жесты

Подробнее


Widget

Widget - это иммутабельное описание части пользовательского интерфейса. Виджет связан с элементом, который управляет рендерингом. Виджеты образуют сруктуру, а не дерево


Element

Element - это мутабельное представление виджета в определенном месте дерева. Управляют жизненым циклом, связывают виджеты и объекты рендеринга.


RenderObject

RenderObject - это мутабельный объект дерева визуализации. У него есть родительский объект, а также поле с данными, которое родительский объект использует для хранения специфичной информации, касающейся самого этого объекта, например, его позицию. Данный объект отвечает за отрисовку, учёт размеров и ограничений, прослушивание и обработку нажатий. При необходимости перерисовки помечается как dirty. Перерисовывается, используя свой метод layer


Виды виджетов

Proxy - это виджеты, которые хранят некоторую информацию и делают её доступной для потомков. Эти виджеты не принимают непосредственного участия в формировании пользовательского интерфейса, но используются для получения информации, которую они могут предоставить.

  • InheritedWidget
  • ParentDataWidget (LayoutId, Flexible, KeepAlive и т.д.)
  • NotificationListener

Renderer - это виджеты, которые имеют непосредственное отношение к компоновке экрана, поскольку они определяют размеры, положение, отрисовку

  • Row
  • Column
  • Stack
  • Padding
  • Align
  • Opacity

Component - это виджеты, которые предоставляют непосредственно не окончательную информацию, связанную с размерами, позициями, внешним видом, а скорее данные (или подсказки), которые будут использоваться для получения той самой финальной информации

  • RaisedButton
  • Scaffold
  • Text
  • GestureDetector
  • Container

Виды элементов

image ComponentElement - компоновочный элемент, который явно не содержит логику рисования/отображения. Есть метод build(), который возвращает виджет. Образуется только при создании виджетов StatelessWidget, StatefulWidget, InheritedWidget.

  • ProxyElement
  • StatelessElement
  • StatefulElement

RenderObjectElement - отображающий элемент, явно участвующий в рисовании компонентов на экране. Содержит renderObject и наследуется от класса Element. Образуется при создании виджетов Padding, Column, Row, Center и др.

  • LeafRenderObjectElement
  • ListWheelElement
  • MultiChildRenderObjectElement
  • RootRenderObjectElement
  • SingleChildRenderObjectElement
  • SliverMultiBoxAdaptorElement
  • SlottedRenderObjectElement

Жизненный цикл Element-а

  1. Элемент создаётся посредством вызова метода Widget.createElement и конфигурируется экземпляром виджета, у которого был вызван метод.
  2. С помощью метода mount созданный элемент добавляется в заданную позицию родительского элемента. При вызове данного метода также ассоциируются дочерние виджеты и элементам сопоставляются объекты дерева рендеринга.
  3. Виджет становится активным и должен появиться на экране.
  4. В случае изменения виджета, связанного с элементом (например, если родительский элемент изменился), есть несколько вариантов развития событий. Если новый виджет имеет такой же runtimeType и key, то элемент связывается с ним. В противном случае, текущий элемент удаляется из дерева, а для нового виджета создаётся и ассоциируется с ним новый элемент.
  5. В случае, если родительский элемент решит удалить дочерний элемент, или промежуточный между ними, это приведет к удалению объекта рендеринга и переместит данный элемент в список неактивных, что приведет к деактивации элемента.
  6. Когда элемент считается неактивным, он не находится на экране. Элемент может находиться в неактивном состоянии только до конца текущего фрейма, если за это время он остается неактивным, он демонтируется, после этого считается несуществующим и больше не будет включен в дерево.
  7. При повторном включении в дерево элементов, например, если элемент или его предки имеют глобальный ключ, он будет удален из списка неактивных элементов, будет вызван метод activate, и рендер объект, сопоставленный данному элементу, снова будет встроен в дерево рендеринга. Это означает, что элемент должен снова появиться на экране.

GlobalKeys

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


LocalKeys

LocalKeys - это ключи, которые нужны для идентификации виджетов в коллекции с одинаковыми значениеми должны быть уникальными среди виджетов с одним и тем же родительским виджетом. Могут использоваться для тестов:

  • ValueKey - это ключ, который использует значение определенного типа для идентификации самого себя. Переопределяет оператор сравнения. Если value одниковое, то ключи одинаковые
  • UniqueKey - это ключ, который равен только самому себе
  • ObjectKey - это ключ, который используется для привязки идентификатора виджета к идентификатору объекта, используемого для создания этого виджета

Устройство Flutter под капотом

image

Уровень фреймворка — всё, с чем мы работаем в момент написания приложения, и все служебные классы, позволяющие взаимодействовать написанному нами с уровнем движка. Всё, относящееся к данному уровню написано на Dart. Flutter Framework взаимодействует с Flutter Engine через слой абстракции, называемый Window

Уровень движка — более низкий уровень, чем уровень фреймворка, содержит классы и библиотеки, позволяющие работать уровню фреймворка. В том числе виртуальная машина Dart, Skia и тд.

Уровень платформы — специфичные механизмы, относящиеся к конкретной платформе запуска.

Flutter Engine уведомляет Flutter Framework, когда:

  • Событие, представляющее интерес, происходит на уровне устройства (изменение ориентации, изменение настроек, проблема с памятью, состояние работы приложения…)
  • Какое-то событие происходит на уровне стекла (жест)
  • Канал платформы отправляет некоторые данные
  • Но также и в основном, когда Flutter Engine готов к рендерингу нового кадра

Модель выполнения во Flutter

  1. Создается и запускается новый процесс — Thread (Isolate). Это единственный процесс, в котором будет выполняться ваше приложение.
  2. Инициализируются две очереди с MicroTask и Event, тип очередей FIFO (прим.: first in first out, т.е. сообщение, пришедшие раньше, будут раньше обработаны)
  3. Исполняется функция main()
  4. Запускается Event Loop. Он управлет порядком исполнения вашего кода, в зависимости от содержимого двух очередей: MicroTask и Event. Представляет собой "бесконечный" цикл.
  5. Event Loop с определённой частотой проверяет MicroTask и Event. Если есть что-то в MicroTask, то оно выполняется перед очередью Event.

CustomPaint

CustomPaint - это класс, который создает «холст» для рисования. В методе paint в качестве аргументов поступает canvas, который позволяет рисовать различные фигуры


WidgetsFlutterBinding

WidgetsFlutterBinding — конкретная реализация привязки приложений на основе инфраструктуры виджетов. По сути своей — это клей, соединяющий фреймворк и движок Flutter. WidgetsFlutterBinding состоит из множества связей: GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding.

Метод scheduleAttachRootWidget является отложенной реализацией attachRootWidget. Принадлежит данный метод WidgetsBinding. В описании к нему сказано, что он присоединяет переданный виджет к renderViewElement — корневому элементу дерева элементов.

Метод scheduleWarmUpFrame принадлежит SchedulerBinding и используется для того, чтобы запланировать запуск кадра как можно скорее, не ожидая системного сигнала Vsync.


Bindings

image

Bindings - это классы для обмена данными между Flutter Framework и Flutter Engine. Каждая привязка отвечает за обработку набора конкретных задач, действий, событий, сгруппированных по области деятельности.

BaseBinding — базовый абстрактный класс, давайте тогда рассмотрим конкретные реализации биндингов. Среди них мы увидим:

ServicesBinding отвечает за перенаправление сообщений от текущей платформы в обработчик данных сообщений (BinaryMessenger);

PaintingBinding отвечает за связь с библиотекой отрисовки.

RenderBinding отвечает за связь между деревом рендеринга и движком Flutter.

WidgetBinding отвечает за связь между деревом виджетов и движком Flutter.

SchedulerBinding — планировщик очередных задач, таких как:

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

SemanticsBinding отвечает за связь слоя семантики и движком Flutter.

GestureBinding отвечает за работу с подсистемой жестов.


Каналы платформы

image (!) Платформенные взаимодействия возможны только в главном изоляте. Этот тот изолят, который создается при запуске вашего приложения.

Канал платформы — это двусторонний канал связи между кодом на dart и нативом, который объединяет имя канала и кодек для кодирования сообщений в двоичную форму и обратно. Вызовы асинхронны. Каждый канал должен иметь уникальный идентификатор.

Каналы сообщений - это каналы платформы, предназначенные для обмена сообщениями между нативным кодом и flutter-приложением.
Кодеки сообщений:

  • BinaryCodec Реализуя сопоставление идентификаторов в байтовых буферах, этот кодек позволяет вам наслаждаться удобством объектов канала в тех случаях, когда вам не требуется кодирование/декодирование. Каналы сообщений Dart с этим кодеком имеют тип BasicMessageChannel.
  • JSONMessageCodec Работает с «JSON-подобными» значениями (строки, числа, логические значения, null, списки этих значений и мапы строка-ключ с этими данными). Списки и мапы неоднородны и могут быть вложены друг в друга. Во время кодирования значения преобразуются в строки JSON, а затем в байты с использованием UTF-8. Каналы сообщений Dart имеют тип BasicMessageChannel с этим кодеком.
  • StandardMessageCodec Работает с несколько более обобщенными значениями, чем кодек JSON, поддерживая также однородные буферы данных (UInt8List, Int32List, Int64List, Float64List) и мапы с нестроковыми ключами. Обработка чисел отличается от JSON тем, что целые числа Dart поступают на платформу как 32- или 64-битные целые числа со знаком, в зависимости от величины никогда как числа с плавающей запятой. Значения кодируются в специальном, достаточно компактном и расширяемом двоичном формате. Стандартный кодек предназначен для выбора по умолчанию для канала связи во Flutter. Что касается JSON, каналы сообщений Dart, созданные с использованием стандартного кодека, имеют тип BasicMessageChannel.

Каналы методов — это каналы платформы, предназначенные для вызова нативного кода из flutter-приложения.
Кодеки методов:

  • StandardMethodCodec делегирует кодирование значений полезной нагрузки (payload) в StandardMessageCodec. Поскольку последний является расширяемым, то же самое можно сказать и о первом.
  • JSONMethodCodec делегирует кодирование значений полезной нагрузки (payload) в JSONMessageCodec.

Каналы событий — это специализированные каналы платформы, предназначенные для использования в случае представления событий платформы Flutter в виде потока Dart. Работает как обычный Stream

Подробнее


Режимы сборки

  • Debug (JIT) для разработки
  • Release (AOT) для публикации приложения
  • Profile (AOT) для анализа производительности

Package и Plugin

  • Package написан только на dart
  • Plugin использует dart и специфичный код для платформы

FFI Plugin

  • FFI Plugin - плагин, в котором для написания специфичных платформенных частей используется Dart FFI. Позволяет запустить код на C / C++

Этапы анимации

  • Ticker просит SchedulerBinding зарегистрировать обратный вызов и сообщить Flutter Engine, что надо разбудить его, когда появится новый обратный вызов.
  • Когда Flutter Engine готов, он вызывает SchedulerBinding через запрос onBeginFrame.
  • SchedulerBinding обращается к списку обратных вызовов ticker и выполняет каждый из них.
  • Каждый tick перехватывается "заинтересованным" контроллером для его обработки.
  • Если анимация завершена, то ticker "отключён", иначе ticker запрашивает SchedulerBinding для планирования нового обратного вызова.
  • ...

Виды анимаций

  • Tween animation. Начало, конец, время, скорость заранее определенно
  • Physics-based animation. Имитируют реальное поведение

Что такое Tween

Tween - это объект, который описывает между какими значениями анимируется виджет и отвечает за вычисление текущего значения анимации


Tween анимации

  • Implicit Animations - это набор Implicitly Animated Widgets, которые анимируются самостоятельно при их перестройке с новыми аргументами. (AnimatedAlign, AnimatedContainer, AnimatedPadding и т.д.)
  • Explicit Animations - это набор элементов управления анимационными эффектами. Предоставляют куда больше контроля над анимацией, чем Implicit Animations. Для использования необходимо подмешать к стейту вашего виджета SingleTickerProviderStateMixin / TickerProviderStateMixin, создать AnimationController и зависящие от него Animation, передать анимацию в Transition Widget (AlignTransition, DecoratedBoxTransition, SizeTransition и т.д.) SingleTickerProviderStateMixin / TickerProviderStateMixin создает Ticker
    Ticker вызывает callback на каждый фрейм анимации
    AnimationController пределяет все фреймы анимации - управляет анимацией (forward, reverse, repeat, stop, reset и т.д.)
    Animation отдает текущее значение анимации, а также позволяет подписаться на обновления значения/статуса анимации

Построение кадра

  1. Некоторые внешние события приводят к необходимости обновления отображения.
  2. Schedule Frame отправляется к Flutter Engine
  3. Когда Flutter Engine готов приступить к обновлению рендеринга, он создает Begin Frame запрос
  4. Этот Begin Frame запрос перехватывается Flutter Framework, который выполняет задачи, связанные в основном с Tickers (например, анимацию)
  5. Эти задачи могут повторно создать запрос для более поздней отрисовки (пример: анимация не закончила своё выполнение, и для завершения ей потребуется получить еще один Begin Frame на более позднем этапе)
  6. Далее Flutter Engine отправляет Draw Frame, который перехватывается Flutter Framework, который будет искать любые задачи, связанные с обновлением макета с точки зрения структуры и размера
  7. После того, как все эти задачи выполнены, он переходит к задачам, связанным с обновлением макета с точки зрения отрисовки
  8. Если на экране есть что-то, что нужно нарисовать, то новая сцена для визуализации отправляется в Flutter Engine, который обновит экран
  9. Затем Flutter Framework выполняет все задачи, которые будут выполняться после завершения рендеринга (PostFrame callbacks), и любые другие последующие задачи, не связанные с рендерингом

Расчёт макета

  • Ограничения спускаются вниз по дереву, от родителей к детям.
  • Размеры идут вверх по дереву от детей к родителям.
  • Родители устанавливают положение детей.

BuildOwner

BuildOwner — менеджер сборки и обновления дерева элементов. Он активно участвует в двух фазах — сборки и завершения сборки. Поскольку BuildOwner управляет процессом сборки дерева, в нем хранятся списки неактивных элементов и списки элементов, нуждающихся в обновлении.
Методы:

  • scheduleBuildFor даёт возможность пометить элемент как нуждающийся в обновлении.
  • lockState защищает элемент от неправильного использования, утечек памяти и пометки на обновления в процессе уничтожения.
  • buildScope осуществляет пересборку дерева. Работает с элементами, которые помечены как нуждающиеся в обновлении.
  • finalizeTree завершает построение дерева. Удаляет неиспользуемые элементы и осуществляет дополнительные проверки в режиме отладки — в том числе на дублирование глобальных ключей.
  • reassemble обеспечивает работу механизма HotReload. Этот механизм позволяет не пересобирать проект при изменениях, а отправлять новую версию кода на DartVM и инициировать обновление дерева.

PipelineOwner

PipelineOwner — менеджер сборки, который занимается работой с деревом отображения.


Garbage Collector

Garbage Collector - это алгоритм, наблюдает за ссылками и очищает память с целью предотвращения её переполнения.

(!) В процессе сборки мусора слой Dart Framework создает канал взаимодействия со слоем Flutter Engine, посредством которого узнает о моментах простоя приложения и отсутствия пользовательского взаимодействия. В эти моменты Dart Framework запускает процесс оптимизации памяти, что позволяет сократить влияния на пользовательский опыт и стабильность приложения.

Сборщик молодого мусора
image

Используемый объём памяти можно разделить на два пространства: активное и неактивное. Новые объекты располагаются в активной части, где по мере её заполнения, живые объекты переносятся из активной области памяти в неактивную, игнорируя мёртвые объекты. Затем неактивная половина становится активной. Этот процесс имеет цикличный характер.

Сборщик старого мусора (Parallel Marking and Concurrent Sweeping)
image

  1. Осуществляется обход дерева объектов, используемые объекты помечаются специальной меткой.
  2. Во время второго этапа происходит повторный проход по дереву объектов, в ходе которого непомеченные в первом этапе объекты перерабатываются
  3. Все метки стираются

Подробнее


Task Runners

image

  • Platform Task Runner: Основной поток платформы. Здесь выполняется код плагинов. Для получения дополнительной информации см. документацию по UIKit для iOS или документацию по MainThread для Android. Этот поток не отображается в наложении производительности.
  • UI Task Runner: Поток UI выполняет код Dart в виртуальной машине Dart VM. Этот поток включает в себя код, написанный вами, и код, выполняемый фреймворком Flutter от имени вашего приложения. Когда ваше приложение создает и отображает сцену, поток UI создает дерево слоев - легкий объект, содержащий команды рисования, не зависящие от устройства, и отправляет дерево слоев в растровый поток для отображения на устройстве. Не блокируйте этот поток! Показан в нижней строке оверлея производительности.
  • Raster Task Runner: Растровый поток получает дерево слоев и отображает его, обращаясь к GPU (графическому процессору). Вы не можете напрямую обращаться к растровому потоку или его данным, но если этот поток работает медленно, то это результат того, что вы сделали в коде Dart. В этом потоке работают графические библиотеки Skia и Impeller. Они показаны в верхней строке оверлея производительности. Ранее этот поток был известен как "GPU-поток", поскольку он выполняет растеризацию для GPU. Однако он выполняется на центральном процессоре. Мы переименовали его в "растровый поток", поскольку многие разработчики ошибочно (но вполне обоснованно) полагали, что этот поток работает на GPU.
  • IO Task Runner: Выполняет дорогостоящие задачи (в основном ввод-вывод), которые в противном случае блокировали бы работу потоков пользовательского интерфейса или растровых потоков. Этот поток не отображается в оверлее производительности.

Подробнее, Подробнее 2

Архитектура

Архитектура

Архитектура - это набор решений по организации программы. Таких, как деление программы на слои, построение связей между ними, управление состоянием, связь с UI. Хорошая архитектура делает слои в приложении слабо связанными, что упрощает внесение изменений, повышает тестируемость кода, упрощает систему


Чистая архитектура

Чистая архитектура - архитектура, которая следует SOLID и делится на три независимых слоя:

  • Data (datasources, models, repositories) получение данных извне
  • Domain (entities, repositories interfaces, usecases) бизнес правила
  • Presentation (bloc, pages, widgets) отображение

Пример


Управление состоянием

Vanilla

Плюсы

  • Низкий порог вхождения.
  • Не требуются сторонние библиотеки.

Минусы

  • При изменении состояния виджета дерево виджетов каждый раз целиком пересоздается.
  • Нарушает принцип единственной ответственности. Виджет отвечает не только за создание UI, но и за загрузку данных, бизнес-логику и управление состоянием.
  • Решения о том как именно отображать текущее состояние принимаются прямо в UI коде. Если состояние станет более сложным, то читаемость кода сильно понизится.

Использование:

  • Widget State

BLoC

Плюсы

  • Четкое разделение ответственности
  • Предсказуемые преобразования Event to State
  • Реактивность. Нет необходимости в вызове дополнительных методов

Минусы

  • Обязательность состояния в Bloc-е
  • Частые изменения библиотеки
  • Зависимость от сторонней библиотеки

Использование:

  • Widget State
  • App State

Redux

Плюсы

  • Единое состояние
  • Все экшены доступны всем

Минусы

  • Единое состояние
  • Зависимость от сторонней библиотеки
  • Большое количество boilerplate кода
  • Все экшены доступны всем

Использование:

  • Widget State
  • App State

Provider

Плюсы

  • Скоупы на поддеревья
  • Flutter ориентирован
  • Нет статики
  • Готовые провайдеры

Минусы

  • Завязка на фреймворк
  • Только 1 провайдер одного типа
  • Зависимость от сторонней библиотеки

Использование:

  • App State
  • Частично DI

Riverpod

Плюсы:

  • Скоупы на поддеревья
  • Flutter ориентирован
  • Нет статики
  • Готовые провайдеры

Минусы:

  • Зависимость от сторонней библиотеки
  • Циклические зависимости падают в runtime-е

Использование:

  • Widget State
  • App State
  • DI

Dependency Injection

Dependency injection (DI) - это механизм, который позволяет сделать взаимодействующие в приложении объекты слабосвязанными с помощью интерфейсов. Это делает всю систему более гибкой, более адаптируемой и расширяемой


Архитектурные патерны

MVVM

Части:

  • Model содержит в себе всю логику приложения, она хранит и обрабатывает данные, при этом не взаимодействуя с пользователем напрямую
  • View отображает данные, которые ему передали
  • ViewModel связывает модель и представление (передаёт данные между ними)

Использование:

  • Используется в ситуации, когда возможно связывание данных без необходимости ввода специальных интерфейсов представления (т.е. отсутствует необходимость реализовывать IView);
  • Частым примером является технология WPF.

Подробнее Пример

MVC

Части:

  • Model содержит в себе всю логику приложения, она хранит и обрабатывает данные, при этом не взаимодействуя с пользователем напрямую
  • View отображает данные, которые ему передали
  • Controller перехватывает событие извне и в соответствии с заложенной в него логикой, реагирует на это событие изменяя Mодель, посредством вызова соответствующего метода. После изменения Модель использует событие о том что она изменилась, и все подписанные на это события Представления, получив его, обращаются к Модели за обновленными данными, после чего их и отображают

Использование:

  • Используется в ситуации, когда невозможно связывание данных (нельзя использовать Binding);

Пример

MVP

Части:

  • Model содержит в себе всю логику приложения, она хранит и обрабатывает данные, при этом не взаимодействуя с пользователем напрямую
  • View отображает данные, которые ему передали
  • Presenter подписывается на события представления, по запросу изменяет модель, обновляет представление

Использование:

  • Используется в ситуации, когда связь между представление и другими частями приложения невозможна (и Вы не можете использовать MVVM или MVP);

Способы осуществления навигации

Navigator

  • Идёт из коробки

Go Router

  • Парсинг пути и параметров запроса (например, "user/:id")
  • Поддержка deep-links
  • Поддержка перенаправления - вы можете перенаправить пользователя на другой URL-адрес в зависимости от состояния приложения
  • Именованные роуты

Auto Route

  • Парсинг пути и параметров запроса (например, "user/:id")
  • Поддержка deep-links
  • Защищённые маршруты
  • Именованные роуты
  • Разные варианты анимаций

Базы данных

Нереляционные (NoSQL):
Hive

Плюсы:

  • Реализация на чистом Dart, без платформенного кода, за счёт чего одинаково работает на разных платформах
  • Прост в использовании
  • Быстрая запись / чтение
  • Мало boilerplate кода
  • Наличие кодогенерации
  • Поддерживается на всех платформах

Минусы:

  • Связи между объектами нужно поддерживать вручную
  • Ограничение по количеству адаптеров для объектов - 224
  • Ограничение по количеству полей в адаптере - 255
  • Плохо подходит для работы с большим объемом данных
  • Выгружает всю бд в память
  • Нельзя получить доступ к box-у, созданному в другом изоляте
  • Наличие кодогенерации
  • Нет миграций

Использование:

  • Небольшой объём данных
  • Необходимость в сохранении своих дата моделей

Shared Preferences

Плюсы:

  • Прост в использовании
  • Синхронное чтение из кэша в памяти
  • Поддерживается на всех платформах

Минусы:

  • Разные реализации для Android/iOS и других платформ
  • Обращение к платформенному коду
  • Не гарантируется запись на диск после успешного выполнения метода
  • Нельзя сохранять сложные объекты из коробки

Использование:

  • Небольшой объём данных
  • Необходимость в быстром внедрении решения
  • Сохранение настроек приложения в виде примитивных данных

Реляционные (SQL):
SQFLite

Плюсы

  • Есть миграции
  • Поддерживает связи между сущностями
  • Не выгружает бд в оперативную память
  • Возможность написания сложных запросов на SQL

Минусы:

  • Сложен в использовании
  • Не поддерживается в Web, Linux, Windows
  • Скорость работы ниже чем у NoSQL
  • На разных OS и версиях могут быть разные версии SQLite

Использование:

  • Большой объём данных
  • Хранение сложно структурированных данных

Drift

  • Есть миграции
  • Поддерживает связи между сущностями
  • Не выгружает бд в оперативную память
  • Возможность написания сложных запросов на SQL

Плюсы

  • Есть миграции
  • Быстрый
  • Поддерживается на всех платформах

Минусы:

  • Сложен в использовании
  • Скорость работы ниже чем у NoSQL

Использование:

  • Большой объём данных
  • Хранение сложно структурированных данных

Тестирование

Виды тестов

  • Модульный тест тестирует одну функцию, метод или класс. Его цель - проверить правильность работы определенной функции, метода или класса. Внешние зависимости для тестируемого модуля обычно передаются как параметр.
  • Виджет тест тестирует один виджет. Цель такого теста — убедиться, что пользовательский интерфейс виджета выглядит и взаимодействует, как запланировано. Тестирование виджета происходит в тестовой среде, которая обеспечивает контекст жизненного цикла виджета. Также тестируемый виджет должен иметь возможность получать действия и события пользователя и отвечать на них .
  • Интеграционный тест тестирует все приложение или его большую часть. Цель интеграционного теста — убедиться, что все тестируемые виджеты и сервисы работают вместе, как ожидалось. Кроме того, вы можете использовать интеграционные тесты для проверки производительности вашего приложения. Как правило, интеграционный тест выполняется на реальном устройстве или эмуляторе.

TDD

TDD — это методика разработки приложений, при которой сначала пишется тест, покрывающий желаемое изменение, а затем — код, который позволит пройти тест.

Паттерны разработки

Порождающие. Отвечают за удобное и безопасное создание новых объектов или даже целых семейств объектов.

  • Factory Method (Фабричный Метод). Порождающий паттерн проектирования, который определяет общий интерфейс для создания объектов в суперклассе, позволяя подклассам изменять тип создаваемых объектов.
  • Abstract Factory (Абстрактная Фабрика). Порождающий паттерн проектирования, который позволяет создавать семейства связанных объектов, не привязываясь к конкретным классам создаваемых объектов.
  • Builder (Строитель). Порождающий паттерн проектирования, который позволяет создавать сложные объекты пошагово. Строитель даёт возможность использовать один и тот же код строительства для получения разных представлений объектов.
  • Prototype (Прототип). Порождающий паттерн проектирования, который позволяет копировать объекты, не вдаваясь в подробности их реализации.
  • Singleton (Одиночка). Порождающий паттерн проектирования, который гарантирует, что у класса есть только один экземпляр, и предоставляет к нему глобальную точку доступа.

Структурные. Отвечают за построение удобных в поддержке иерархий классов.

  • Adapter (Адаптер). Структурный паттерн проектирования, который позволяет объектам с несовместимыми интерфейсами работать вместе.
  • Bridge (Мост). Структурный паттерн проектирования, который разделяет один или несколько классов на две отдельные иерархии — абстракцию и реализацию, позволяя изменять их независимо друг от друга.
  • Composite (Компоновщик). Структурный паттерн проектирования, который позволяет сгруппировать множество объектов в древовидную структуру, а затем работать с ней так, как будто это единичный объект.
  • Decorator (Декоратор). Структурный паттерн проектирования, который позволяет динамически добавлять объектам новую функциональность, оборачивая их в полезные «обёртки».
  • Facade (Фасад). Структурный паттерн проектирования, который предоставляет простой интерфейс к сложной системе классов, библиотеке или фреймворку.
  • Flyweight (Легковес). Паттерн проектирования, который позволяет вместить бóльшее количество объектов в отведённую оперативную память. Легковес экономит память, разделяя общее состояние объектов между собой, вместо хранения одинаковых данных в каждом объекте.
  • Proxy (Заместитель). Структурный паттерн проектирования, который позволяет подставлять вместо реальных объектов специальные объекты-заменители. Эти объекты перехватывают вызовы к оригинальному объекту, позволяя сделать что-то до или после передачи вызова оригиналу.

Поведенческие. Решают задачи эффективного и безопасного взаимодействия между объектами программы.

  • Chain of Responsibility (Цепочка Обязанностей). Поведенческий паттерн проектирования, который позволяет передавать запросы последовательно по цепочке обработчиков. Каждый последующий обработчик решает, может ли он обработать запрос сам и стоит ли передавать запрос дальше по цепи.
  • Command (Команда). Поведенческий паттерн проектирования, который превращает запросы в объекты, позволяя передавать их как аргументы при вызове методов, ставить запросы в очередь, логировать их, а также поддерживать отмену операций.
  • Iterator (Итератор). Поведенческий паттерн проектирования, который даёт возможность последовательно обходить элементы составных объектов, не раскрывая их внутреннего представления.
  • Mediator (Посредник). Поведенческий паттерн проектирования, который позволяет уменьшить связанность множества классов между собой, благодаря перемещению этих связей в один класс-посредник.
  • Memento (Снимок). Поведенческий паттерн проектирования, который позволяет сохранять и восстанавливать прошлые состояния объектов, не раскрывая подробностей их реализации.
  • Observer (Наблюдатель). Поведенческий паттерн проектирования, который создаёт механизм подписки, позволяющий одним объектам следить и реагировать на события, происходящие в других объектах.
  • State (Состояние). Поведенческий паттерн проектирования, который позволяет объектам менять поведение в зависимости от своего состояния. Извне создаётся впечатление, что изменился класс объекта.
  • Strategy (Стратегия). Поведенческий паттерн проектирования, который определяет семейство схожих алгоритмов и помещает каждый из них в собственный класс, после чего алгоритмы можно взаимозаменять прямо во время исполнения программы.
  • Template Method (Шаблонный Метод). Поведенческий паттерн проектирования, который определяет скелет алгоритма, перекладывая ответственность за некоторые его шаги на подклассы. Паттерн позволяет подклассам переопределять шаги алгоритма, не меняя его общей структуры.
  • Visitor (Посетитель). Поведенческий паттерн проектирования, который позволяет добавлять в программу новые операции, не изменяя классы объектов, над которыми эти операции могут выполняться.

Подробнее