Как мы переходили с Xamarin на Flutter
Всем привет! Меня зовут Виктор, я представляю одну из команд мобильной разработки компании DD Planet.
В этой статье расскажу о своем личном опыте и опыте нашей команды по переходу с кроссплатформенного фреймворка Xamarin Native на Flutter.
Дисклеймер
Статья отражает личное мнение автора и его команды.
Любой из описанных ниже фреймворков, языков и подходов можно использовать для разработки кроссплатформенных мобильных приложений, каждый имеет свои плюсы и минусы. Для решения поставленных задач рекомендуем выбирать наиболее подходящие для вас технологии.
Проблемы, с которыми мы столкнулись при использовании Xamarin Native
Необходимость реализации UI отдельно для каждой платформы при наличии общей бизнес-логики. Это занимает время и требует знаний платформенных компонентов даже для приложений с одинаковым интерфейсом на обеих мобильных платформах.
Отсутствие возможности проверить внесенные в UI изменения без пересборки приложения. Отсутствие механизма Hot Reload.
Относительно небольшое и слабо развивающееся community. Недостаточное количество библиотек с готовыми решениями, при этом часть из этих библиотек уже просто не поддерживаются. Не всегда удается без проблем обернуть и использовать необходимые нативные библиотеки.
Сложность поиска квалифицированных специалистов из-за недостаточной популярности фреймворка по сравнению с аналогами.
Отсутствие удобных и доступных механизмов для профилирования приложения. Существует лишь платное решение — Xamarin Profiler.
Малая популярность фреймворка. Практически у всех на слуху такие кроссплатформенные фреймворки, как React Native и Flutter, но не Xamarin, из-за чего сложно объяснить клиенту, какие преимущества он получит при выборе Xamarin Native по сравнению с аналогами.
Почитать детально о кроссплатформенном фреймворке Xamarin можно на портале Xamarin.ru, где собрано очень много полезной информации, основанной на практическом опыте наших коллег и профессиональной литературе.
Фреймворки, которые мы рассматривали для перехода
.NET MAUI — решает проблему отсутствия кроссплатформенного UI с помощью общей XAML-разметки и проблему отсутствия Hot Reload с помощью механизма XAML Hot Reload, но другие проблемы остаются.
React Native — решает все описанные нами проблемы.
Flutter — также решает все описанные нами проблемы.
KMP и Compose Multiplatform — достаточно молодая технология, использование которой для коммерческих аутсорс-проектов вызывает некоторые опасения в связи с возможным отсутствием необходимых готовых библиотек и небольшим community для решения возникающих проблем.
Оставалось выбрать между React Native и Flutter
В React Native нам не нравилось следующее:
Отсутствие строгой типизации в JavaScript и синтаксические конструкции в TypeScript. Это достаточно субъективная причина отказа от технологии, но для нас она играла немаловажную роль.
Возможные проблемы с производительностью в приложениях со сложной логикой и множеством экранов.
Возможные проблемы отображения на устройствах со старыми версиями ОС.
Наличие JIT-компиляции в релизе при использовании JavaScriptCore engine, что негативно влияет на производительность приложения (AOT-компиляция доступна при использовании Hermes engine).
Возможные рандомные ошибки даже при обновлении чистого проекта, на исправление которых приходится тратить дополнительное время.
Ознакомившись со множеством статей, мнениями экспертов и другими материалами по Flutter, мы решили в следующих проектах использовать именно его. При этом основной вопрос к Flutter был только в сферах применения языка Dart.
Описание преимуществ разработки на Flutter можно найти в материалах от команды Surf: Flutter — единственная правильная кроссплатформа для приложений и Почему мобильное приложение на Flutter — хорошая идея для бизнеса. Там же есть возможность ознакомиться с мнениями экспертов об этом кроссплатформенном фреймворке. Рекомендуем материал к прочтению.
Как мы подготавливали наш первый проект к разработке на Flutter
1. Выбор архитектуры проекта
В качестве архитектуры нашего первого проекта на Flutter мы решили использовать Clean Architecture с четким разделением слоев на data, domain и presentation:
Чистая архитектура позволяет сделать код:
многоразовым и повторно используемым даже в других проектах;
гибким и масштабируемым;
чистым и понятным даже для новых разработчиков;
тестируемым за счет четкого разделения слоев;
менее зависимым от технологий в случае замены базы данных, state management, библиотек и т. д.
Применить чистую архитектуру к нашему первому проекту на Flutter нам очень помогли туториалы от Matej Rešetár. В его обучающем материале можно найти детальное описание структуры каждого из слоев чистой архитектуры с примерами кода.
Далее нам предстоял следующий этап — выбор state management.
2. Выбор state management
Из всего многообразия механизмов для управления состоянием мы выбрали BLoC:
BLoC позволяет:
отследить, в каком состоянии находится приложение в любой момент времени;
легко протестировать каждый кейс, чтобы убедиться, что приложение реагирует соответствующим образом;
работать максимально эффективно и повторно использовать компоненты даже в других приложениях.
Также BLoC state management достаточно популярен и подходит как для простых, так и для более сложных приложений.
Описание всех возможных подходов к управлению состоянием вы можете найти в официальной документации Flutter: List of state management approaches. Там содержится исчерпывающая информация о преимуществах и недостатках каждого из подходов с примерами их использования на практике. После знакомства с этим материалом вам будет проще принять решение о выборе того или иного state management.
3. Выбор структуры проекта
Мы выбирали между двумя подходами: feature-first и layer-first.
Было решено использовать feature-first — подход, позволяющий достаточно удобно работать с отдельными фичами в рамках их директорий. Это очень актуально при наличии большого количества фич в проекте, потому что все необходимые слои сосредоточены внутри feature.
Соответственно, мы остановились на следующей структуре проекта:
в директории common содержался общий код для разных проектов (в дальнейшем предполагалось на основе common сформировать package, который без проблем переносился бы в другой проект);
в директории core находился код, предназначенный только для этого проекта и используемый в нескольких фичах;
в директории features содержались конкретные фичи со своими слоями.
Для более детального ознакомления с преимуществами и недостатками популярных подходов к структурированию проектов на Flutter рекомендуем статью Andrea Bizzotto: Flutter Project Structure: Feature-first or Layer-first?
4. Настройка среды и выбор плагинов для удобства разработки
В качестве среды для разработки мобильных приложений на Flutter мы выбрали Android Studio, так как она очень похожа на JetBrains Rider, которую мы использовали при разработке мобильных приложений на Xamarin.
Мы посчитали очень полезной настройку Android Studio → Settings → Languages & Frameworks → Flutter → Show UI Guides for build methods, которая позволяет упростить восприятие структуры build-методов за счет наличия направляющих (белых) линий в дереве виджетов:
Для удобства разработки мы использовали следующие плагины:
Flutter и Dart для отображения подсказок при написании кода, подсветку синтаксиса, помощь в создании виджетов, отладку и другие функции.
Clean Architecture for Flutter для быстрого создания структуры новых фич.
Bloc для быстрого создания файлов BLoC state management.
Dart Barrel File Generator для генерации barrel-файлов с экспортами (к сожалению, данный плагин пока что несовместим с последними версиями Android Studio — Hedgehog и Iguana).
5. Настройка статического анализатора кода и git hooks
Заключительным этапом предстояло настроить и автоматизировать процесс статического анализа кода для своевременного обнаружения и предотвращения ошибок, а также обеспечения соответствия кода рекомендованным правилам.
На этом этапе мы ограничились стандартными настройками для более строгих проверок типов и рекомендованными правила из пакета flutter_lints:
include: package:flutter_lints/flutter.yaml
analyzer:
language:
strict-casts: true
strict-inference: true
strict-raw-types: true
exclude:
- '.dart_tool/**'
- 'build/**'
- '**/*.g.dart'
- '**/*.config.dart'
- '**/*.gen.dart'
- '**/*.freezed.dart'
В ближайших планах у нас использовать plugin для статического анализа от Dart Code Metrics, который позволит сформировать более тонкую настройку под наши нужды.
В целях сокращения цикла обратной связи для разработчиков было решено использовать механизмы git hooks, которые помогли нам предотвратить попадание кода с потенциально возможными ошибками в репозиторий git.
Для этого мы создали два скрипта:
installHooks.sh, позволяющий достаточно удобно установить хуки в локальный репозиторий разработчика;
pre-commit — хук, срабатывающий непосредственно перед попыткой коммита и проверяющий код статическим анализатором.
Далее представлено содержимое этих скриптов.
installHooks.sh:
#!/bin/bash
# Путь до файла pre-commit
PRE_COMMIT_PATH="hooks/pre-commit"
# Путь до файла с Flutter SDK path
FLUTTER_PATH_FILE_NAME=".git/hooks/flutter_path.txt"
# Записываем в файл переданный скрипту путь к Flutter SDK
echo $1 > "$FLUTTER_PATH_FILE_NAME"
cp "$PRE_COMMIT_PATH" .git/hooks/
chmod +x .git/hooks/pre-commit
echo "Файл pre-commit успешно установлен в директорию .git/hooks."
pre-commit:
#!/usr/bin/env bash
# Путь до файла с Flutter SDK path
FLUTTER_PATH_FILE_NAME=".git/hooks/flutter_path.txt"
flutterPath="$(<"$FLUTTER_PATH_FILE_NAME")"
if [ ! -f $flutterPath/flutter ]; then
echo "flutter command not found: try to run installHooks.sh with correct sdk path">&2
exit 1
fi
OUTPUT="$($flutterPath/flutter analyze)"
echo "$OUTPUT">&2
echo>&2
if grep -q "error •\|warning •\|info •\|error -\|warning -\|info -" <<< "$OUTPUT"; then
echo "flutter analyze found problems">&2
exit 1
fi
Также мы использовали хук commit-msg для проверки соответствия сообщений к коммитам соглашению о коммитах. Это дало нам возможность:
автоматизировать создание списка изменений;
информировать разработчиков о характере изменений;
упрощать разработчикам участие в проекте, позволяя им изучать более структурированную историю коммитов.
С какими особенностями мы столкнулись после перехода с Xamarin Native на Flutter
1. Однопоточность языка Dart
Dart имеет однопоточную модель выполнения с поддержкой изолятов, цикла событий (event loop) и асинхронного кода.
Весь код Dart выполняется в изолятах, начиная с основного изолята по умолчанию, из которого можно создать другие изоляты. Каждый изолят имеет собственную изолированную память и собственный event loop. Изоляты могут общаться между собой только посредством сообщений.
Во время работы приложения все события добавляются в очередь, которая называется очередью событий и обрабатывается с помощью event loop. Event loop отвечает за выполнение кода, сбор и обработку событий и многое другое. Событиями может быть что угодно — от запросов на перерисовку пользовательского интерфейса до пользовательских касаний.
Event loop обрабатывает события в том порядке, в котором они поставлены в очередь, по одному за раз.
2. Sound null safety
Язык Dart обеспечивает «надежную» null-безопасность.
Null safety предотвращает ошибки, возникающие в результате непреднамеренного доступа к переменным, для которых установлено значение null.
При sound null safety все переменные требуют значения. Это означает, что Dart считает, что все переменные не допускают значения null.
Sound null safety превращает потенциальные ошибки времени выполнения в ошибки анализа при написании кода.
3. Отсутствие модификаторов доступа public, protected и private
В Dart нет модификаторов доступа: public, protected и private.
При этом есть возможность ограничить видимость переменной, метода или класса с помощью нижнего подчеркивания »_», в результате чего никто не сможет получить к ним доступ за пределами файла-объявления (за исключением нотации part of / part).
4. Отсутствие namespaces
В Dart нет концепции пространств имен, но вместо этого есть библиотеки. Можно считать библиотеку своего рода эквивалентом пространства имен, поскольку библиотека может состоять из нескольких файлов и содержать несколько классов и функций.
5. Особенности синтаксиса языка Dart
Dart поддерживает:
Spread operator (…) и null-aware spread operator (…?), позволяющие достаточно удобно вставить все элементы одного списка в другой;
Cascades (…, ?…) для выполнения последовательности операций над одним и тем же объектом, что часто избавляет от необходимости создавать временную переменную и позволяет писать более короткий и гибкий код;
if-case внутри литералов коллекции для удобства заполнения коллекций при их объявлении.
И некоторые другие особенности.
Итоги и выводы
В результате перехода с Xamarin Native на Flutter нам удалось:
сократить время на верстку экранов в два раза из-за отсутствия необходимости в реализации UI на обеих мобильных платформах;
существенно сократить время на отладку кода за счет механизма Hot Reload;
минимизировать количество null reference exception за счет sound null safety;
сократить количество ошибок, связанных с использованием асинхронной парадигмы программирования, благодаря отсутствию необходимости обеспечивать синхронизацию доступа к разделяемым ресурсам.
расширить пул используемых нами актуальных технологий для разработки кроссплатформенных мобильных приложений, позволяя привлекать больше клиентов.
По нашему мнению, Flutter отлично подходит как для прототипирования, так и для более сложных проектов благодаря:
высокой скорости разработки из-за простоты языка Dart и гибкой верстки;
большому community для использования готовых библиотек и оперативного решения возникающих вопросов;
простой работе с анимациями, позволяющей сделать их максимально плавными;
высокой производительности приложений в релизе за счет AOT-компиляции;
возможности единообразного отображения и поведения приложения даже на старых версиях ОС.
Также команда Flutter предоставляет структурированную и достаточно подробную документацию, в которой можно найти ответы на большую часть вопросов, связанных с этим кроссплатформенным фреймворком, в том числе и best practices.
Если вы уже являетесь разработчиком, использующим одну из описанных ниже технологий или фреймворков, рекомендуем обратить внимание на документацию по адаптации:
Этот материал позволит вам в кратчайшие сроки и с минимальными усилиями начать разрабатывать на Flutter, используя уже имеющиеся у вас знания и навыки.
Мы не рекомендуем использовать Flutter в приложениях, в основе которых лежит взаимодействие с аппаратным оборудованием устройства через ОС. В этих случаях рекомендуем отдать предпочтение нативной разработке под конкретные платформы.
Поделитесь в комментариях своим опытом использования фреймворков. Какой из них ближе всего лично вам?
ведущий инженер-программист DD Planet