Если рассматривать UI как функцию от State, то:
— ...less не имеют внутреннего состояния, зависят только от конфигурационных параметров и от родительских виджетов и перерисовываются, когда перерисовывается родительский виджет.
— ...ful — для изменяемых виджетов с изменяемым внутренним состоянием (State). Можно использовать setState. Можно вызывать перерисовку самого себя.
В основном вызов setState ведет к тому, что на следующей итерации данный элемент помечается флажком о том, что ему требуется перестройка.
3. Какие ещё существуют способы управления State? Если рассматривать приложение в целом, а не отдельный виджет, то вопрос — про архитектурный подход. Потому что во Flutter серьёзная разработка невозможна без использования чёткой архитектуры: во-первых, вы сами себя можете запутать, во-вторых, — другим будет очень тяжело разобраться в вашем коде. В основе практически каждого архитектурного подхода лежат базовые принципы. Главный из них — это использование InheritedWidget. Это связано с тем, что дерево виджетов строится сверху вниз и, используя Provider, ValueListenable или ChangeNotifier можно изменять состояние извне. InheritedWidget -> Provider, ValueListenable, ChangeNotifier
Один из самых популярных подходов для контролирования State — это использование BLoC. Расшифровывается как «Business Logic Component» и представляет собой звено между UI и Data, которое описывает логику.
Таким образом, UI знает о данных, которые он должен отобразить, но не знает, что конкретно он должен с ними делать.
Еще один популярный архитектурный подход — на основе Redux.
В отличие от BLoC здесь основными единицами являются не отдельные страницы со своей логикой. В Redux ядром является наличие глобального State приложения, к которому обращаются все остальные окна.
Третьим архитектурным решением является Riverpod. Его часто используют в Яндексе. Он основан на Provider и работает по той же логике.
Стоит упомянуть про GetX и MobX. Это не лучшие архитектурные подходы по опыту нашей команды. Мы стараемся их избегать.
4. Для чего применяются Keys? 1) Основное, что можно делать ключами, — получать указатель на конкретный виджет. Используется для получения доступа к context и state для обращения к полям и методам целевого виджета (не лучший способ), проверки на существование контекста, и обращение к элементу, например, для определения позиции на экране.
2) Перерисовка виджета через подмену ключа.
3) Адекватное отображение элементов в списке виджетов.
Попробуйте использовать два разных по содержанию, но гомогенных списка виджетов с картинками одновременно в двух табах. Рано или поздно вы столкнетесь с косяками в информации в этих списках. Потому используйте уникальные ключи для каждого отдельного элемента.
5. Какие типы Keys бывают? Есть главный класс Key и его «наследники»:
— ValueKey (содержит константу, строку или конкретное значение). Используется для идентификации конкретного виджета;
— ObjectKey — в качестве параметра можно использовать объекты;
— UniqueKey — не принимает конкретные значения, но генерируется уникальным идентификатором;
— GlobalKey — получение доступа к стейту виджета извне (например, размера виджета после отрисовки или его позицию).
6. Как Flutter работает «под капотом»? Здесь вы можете перечислить три основных слоя Flutter: Application, Flutter и Platform. Если вам нужна подробная информация, можете о них почитать в документации, там все хорошо расписано, но обычно это не спрашивают — это вопросы для продвинутого уровня.
Часто спрашивают, например, следующее: «Почему у меня debug-версия тормозит, а релизная работает 120 кадров, и всё идеально летает?». Это приводит к ответу о двух подходах к компиляции: статическая компиляция и динамическая интерпретация.
Статическая компиляция работает за счёт того, что исходный код и исходная команда скомпилированы в исполняемый файл. Данный подход называется AOT (Ahead of Time).
Динамическая интерпретация представляет собой выполнение скрипта. Например, так работает Python, JavaScript. Данный подход называется Just in Time, или сокращённо — JIT.
JIT будет более медленным, потому что он выполняется в процессе работы приложения. AOT будет выполняться быстрее, потому что он прекомпилированный.
Как это относится к Flutter? Если вы запускаете, например, из Android Studio ваше приложение, и оно запускается в debug-режиме, то приложение будет скомпилировано Just in Time. Это позволяет использовать главные фичи Flutter — Hot Reload и Hot Restart. В релизной сборке вы будете компилироваться в AOT в машинный код, чтобы обеспечить лучшую производительность, минимальный размер и убрать то, что не нужно в релизе.
7. Компиляция — это, конечно, хорошо. Но что такое Widget, и как он отрисовывается? Всё во Flutter — это виджеты. Виджет — это описание некоторого Element. Виджет — это неизменяемое описание части пользовательского интерфейса, а элемент управляет сущностью дерева рендеринга.
Если нам нужна изменяемая конфигурация, то используется специальная сущность State, которая и описывает текущее состояние этого виджета. Однако, состояние связано не с виджетом, а с его элементным представлением.
Один и тот же виджет может быть включен в дерево виджетов множество раз или вовсе не быть включенным. Но каждый раз, когда виджет включается в дерево виджетов, ему сопоставляется элемент.
Элемент создаётся из виджета посредством вызова метода Widget.createElement. Элемент может находиться в неактивном состоянии только до конца текущего фрейма. Если за это время он остается неактивным, он демонтируется (unmount), после этого считается несуществующим, и больше не будет включен в дерево.
Наверняка здесь вас спросят, что такое BuildContext. Это то, что управляет позицией виджета в дереве виджетов (по факту, это Element, ограниченный специальным интерфейсом). Этого достаточно для ответа. Также есть RenderObject — объект дерева визуализации. Он содержит в себе информацию протокола отрисовки и расположения данного виджета.
Widget, Element и RenderObject составляют три основных дерева отрисовки на Flutter.
8. Поговорим о многопоточности. Что вы о ней знаете, и как она реализована на Flutter? Почему во Flutter используется только один поток? Потому что Flutter использует Dart, Dart — однопоточный. Flutter также работает, в основном, в одном потоке. (Для продвинутых разработчиков ответ не так очевиден, на самом деле под капотом 4 потока, но изначально доступен программисту 1). Почему? Потому что в контексте Flutter-приложения любой процесс, даже само приложение, является некоторым потоком. В контексте Dart оно называется Isolate, а не Thread (как в C++), потому что у него есть некоторые отличия от обычных тредов.
Когда процесс создан, Dart автоматически выполняет следующие действия:
— инициализирует две очереди (Queues) с именами MicroTask (микрозадания) и Event (событие);
— исполняет метод main() и, по завершении этого метода запускает Event Loop (цикл событий).
Event Loop — это такая абстрактная молотильня с двумя очередями на входе. Одна очередь с большими событиями (Event), а во вторая — с краткосрочными действиями (MicroTask). В течение всего времени жизни основного процесса приложения, один единственный невидимый вам процесс Event Loop будет управлять порядком выполнения вашего кода в зависимости от содержимого двух очередей: MicroTask и Event.
Это описывает, как работает Dart в одном потоке. Есть асинхронность, которая позволяет в течение единого процессорного времени обрабатывать их по очереди. Например, есть событие, которое неизвестно, будет ли выполнено или когда выполнено. Например, запрос к API: мы не знаем, придет ответ за 3 секунды или за 30 секунд. Поэтому в асинхронном подходе есть такое понятие, как Future.
Future представляет собой задачу, которая выполняется асинхронно и завершается (успешно или с ошибкой) когда-то в будущем. По завершении кода (успешно или с ошибкой) будет вызван метод then() или catchError() экземпляра Future.
Когда вы помечаете объявление метода ключевым словом async, для Dart это значит, что:
— результат выполнения метода — это Future;
— он синхронно выполняет код этого метода, пока не встретит первое ключевое слово await, после чего исполнение метода приостанавливается;
— оставшийся код будет запущен на исполнение как только Future, связанный с ключевым словом await будет завершён.
Асинхронный метод НЕ выполняется параллельно, он выполняется в последовательности, определяемой Event Loop.
9. А как же тогда запустить сложные вычисления отдельно от основного потока? Что такое Isolate? Как было упомянуто ранее, понятие Isolate в Dart соответствует общепринятому Thread (поток). Серьезные отличия в том, что треды не разделяют память. Соответственно, мы не можем перегонять данные напрямую, априори не может возникнуть проблема Deadlock. Реализовано общение между Isolates на основе сообщений, которые мы шлём на порты. Если у нас есть два Isolate — основной и дополнительный — у каждого будет свой Event Loop, у каждого будет свой участок памяти, с которым он работает, и у каждого — MicroTask и Event. Общение происходит посредством месседжей на порты. Можно задаться вопросом — зачем так сложно-то? Чтобы избежать гонок, их оставим для Need for Speed, тут же нет разделенной памяти и поэтому нет всех типичных сложностей многопоточности с синхронизацией
А как вообще можно запустить Isolate? Есть несколько способов:
Низкоуровневое решение через Isolate и порты. В случае, когда вам нужно выполнить код в отдельном потоке и нет необходимости в коммуникации с Isolate по завершении, в Dart есть вспомогательная функция compute, которая порождает Isolate, исполняет на нем коллбэк, передавая ему необходимые данные, возвращает значение — результат коллбэк-функции — и убивает Isolate по завершении выполнения коллбэка. Важное ограничение: коллбэк ДОЛЖЕН быть функцией верхнего уровня и НЕ МОЖЕТ быть замыканием или методом класса (даже статическим).
И еще одно ограничение: платформенные взаимодействия (Platform-Channel communication) возможны только в главном Isolate (main isolate). Это тот Изолят, который создается при запуске вашего приложения. Другими словами, платформенные взаимодействия невозможны в экземплярах Isolate, создаваемых вами программно.
10. А работают ли принципы SOLID с Flutter?
Да, работают.
Принцип единоответствия — грамотная организация классов.
Принцип openclosed можно показать на примере. Если у нас есть действия, которые выполняют разные объекты, то могут унаследоваться единые абстракции, соответственно, реализация происходит уже в наследниках.
Следующие два подхода мы объединили. Это принцип подстановки Барбары Лисков и сегрегация интерфейсов. Если кратко, принцип Барбары Лисков говорит, что объекты-наследники должны выполнять те методы, которые может сделать родитель. Принцип сегрегация интерфейсов — интерфейсы должны быть не перегружены, реализовывать какое-то конкретное поведение.
Принцип инверсии зависимостей — реализация должна зависеть от абстракций.
Например, у нас есть класс CheckOut, мы выполняем оплату. Как эта оплата будет проходить, этот класс не знает. Он знает, что у него есть интерфейс Payment, и один из объектов классов, которые реализуют интерфейс. Реализация зависит не от конкретного вида того, как он будет реализован, а от абстракции. А то, что наследуется от абстракции, будет определять, как это будет реализовано.
Также при ответе на этот вопрос можно рассказать, что такое DTO, API. Уверены, что вы легко это найдете самостоятельно.
Что делать, если хочется изучить Flutter с нуля Обещанные 10 вопросов разобрали. Если у вас возникло ощущение «Очень интересно, но не всё понятно», то у нас для вас крутейший бонус: мы выложили в онлайн абсолютно бесплатно базовый курс для Flutter-разработчиков!
Осенью мы провели большой офлайн-курс, лучшие студенты уже стали частью нашей команды, а теперь делимся знаниями с широкой аудиторией разработчиков. Курс подойдет даже для тех, кто не занимался программированием. В онлайн-версии мы оставили домашние задания, поэтому вы сможете отточить навыки и даже положить первые работы в портфолио. Задания разделены по степени сложности, поэтому будет интересно и тем, кто уже изучает Flutter.
Подписывайтесь на YouTube-канал Mad Brains и осваивайте новую профессию в новом году!