Объектно-ориентированное программирование
— это парадигма программирования, основанная на представлении программы в виде совокупности объектов, каждый из которых является экземпляром определенного класса, а классы образуют иерархию наследования.
Абстракция
Моделирование взаимодействий сущностей в виде абстрактных классов и интерфейсов для представления системыИнкапсуляция
Объединение данных и методов, работающих с ними, в классеНаследование
Создания новых классов на основе существующихПолиморфизм
позволяет объектам различных классов обрабатывать данные разными способами, несмотря на то что они используют один и тот же интерфейс.
Single Responsibility Principle (Принцип единственной ответственности)
Класс должен отвечать только за что-то одно.Open-Closed Principle (Принцип открытости-закрытости)
Программные сущности должны быть открыты для расширения, но закрыты для модификации.Liskov Substitution Principle (Принцип подстановки Барбары Лисков)
Наследующий класс должен дополнять, а не замещать поведение базового класса.Interface Segregation Principle (Принцип разделения интерфейса)
Клиенты не должны имплементировать логику, которую они не используют.Dependency Inversion Principle (Принцип инверсии зависимостей)
Модули верхних уровней не должны зависеть от модулей нижних уровней. Классы и верхних, и нижних уровней должны зависеть от одних и тех же абстракций (при чём абстракции не должны знать о деталях).
Императивный стиль
-
function double (arr) {
let results = []
for (let i = 0; i < arr.length; i++){
results.push(arr[i] * 2)
}
return results
}
Декларативный стиль
function double (arr) {
return arr.map((item) => item * 2)
}
-
DI
- паттерн проектирования + Основная идея чтобы объекты не создавали зависимости сами а брали из вне. Например в классы мы передаем сервисы и usecase и итераткоры и взаимодействуем с ними. -
Service Locator
- патерн проектирования , основная идея в том , что это Class который содержет все зависимости приложения. Можно самому создать, а можно использовать GetIt.
Доступ к Service Locator
может производиться из любого место в коде. В этом заключается его основной минус
Порождающие
Структурные
Поведенческие
Порождающие
. Отвечают за удобное и безопасное создание новых объектов.
Структурные
. Отвечают за построение удобных в поддержке иерархий классов.
Поведенческие
. Решают задачи эффективного и безопасного взаимодействия между объектами программы.
Порождающие
. Отвечают за удобное и безопасное создание новых объектов.
-
Singleton. у класса есть только один экземпляр, и предоставляет к нему глобальную точку доступа.
-
Builder. позволяет создавать сложные объекты пошагово. Строитель даёт возможность использовать один и тот же код строительства для получения разных представлений объектов. PizzaBuilder Pizza
-
Prototype (Прототип). копировать объекты, не вдаваясь в подробности их реализации. (abstrtact + @override)
-
Factory Method (Фабричный Метод). паттерн который определяет общий интерфейс для создания объектов в суперклассе, позволяя подклассам изменять тип создаваемых объектов.
class SMSNotification implements Notification {
@override
void send(String message) {
print("Sending SMS with message: $message");
}
}
class NotificationFactory {
static Notification createNotification(String type) {
if (type == "email") {
return EmailNotification();
} else if (type == "sms") {
return SMSNotification();
} else {
throw Exception('Notification type not supported.');
}
}
}
var emailNotification = NotificationFactory.createNotification("email");
emailNotification.send("Hello! This is an email.");
- Abstract Factory (Абстрактная Фабрика). Это когда у нас есть разные фабрики
class WindowsFactory implements OSFactory {
@override
Button createButton() {
return WindowsButton();
}
@override
Checkbox createCheckbox() {
return WindowsCheckbox();
}
}
class MacOSFactory implements OSFactory {
@override
Button createButton() {
return MacOSButton();
}
@override
Checkbox createCheckbox() {
return MacOSCheckbox();
}
void main {
GUIFactory factory;
String os = 'Windows';
if (os == 'Windows') {
factory = WindowsFactory();
} else {
factory = MacOSFactory();
}
}
}
Структурные
. Отвечают за построение удобных в поддержке иерархий классов.
- Bridge (Мост). разделяет один или несколько классов на две отдельные иерархии — абстракцию и реализацию, позволяя изменять их независимо друг от друга.
- Decorator (Декоратор). Структурный паттерн проектирования, который позволяет динамически добавлять объектам новую функциональность, оборачивая их в полезные «обёртки».
- Adapter (Адаптер). позволяет объектам с несовместимыми интерфейсами работать вместе. обворачиваем старый класс новым Адаптером
abstract class NewUserTarget {
void processName(String name);
void processEmail(String email);
}
class OldUserProcessor {
void updateUserName(String name) {
print("Updating user name to $name");
}
void updateUserEmail(String email) {
print("Updating user email to $email");
}
}
class UserAdapter implements NewUserTarget {
final OldUserProcessor _oldUserProcessor;
UserAdapter(this._oldUserProcessor);
@override
void processName(String name) {
_oldUserProcessor.updateUserName(name);
}
@override
void processEmail(String email) {
_oldUserProcessor.updateUserEmail(email);
}
}
Поведенческие
. Решают задачи эффективного и безопасного взаимодействия между объектами программы.
- Observer (Наблюдатель). Поведенческий паттерн проектирования, который создаёт механизм подписки, позволяющий одним объектам следить и реагировать на события, происходящие в других объектах.
- State (Состояние). Поведенческий паттерн проектирования, который позволяет объектам менять поведение в зависимости от своего состояния. Извне создаётся впечатление, что изменился класс объекта.
- Iterator (Итератор). Поведенческий паттерн проектирования, который даёт возможность последовательно обходить элементы составных объектов, не раскрывая их внутреннего представления.
DAO (Data Access Object, объект доступа к данным)
— абстрактный интерфейс к какому-либо типу базы данных или иному механизму храненияDTO (Data Transfer Object, объект переноса данных)
- это объект для передачи данных (объектов без поведения) между слоямиVO (Value Object, объект-значение)
⎼ это объект без специальных методов, имеющий набор свойств (полей) примитивных типов данных или тоже Value objectBO (Business Object, объект бизнеса)
- это объект, который представляют некую сущность из определенного «домена», то есть отрасли, для которой разработано приложение
Стек
— это область оперативной памяти, в которой хранятся временные данные, таких как локальные переменные и адреса возврата функций. Объем памяти, выделенный под стек, ограничен. Стек работает в порядке LIFOКуча
— это область оперативной памяти, в которой хранятся данные, созданные во время выполнения программы. Куча используется для динамического выделения памяти для объектов, которые могут изменять размер во время выполнения программы. Размер кучи задаётся при запуске приложения, но, в отличие от стека, он ограничен лишь физически. Выделение памяти в куче происходит медленнее, чем в стеке.
final
вычисляется в runtime-е. Константно только значение экземпляра. При использовании экземпляра final в памяти выделяется новая область памяти, даже если значение объекта будет идентично.const
вычисляется во время компиляции. Константно не только значение, но и сам экземпляр. При использовании const переменной новая область памяти не выделяется, а используется ссылка на уже существующий экземпляр
Хэш-код
- геттер, у любого объекта, который возвращает int
. Нужен при сохранении объекта в map
или set
. Хэш-коды должны быть одинаковыми для объектов, которые равны друг другу в соответствии с оператором ==
int get hashCode => Object.hash(runtimeType, ..., ...);
Extension
— это синтаксический сахар, который позволяет расширить существующий класс (добавить методы, операторы, сеттеры и геттеры)
Миксин
- это механизм множественного наследования, который позволяет классам использовать функциональность других классов без явного наследования.
Миксины в Dart определяются ключевым словом mixin
. Они могут содержать методы, поля и геттеры/сеттеры, но не могут иметь конструкторов. Вместо этого, миксины инициализируются автоматически, когда они применяются к классу. Для использования миксинов применяется оператор with
Если у миксинов будет метод с одинаковым названием, то останется реализация, которая указана в последнем миксине. Так как миксины будут переопределять этот метод
Sound Null Safety
– это дополнение к языку Dart, которое усиливает систему типов, отделяя типы, допускающие значение Null
, от типов, не допускающих значения Null
. Это позволяет разработчикам предотвращать ошибки, связанные с Null
.
С появлением null safety в Dart, иерархия классов и интерфейсов была изменена для учета новых требований по безопасности типов. Вот основные изменения:
-
Добавление non-nullable типов:
- Non-nullable типы обозначают, что значение не может быть null.
- Все существующие типы были разделены на non-nullable и nullable версии. Например,
int
сталint
(non-nullable) иint?
(nullable)
-
Новый корень иерархии - "Object?":
- Введен новый корневой класс
Object?
, который может быть null. В предыдущих версиях Dart, корневым классом былObject
- Введен новый корневой класс
-
Изменения в иерархии ошибок:
- Введен новый класс
NullThrownError
, который представляет собой ошибку, возникающую при попытке выброситьnull
исключение
- Введен новый класс
-
late
иrequired
:- Введены ключевые слова
late
иrequired
для обозначения переменных, которые могут быть инициализированы позднее и обязательно должны быть проинициализированы при объявлении, соответственно.
- Введены ключевые слова
Late
- это ключевое слово в dart, которое позволяет объявить non-nullable переменную и при этом не установить для нее значение. Значение инициализируется только тогда, когда мы к нему обращаемся
Generics
- это параметризованные типы. Они позволяют программе уйти от жесткой привязки к определенным типам, определить функционал так, чтобы он мог использовать данные любых типов и обеспечить их безопасность. Так же обобщения снижают повторяемость кода, дают вам возможность предоставить единый интерфейс и реализацию для многих типов.
Dart VM (Dart virtual machine)
- среда выполнения Dart
Компоненты:
Среда исполнения
Сборщик мусора
Основные библиотеки и нативные методы
Система отладка
Профилировщик
Симулятор ARM архитектуры
Зона - это механизм, который позволяет управлять и обрабатывать ошибки и другие события, происходящие в определенных областях кода.
- Защита вашего приложения от завершения из-за необработанного исключения
- Ассоциирование данных, известных как
zone-local values
, с отдельными зонами - Переопределение ограниченного набора методов, таких как print() и scheduleMicrotask(), внутри части или всего кода
- Выполнение операции каждый раз, когда код входит или выходит из зоны. Эти операции могут включать в себя запуск или остановку таймера или сохранение 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
сам по себе не может быть создан. Используется как возвращаемый тип при гарантированной ошибке.
Covariant
- это ключевое слово в dart, которое указывает на то, что тип возвращаемого значения может быть изменен на более узкий тип в подклассе.
Аннотации
— это синтаксические метаданные, которые могут быть добавлены к коду. Другими словами, это возможность добавить дополнительную информацию к любому компоненту кода, например, к классу или методу. Аннотации всегда начинаются с символа @
(@override
, @required
). Любой класс может служить аннотацией, если в нем определен const конструктор.
Спецификатор | Общий эквивалент | Байты | Минимальное значение | Максимальное значение |
---|---|---|---|---|
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 НЕ выполняется параллельно, а выполняется в последовательности, определяемой Event Loop.
Состояния Future:
- Uncompleted - операция не завершена
- Completed with Result - операция завершена успешно
- Completed with Error - операция завершена с ошибкой
Future(FutureOr<T> computation())
: создает объект future, который с помощью метода Timer.run запускает функцию computation асинхронно и возвращает ее результат.FutureOr<T>
: указывает, что функция computation должна возвращать либо объект Future либо объект типа T. Например, чтобы получить объект Future, функция computation должна возвращать либо объект Future, либо объект intFuture.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
перемещает весь последующий код в then
у Future
, которую мы дожидаемся
Event Loop
- вечный цикл, выполняющий все поступающие в изолят задачи.
В нём есть две FIFO очереди задач:
Очередь MicroTask
Используется для очень коротких действий, которые должны быть выполнены асинхронно, сразу после завершения какой-либо инструкции перед тем, как передать управление обратно Event Loop. Очередь MicroTask имеет приоритет перед очередью Event
Очередь Event
Используется для планирования операций, которые получают результат от внешних событий (операции ввода/вывода, жесты, рисование, таймеры, потоки)
Completer
позволяет поставлять Future, отправлять событие о выполнении или событие об ошибке. Это может быть полезно, когда нужно сделать цепочку Future и вернуть результат.
Stream
- это последовательность асинхронных событий. Stream сообщает вам, что есть событие и когда оно будет готово
Single subscription
- это вид потока, при котором может быть только один подписчик.Broadcast
- это вид потока, при котором может быть много подписчиков. При этом Broadcast стримы отдают свои данные вне зависимости от того, подписан ли кто-нибудь на них или нет. Подписчики стрима получают события только с момента подписки, а не с момента старта жизни стрима
Генератор
это ключевое слово, которое позволяет создавать последовательность значений с помощью yield
- sync* - это синхронный генератор. Возвращает
Iterable
- async* - это aсинхронный генератор. Возвращает
Stream
Dart
— однопоточный язык программирования. Он исполняет одновременно одну инструкцию. Но при этом мы можем запустить код в отдельном поток с помощью Isolate
Isolate
- это легковесный процесс (поток исполнения), который выполняется параллельно с другими потоками и процессами в приложении. Каждый Isolate
в Dart имеет свой собственный экземпляр виртуальной машины Dart, собственную память и управляется с помощью своего Event Loop
.
Compute
- это функция, которая создаёт изолят и запускает переданный код.
Deadlock
— каждый из потоков ожидают событий, которые могут предоставить другие потокиRace conditions
— проявление недетерминизма исполнителя программы при различном относительном порядке исполнения команд в различных потокахLock Contention
— основное время потока проводится не в исполнении полезной работы, а в ожидании блокированного другим потоком ресурсаLive Lock
— поток захватывает ресурс, но после того, как убедится, что завершить работу не может, освобождает ресурс, аннулируя результаты
StatelessWidget
- это виджет, который не имеет состояния, в процессе работы приложения не изменяет своих свойств. Они могут изменяться лишь посредством внешних событий, которые возникают в родительских виджетахStatefulWidget
- это виджет, который хранит состояние, в процессе работы приложения он может его изменять динамически с помощьюsetState()
.
createState()
вызывается единожды и создает изменяемое состояние для этого виджета в заданном месте в деревеmounted is true
initState()
вызывается единожды при инициализацииdidChangeDependencies()
вызывается единожды после инициализации и далее при уведомлениях от Inhherited-виджетов вверху по дереву, от которых зависит виджетbuild()
вызывается каждый раз при перерисовкеdidUpdateWidget(Widget oldWidget)
вызывается каждый раз при обновлении конфигурации виджетаsetState()
вызывается императивно для перерисовкиdeactivate()
вызывается, когда ранее активный элемент перемещается в список неактивных элементов, при этом удаляясь из дереваdispose()
вызывается, когда этот объект удаляется из дерева навсегдаmounted is false
BuildContext
- это интерфейс, который имплементирует Element
.
BuildContext
может быть полезен, когда нужно:
- Получить ссылку на объект
RenderObject
, соответствующий виджету (или, если виджет не является Renderer, то виджету-потомку) - Получить размер
RenderObject
- Обратиться к дереву и получить ближайший родительский
InheritedWidget
. Это используется фактически всеми виджетами, которые обычно реализуют метод of (например,MediaQuery.of(context)
,Theme.of(context)
и т.д.)
InheritedWidget
— это виджет, который предоставляет своим потомкам возможность взаимодействовать с данными, хранящимися в нём. Решает проблему с передачей данных через конструкторы. Может уведомлять виджетов внизу по дереву об изменениях в собственных данных, тем самым провоцируя их перерисовку.
Для получения Inherited виджета необходимо вызвать context.dependOnInheritedWidgetOfExactType<T extends InheritedWidget>()
в didChangeDependencies()
Сложность у операции получения InheritedWidget - O(1). Такая скорость достигается за счёт того, что Inherited виджеты хранятся в виде хэш-таблицы в Element
-е
Widget Tree
состоит изWidget
, которые используются для описания пользовательского интерфейсаElement Tree
состоит изElement
, которые управляют жизненым циклом виджета и связывают виджеты и объекты рендеринга.Render Tree
состоит изRenderObject
, которые используются для определения размеров, положения, геометрии, определения зон экрана, на которые могут повлиять жесты
Widget
- это иммутабельное описание части пользовательского интерфейса. Виджет связан с элементом, который управляет рендерингом. Виджеты образуют сруктуру, а не дерево
Element
- это мутабельное представление виджета в определенном месте дерева. Управляют жизненым циклом, связывают виджеты и объекты рендеринга.
RenderObject
- это мутабельный объект дерева визуализации. У него есть родительский объект, а также поле с данными, которое родительский объект использует для хранения специфичной информации, касающейся самого этого объекта, например, его позицию. Данный объект отвечает за отрисовку, учёт размеров и ограничений, прослушивание и обработку нажатий. При необходимости перерисовки помечается как dirty
. Перерисовывается, используя свой метод layer
Proxy
- это виджеты, которые хранят некоторую информацию и делают её доступной для потомков. Эти виджеты не принимают непосредственного участия в формировании пользовательского интерфейса, но используются для получения информации, которую они могут предоставить.
InheritedWidget
ParentDataWidget
(LayoutId
,Flexible
,KeepAlive
и т.д.)NotificationListener
Renderer
- это виджеты, которые имеют непосредственное отношение к компоновке экрана, поскольку они определяют размеры, положение, отрисовку
Row
Column
Stack
Padding
Align
Opacity
Component
- это виджеты, которые предоставляют непосредственно не окончательную информацию, связанную с размерами, позициями, внешним видом, а скорее данные (или подсказки), которые будут использоваться для получения той самой финальной информации
RaisedButton
Scaffold
Text
GestureDetector
Container
ComponentElement
- компоновочный элемент, который явно не содержит логику рисования/отображения. Есть метод build()
, который возвращает виджет. Образуется только при создании виджетов StatelessWidget
, StatefulWidget
, InheritedWidget
.
ProxyElement
StatelessElement
StatefulElement
RenderObjectElement
- отображающий элемент, явно участвующий в рисовании компонентов на экране. Содержит renderObject
и наследуется от класса Element
. Образуется при создании виджетов Padding
, Column
, Row
, Center
и др.
LeafRenderObjectElement
ListWheelElement
MultiChildRenderObjectElement
RootRenderObjectElement
SingleChildRenderObjectElement
SliverMultiBoxAdaptorElement
SlottedRenderObjectElement
- Элемент создаётся посредством вызова метода
Widget.createElement
и конфигурируется экземпляром виджета, у которого был вызван метод. - С помощью метода
mount
созданный элемент добавляется в заданную позицию родительского элемента. При вызове данного метода также ассоциируются дочерние виджеты и элементам сопоставляются объекты дерева рендеринга. - Виджет становится активным и должен появиться на экране.
- В случае изменения виджета, связанного с элементом (например, если родительский элемент изменился), есть несколько вариантов развития событий. Если новый виджет имеет такой же
runtimeType
иkey
, то элемент связывается с ним. В противном случае, текущий элемент удаляется из дерева, а для нового виджета создаётся и ассоциируется с ним новый элемент. - В случае, если родительский элемент решит удалить дочерний элемент, или промежуточный между ними, это приведет к удалению объекта рендеринга и переместит данный элемент в список неактивных, что приведет к деактивации элемента.
- Когда элемент считается неактивным, он не находится на экране. Элемент может находиться в неактивном состоянии только до конца текущего фрейма, если за это время он остается неактивным, он демонтируется, после этого считается несуществующим и больше не будет включен в дерево.
- При повторном включении в дерево элементов, например, если элемент или его предки имеют глобальный ключ, он будет удален из списка неактивных элементов, будет вызван метод
activate
, и рендер объект, сопоставленный данному элементу, снова будет встроен в дерево рендеринга. Это означает, что элемент должен снова появиться на экране.
GlobalKeys
- это ключи, которые предоставляют доступ к виджетам. Для виджетов с отслеживанием состояния глобальные ключи также предоставляют доступ к состоянию. Позволяют виджетам менять родителей в любом месте приложения без потери состояния. Должны быть уникальны для всего приложения.
LocalKeys
- это ключи, которые нужны для идентификации виджетов в коллекции с одинаковыми значениеми должны быть уникальными среди виджетов с одним и тем же родительским виджетом. Могут использоваться для тестов:
ValueKey
- это ключ, который использует значение определенного типа для идентификации самого себя. Переопределяет оператор сравнения. Если value одниковое, то ключи одинаковыеUniqueKey
- это ключ, который равен только самому себеObjectKey
- это ключ, который используется для привязки идентификатора виджета к идентификатору объекта, используемого для создания этого виджета
Уровень фреймворка
— всё, с чем мы работаем в момент написания приложения, и все служебные классы, позволяющие взаимодействовать написанному нами с уровнем движка. Всё, относящееся к данному уровню написано на Dart. Flutter Framework
взаимодействует с Flutter Engine
через слой абстракции, называемый Window
Уровень движка
— более низкий уровень, чем уровень фреймворка, содержит классы и библиотеки, позволяющие работать уровню фреймворка. В том числе виртуальная машина Dart
, Skia
и тд.
Уровень платформы
— специфичные механизмы, относящиеся к конкретной платформе запуска.
Flutter Engine
уведомляет Flutter Framework
, когда:
- Событие, представляющее интерес, происходит на уровне устройства (изменение ориентации, изменение настроек, проблема с памятью, состояние работы приложения…)
- Какое-то событие происходит на уровне стекла (жест)
- Канал платформы отправляет некоторые данные
- Но также и в основном, когда Flutter Engine готов к рендерингу нового кадра
- Создается и запускается новый процесс —
Thread (Isolate)
. Это единственный процесс, в котором будет выполняться ваше приложение. - Инициализируются две очереди с
MicroTask
иEvent
, тип очередейFIFO
(прим.: first in first out, т.е. сообщение, пришедшие раньше, будут раньше обработаны) - Исполняется функция
main()
- Запускается
Event Loop
. Он управлет порядком исполнения вашего кода, в зависимости от содержимого двух очередей:MicroTask
иEvent
. Представляет собой "бесконечный" цикл. Event Loop
с определённой частотой проверяетMicroTask
иEvent
. Если есть что-то вMicroTask
, то оно выполняется перед очередьюEvent
.
CustomPaint
- это класс, который создает «холст» для рисования. В методе paint
в качестве аргументов поступает canvas
, который позволяет рисовать различные фигуры
WidgetsFlutterBinding
— конкретная реализация привязки приложений на основе инфраструктуры виджетов. По сути своей — это клей, соединяющий фреймворк и движок Flutter. WidgetsFlutterBinding
состоит из множества связей: GestureBinding
, ServicesBinding
, SchedulerBinding
, PaintingBinding
, SemanticsBinding
, RendererBinding
, WidgetsBinding
.
Метод scheduleAttachRootWidget
является отложенной реализацией attachRootWidget
. Принадлежит данный метод WidgetsBinding
. В описании к нему сказано, что он присоединяет переданный виджет к renderViewElement
— корневому элементу дерева элементов.
Метод scheduleWarmUpFrame
принадлежит SchedulerBinding
и используется для того, чтобы запланировать запуск кадра как можно скорее, не ожидая системного сигнала Vsync
.
Bindings
- это классы для обмена данными между Flutter Framework
и Flutter Engine
. Каждая привязка отвечает за обработку набора конкретных задач, действий, событий, сгруппированных по области деятельности.
BaseBinding
— базовый абстрактный класс, давайте тогда рассмотрим конкретные реализации биндингов. Среди них мы увидим:
ServicesBinding
отвечает за перенаправление сообщений от текущей платформы в обработчик данных сообщений (BinaryMessenger
);
PaintingBinding
отвечает за связь с библиотекой отрисовки.
RenderBinding
отвечает за связь между деревом рендеринга и движком Flutter.
WidgetBinding
отвечает за связь между деревом виджетов и движком Flutter.
SchedulerBinding
— планировщик очередных задач, таких как:
- вызовы приходящих колбеков, которые инициирует система в
Window.onBeginFrame
— например события тикеров и контроллеров анимаций; - вызовы непрерывных колбеков, которые инициирует система
Window.onDrawFrame
, например, события для обновления системы отображения после того, как отработают приходящие колбеки; - посткадровые колбеки, которые вызываются после непрерывных колбеков, перед возвратом из
Window.onDrawFrame
; - задачи не связанные с рендерингом, которые должны быть выполнены между кадрами.
SemanticsBinding
отвечает за связь слоя семантики и движком Flutter.
GestureBinding
отвечает за работу с подсистемой жестов.
(!) Платформенные взаимодействия возможны только в главном изоляте. Этот тот изолят, который создается при запуске вашего приложения.
Канал платформы
— это двусторонний канал связи между кодом на 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
написан только на dartPlugin
использует dart и специфичный код для платформы
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
- это объект, который описывает между какими значениями анимируется виджет и отвечает за вычисление текущего значения анимации
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
отдает текущее значение анимации, а также позволяет подписаться на обновления значения/статуса анимации
- Некоторые внешние события приводят к необходимости обновления отображения.
Schedule Frame
отправляется кFlutter Engine
- Когда
Flutter Engine
готов приступить к обновлению рендеринга, он создаетBegin Frame
запрос - Этот
Begin Frame
запрос перехватываетсяFlutter Framework
, который выполняет задачи, связанные в основном сTickers
(например, анимацию) - Эти задачи могут повторно создать запрос для более поздней отрисовки (пример: анимация не закончила своё выполнение, и для завершения ей потребуется получить еще один
Begin Frame
на более позднем этапе) - Далее
Flutter Engine
отправляетDraw Frame
, который перехватываетсяFlutter Framework
, который будет искать любые задачи, связанные с обновлением макета с точки зрения структуры и размера - После того, как все эти задачи выполнены, он переходит к задачам, связанным с обновлением макета с точки зрения отрисовки
- Если на экране есть что-то, что нужно нарисовать, то новая сцена для визуализации отправляется в
Flutter Engine
, который обновит экран - Затем
Flutter Framework
выполняет все задачи, которые будут выполняться после завершения рендеринга (PostFrame callbacks
), и любые другие последующие задачи, не связанные с рендерингом - …
- Ограничения спускаются вниз по дереву, от родителей к детям.
- Размеры идут вверх по дереву от детей к родителям.
- Родители устанавливают положение детей.
BuildOwner
— менеджер сборки и обновления дерева элементов. Он активно участвует в двух фазах — сборки и завершения сборки. Поскольку BuildOwner
управляет процессом сборки дерева, в нем хранятся списки неактивных элементов и списки элементов, нуждающихся в обновлении.
Методы:
scheduleBuildFor
даёт возможность пометить элемент как нуждающийся в обновлении.lockState
защищает элемент от неправильного использования, утечек памяти и пометки на обновления в процессе уничтожения.buildScope
осуществляет пересборку дерева. Работает с элементами, которые помечены как нуждающиеся в обновлении.finalizeTree
завершает построение дерева. Удаляет неиспользуемые элементы и осуществляет дополнительные проверки в режиме отладки — в том числе на дублирование глобальных ключей.reassemble
обеспечивает работу механизмаHotReload
. Этот механизм позволяет не пересобирать проект при изменениях, а отправлять новую версию кода наDartVM
и инициировать обновление дерева.
PipelineOwner
— менеджер сборки, который занимается работой с деревом отображения.
Garbage Collector
- это алгоритм, наблюдает за ссылками и очищает память с целью предотвращения её переполнения.
(!) В процессе сборки мусора слой Dart Framework
создает канал взаимодействия со слоем Flutter Engine
, посредством которого узнает о моментах простоя приложения и отсутствия пользовательского взаимодействия. В эти моменты Dart Framework
запускает процесс оптимизации памяти, что позволяет сократить влияния на пользовательский опыт и стабильность приложения.
Используемый объём памяти можно разделить на два пространства: активное и неактивное. Новые объекты располагаются в активной части, где по мере её заполнения, живые объекты переносятся из активной области памяти в неактивную, игнорируя мёртвые объекты. Затем неактивная половина становится активной. Этот процесс имеет цикличный характер.
Сборщик старого мусора (Parallel Marking and Concurrent Sweeping)
- Осуществляется обход дерева объектов, используемые объекты помечаются специальной меткой.
- Во время второго этапа происходит повторный проход по дереву объектов, в ходе которого непомеченные в первом этапе объекты перерабатываются
- Все метки стираются
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
: Выполняет дорогостоящие задачи (в основном ввод-вывод), которые в противном случае блокировали бы работу потоков пользовательского интерфейса или растровых потоков. Этот поток не отображается в оверлее производительности.
Архитектура - это набор решений по организации программы. Таких, как деление программы на слои, построение связей между ними, управление состоянием, связь с 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 (DI)
- это механизм, который позволяет сделать взаимодействующие в приложении объекты слабосвязанными с помощью интерфейсов. Это делает всю систему более гибкой, более адаптируемой и расширяемой
Части:
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
— это методика разработки приложений, при которой сначала пишется тест, покрывающий желаемое изменение, а затем — код, который позволит пройти тест.