Давайте немного отступим от практических основ Flutter и взглянем на внутреннее устройство фреймворка. Здесь мы узнаем из чего состоит Flutter-приложение, на какие слои разбит фреймворк и познакомимся с тремя режимами сборки: debug, profile и release.
Компоненты flutter приложения
Для начала посмотрим, из каких компонентов состоит стандартное Flutter-приложение. Чтобы его создать воспользуйтесь командой flutter create
.
С первым слоем вы уже знакомы — это содержимое папки lib. Здесь происходит описание работы приложения: вёрстка интерфейса, программирование бизнес-логики, отправка запросов на сервер и так далее. Эту часть нашего приложения называют Dart App.
Второй слой — это как раз фреймворк, то есть Flutter. Посмотреть его исходные коды можно в открытом репозитории на GitHub. Он полностью написан на Dart и содержит в себе всё, что может понадобится разработчику: об этом мы также говорили в предыдущих главах.
Третий слой — движок. В документации Flutter он так и называется — engine
. Движок отвечает за рендеринг элементов на экране, платформенные каналы, старт Dart-изолятов и другие функции, про которые мы поговорим чуть позже. Тут важно запомнить, что engine
— это не компилятор Dart-кода. Где происходит компиляция мы расскажем чуть ниже.
Четвёртый слой — Embedder API. Это набор методов, который обязана реализовать конкретная платформа, для того чтобы запустить на ней движок Flutter. Это позволяет не привязывать реализацию движка к конкретной платформе.
Набор сущностей, реализующий этот контракт называется embedder
— пятый слой нашего приложения. embedder
привязывается к операционной системе конкретной платформы, это своего рода адаптер API-системы к API-engine
. Наружу, через публичные методы, открываются различные платформенные возможности: обработка событий операционной системы, рендеринг, старт потоков и другие. Внимательный читатель заметит, что ответственность за рендеринг лежит и на engine
, и на embedder
. Ключевая разница здесь заключается в том, что engine
содержит логику рендеринга, а embedder
— предоставляет возможность рендеринга на платформе.
Эти слои связываются в единое приложение внутри Runner
. В нём все части объединяются воедино в пакет приложения, который можно запустить на целевой платформе. Для этого приложение компилируется с помощью инструментов flutter_tools. О нём мы расскажем подробнее в параграфе 7.5, пока просто запомните, что он существует.
Можно представить это следующей схемой:
Структура flutter приложения
Flutter architectural layers
Теперь рассмотрим подробнее слои Framework, Engine и Embedder — а точнее, их составные компоненты:
Коротко расскажем, зачем они нужны, и какую задачу решают.
Начнём со слоя Framework. Его основные компоненты — UI-библиотеки, виджеты, Rendering и Foundation.
UI-библиотеки Material и Cupertino
Они содержат различные готовые элементы, которые сделаны в соответствии с гайдлайнами Android (Material Design) и iOS (Apple HIG). В них входят различные свойственные конкретным платформам UI-элементы.
- Для Material это MaterialButton, AppBar, Scaffold, GridView, MaterialApp и так далее.
- Для Cupertino — CupertinoTabScaffold, CupertinoListTile, CupertinoSwitch, CupertinoApp и другие.
Widgets
С виджетами вы уже познакомились в главах 1.3 — 1.5. Подробнее на них мы останавливаться не будем, просто акцентируем ваше внимание, что они — один из компонентов фреймворка.
Rendering
Этот слой отвечает за:
- различные математические вычисления;
- сетку координат;
- нажатия и их связку с конкретным виджетом;
- подсчёт размеров элементов;
- вычисление скорости скролла и многое другое.
Как мы уже упоминали в главе 1.3, основным элементом слоя Rendering является RenderObject. Дерево этих объектов передаётся слою Engine для отрисовки.
Важно: слой Rendering не содержит логики построения этого дерева — оно строится на стороне библиотеки Widgets.
И последнее. Слой Rendering — это абстракция над библиотекой Dart под названием dart:ui
.
Она открывает низкоуровневые интерфейсы, которые нужны Flutter для построения приложений: например, классы для обработки ввода или рендеринга.
На самом деле вы можете написать Flutter-приложение без помощи RenderObject, или виджетов или UI-библиотек Material и Cupertino. dart:ui
содержит всё необходимое для отрисовки интерфейса простого приложения (Canvas, TextBox, Paint).
Но тогда вам придётся самому просчитывать всю математику, необходимую для отрисовки, отлавливать и обрабатывать пользовательский ввод и, наконец, самостоятельно рисовать интерфейс.
Может быть такой подход и подойдет для очень простых приложений, но как только вам понадобится более сложный интерфейс, вы упретесь в высокую стену. Поэтому пользуйтесь Rendering.
Foundation, gestures, animations, painting
А еще фреймворк содержит следующие компоненты:
- Gestures — классы для работы с пользовательским вводом (нажатия, скролл, различные жесты и так далее).
- Animations — классы для создания собственных анимаций, если вам не хватило функциональности виджетов из Widgets и Material/Cupertino
- Painting — набор классов, которые оборачивают интерфейс отрисовки
engine
в более удобное API, умеют работать с тенями, изображениями, закруглениями и прочим - Foundation — низкоуровневые классы-утилиты, которые используются другими слоями фреймворка. А ещё тут происходит самое важное — связывание приложения с
engine
. О этом мы подробнее расскажем в параграфе 2.3
Engine
Давайте теперь поговорим про Engine. Общий, платформенно-не-специфичный engine
написан на языках C/C++, он реализует в себе фунционал главных API flutter.
Задачи, которыми занимается engine, можно представить следующей схемой:
Для рендеринга engine
использует графическую open-source библиотеку Skia, разработанную Google. Ей, в том числе, пользуется браузер Google Chrome, система Android и другие продукты.
Embedder
К платформенно-специфичным частям engine
относятся так называемые embedders
. Это shell-оболочки, которые реализуют функционал, специфичный для конкретной платформы. По сути они выполняют связующую роль между платформенно-не-специфичным Engine и конкретной операционной системой: благодаря этому engine
не нужно знать, на какой платформе он сейчас запущен.
В слое embedders
происходит взаимодействие с eventloop
— механизмом, который отвечает за обработку событий и асинхронность. Вот как оно устроено:
- Происходит событие — например, пользователь нажал на кнопку, сервер отправил HTTP-ответ и так далее.
embedder
перехватывает событие и доставляет егоengine
.engine
откладывает операцию ScheduleFrame.- Она планирует задачу перерисовки экрана, путем добавления события в
eventloop
.
Еще embedders
содержит логику запуска task runners
, необходимых для работы Flutter:
Platform task runner
— тут происходит взаимодействие конкретной платформы с движкомengine
: перехват различных событий платформы, нативные App Lifecycle и так далее. Он запускается на основном потоке в платформе.UI task runner
— тут происходит часть пайплайна рендеринга Flutter, eventloop главного изолята.Raster task runner
— непосредственно отрисовка на устройстве. Пересборка виджетов и рендеринг на GPU в одном потоке сильно снижает производительность, поэтому рендеринг на устройстве происходит в отдельном потоке.IO task runner
— загрузка из памяти/сети различных файлов: большие картинки, аудио, бинарные файлы и другие тяжелые операции.
Embedder
сам решает, как распределить task runners
по потокам в системе. В общем случае они распределяются по разным потокам, для лучшей производительности, но это не всегда так.
Build & build types
Теперь когда мы узнали всё необходимое про технологию, которую мы используем как инструмент для разработки и написали свое первое приложение, возникает вопрос: «А как это приложение доставить для наших пользователей»?
До этого времени мы с вами использовали Flutter в так называемом режиме разработки или debug-режиме, но на самом деле это не единственный режим для запуска нашего приложения.
Но прежде чем приступить к разбору различных типов запуска Flutter-приложения давайте с вами поговорим про то, как может компилироваться написанный нами Dart-код
Dart может компилироваться в двух вариантах: Ahead of time и Just in time.
AOT компиляция — это когда наш код собирается до запуска нашего приложения. При AOT-компиляции необходимо пройтись по каждой части программы и скомпилировать ее.
JIT компиляция — когда код может компилироваться прямо в процессе работы нашей программы. При JIT-компиляции нам не обязательно компилировать весь исходный код: мы можем скомпилировать только описание функций и методов. В процессе работы программы, когда исполнение кода дойдет до них, JIT-компилятор скомпилирует их в моменте, если компиляция не производилась ранее.
Принципиальное отличие между JIT и AOT в том, когда и как исходный код компилируется в промежуточное представление, необходимое dartVM.
При разработке приложений, мы как правило, пользуемся JIT-компиляцией кода Dart. Как вы могли догадаться, именно она позволяет нам использовать функционал Hot restart & reload. При внесении изменений в код повторно компилируются только те исходники, которые были нами затронуты. Это очень экономит время на тестировании работы кода и позволяет сфокусироваться на решении задач.
Но такой функционал не даётся нам бесплатно: за удобную и быструю разработку мы платим производительностью нашего приложения. Перекомпиляция в момент работы приложения занимает определённое время и напрямую влияет на производительность программы. При таком раскладе конечный пользователь будет получать просадки кадров и, скорее всего, ему это не очень понравится.
Если же мы компилируем наш Dart-код в AOT-режиме, мы теряем возможность использовать Hot restart & reload, но не тратим ресурсы устройства на компиляцию в процессе исполнения. Пользователи получают более плавную и ровную производительность приложения, быстрый запуск.
Flutter позволяет использовать компиляцию Dart-кода в JIT- и AOT-режиме для разных вариантов сборки и запуска наших приложений. Всего их три — давайте рассмотрим их подробнее.
Debug mode
Это стандартный режим запуска Flutter-приложения. Именно в нём мы запускаем приложение командой flutter run
, если явно не указываем другой тип сборки. Он спроектирован специально для разработки, и не предполагает быстрой производительности или минимально возможного веса приложения. В нём есть возможность пользоваться инструментами разработчика для отладки работы вашего кода, например ставить точки остановки, смотреть стек вызовов в конкретном участке кода, содержимое переменных. В этом режиме dart код собирается JIT компилятором.
Profile mode
Это режим, похожий на Debug mode — за исключением того, что конечный код получается более оптимизированным, приближенным к тому, который получат наши пользователи. Здесь dart runtime
содержит только ту дополнительную информацию, которая нужна для профилирования нашего приложения. Здесь используется AOT-компиляция и не доступен Hot reload & restart. Чтобы собрать приложение в profile-режиме, добавьте флаг --profile
:
flutter run --profile
Обычно profile-сборки собирают, чтобы наблюдать за производительностью приложения. Вы можете воспользоваться инструментом performance view, чтобы посмотреть на стабильность работы вашего приложения.
Release mode
Используется для того, чтобы собрать финальное приложение, которое попадёт в руки конечных пользователей. В этом режиме проводятся всевозможные оптимизации нашего кода, убирается неиспользуемый код, нет проверок для отлова не-критичных ошибок, отключены assert. Здесь используется AOT-компиляция. Чтобы запустить приложение в release mode добавьте флаг --release
:
flutter run --release
Если вы хотите просто собрать конечный артефакт для конкретной платформы воспользуйтесь командой build
. Вам необходимо указать имя интересующего вас артефакта и режим сборки — --release
. Например, чтобы собрать Android Package Kit (.apk), выполните следующую команду:
flutter build apk --release.
По окончанию сборки, директория, куда попал артефакт будет выведена в консоль. Обычно этоbuild/app/outputs/flutter-apk/app-release.apk
На самом деле когда мы вызываем команду flutter run
, внутри она использует эту же команду flutter build. Вы можете очистить директорию build/app/outputs/flutter-apk
(для android), и затем запустить приложение из IDE или командной строки — собранный артефакт вновь окажется в очищенной директории, даже если вы не вызвали напрямую flutter build
. Учтите, что изменения, которые вы делаете, используя Hot restart & reload не попадают в него.
Если у вас есть MacBook, вы можете попробовать запустить команду flutter build ipa
, чтобы собрать iOS-артефакт.
Signing
Чтобы ваше приложение без проблем установилось на устройства, его необходимо подписать: для этого вам нужны ключи и сертификаты.
Подпись приложений это механизм, позволяющий проверить, что обновление приложения, которое вы устанавливаете, приходит из одного и того же источника, и что оно не было подделано.
Представьте, что вы решили установить/обновить приложение Яндекс.Музыки на системе Android из неизвестного источника — а им владеет злоумышленник, не обладающий ключом подписи Яндекса. В этом случае система сообщит вам, что приложение не было создано Яндексом и устанавливать его не рекомендуется
Android signing
Чтобы создать собственный ключ для Android воспользуйтесь следующей командой
Для Mac:
keytool -genkey -v -keystore ~/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload
Для Windows:
keytool -genkey -v -keystore %userprofile%\upload-keystore.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias upload
Важно: У вас должна быть установлена Java и keytool (идёт в комплекте с Java).
Придумайте сложный пароль и не потеряйте сгенерированный файл
Если вы ещё не публиковали своё приложение в сторы то потеря ключа не так страшна — можно сгенерировать новый и использовать его. Но стоит иметь в виду, что после публикации поменять ключ, который используется для подписи, достаточно затруднительно.
Далее чтобы собрать Android-приложение, подписанное вашим ключом, вам необходимо в директории Android-проекта создать файл с названием key.properties
. Проверьте, что файл добавлен в .gitignore: в нём будут лежать пароли от сгенерированного ключа.
Добавьте в key.properties
следующее содержимое:
storePassword=<Ваш пароль store password>
keyPassword=<Ваш пароль от ключа>
keyAlias=upload
storeFile=<Полный путь до директории где лежит сгенерированный ключ>
Например
storePassword=my-store-password
keyPassword=my-key-password
keyAlias=my-custom-alias
storeFile=C:/keys/my-first-app-key.jks
Далее откройте файл build.gradle расположенный в [корневая папка проекта]/android/app/build.gradle
и добавьте в него перед началом блока android
следующие строчки:
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
...
}
Затем найдите блок buildTypes
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now,
// so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
И замените его на
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
Теперь при сборки вашего приложение в release режиме оно будет подписано сгенерированным вами ключем.
Вы можете сделать отдельные ключи для debug и releasе режимов, или разных окружений приложения, например тестовые сборки вы можете подписывать отдельным ключем.
iOS signing
Если вы хотите узнать как подписать ваше iOS приложение, ознакомьтесь с официальной документацией Apple
Apple использует для подписи так называемые сертификаты, которые сам и выдаёт. Они имеют ограниченный срок службы и их периодически нужно будет обновлять.
Сертификаты используются для подписи iOS-артефактов, всего их существует 4 вида:
- developer certificate
- distribution certificata
- provision certificate
- ad-hoc certificate
Если вы не публикуете свое приложение, то для разработки вам будет достаточно первого (developer). Для него не нужен apple developer account и можно создать его автоматически в Xcode, когда вы попытаетесь собрать из него iOS-версию вашего приложения.
Если же вы решите опубликовать приложение, то для получения остальных сертификатов вам потребуется приобрести Apple Developer Account.
Windows, linux, macOS signing
Если у вас возникнет необходимость подписать десктоп-приложение (например перед публикацией в одном из магазинов приложений), то вы можете ознакомиться со следующими материалами:
Web приложения, как правило, не нуждаются в подписи.