2.17. Flutter: архитектура фреймворка. Виды сборки

Давайте немного отступим от практических основ 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, пока просто запомните, что он существует.

Можно представить это следующей схемой:

fluttern

Структура flutter приложения

Flutter architectural layers

Теперь рассмотрим подробнее слои Framework, Engine и Embedder — а точнее, их составные компоненты:

fluttern

Коротко расскажем, зачем они нужны, и какую задачу решают.

Начнём со слоя 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 и другие.

flutter2

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, можно представить следующей схемой:

fluttern

Для рендеринга engine использует графическую open-source библиотеку Skia, разработанную Google. Ей, в том числе, пользуется браузер Google Chrome, система Android и другие продукты.

Embedder

К платформенно-специфичным частям engine относятся так называемые embedders. Это shell-оболочки, которые реализуют функционал, специфичный для конкретной платформы. По сути они выполняют связующую роль между платформенно-не-специфичным Engine и конкретной операционной системой: благодаря этому engine не нужно знать, на какой платформе он сейчас запущен.

fluttern

В слое 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 приложения, как правило, не нуждаются в подписи.

Отмечайте параграфы как прочитанные чтобы видеть свой прогресс обучения

Вступайте в сообщество хендбука

Здесь можно найти единомышленников, экспертов и просто интересных собеседников. А ещё — получить помощь или поделиться знаниями.
Вступить
Сообщить об ошибке
Предыдущий параграф2.16. Project: интернационализация
Следующий параграф3.1. Elements: подробный разбор