Flutter — довольно популярный фреймворк для разработки кроссплатформенных приложений. В основном это приложения под Android и iOS, но все же имеется возможность разрабатывать и под другие платформы, хотя в большинстве случаев еще не все так гладко по сравнению с основными платформами. Этот фреймворк уже сейчас имеет некоторую фан базу, которая, как и положено фан базе, генерирует статьи о том, какой Flutter клевый, крутой, интересный и производительный и т. п. В этих статьях Flutter часто сравнивается с конкурентами, и чаще всего он описан в положительном свете. Ни в коем случае не пытаюсь как‑то умалить его заслуги, но... работая с ним ежедневно, вольно или невольно начинаешь замечать неприятные и раздражающие моменты, с которыми приходится жить. Именно об этих моментах сегодня и пойдет речь.
Конечно, стоит учитывать, что и Dart, и Flutter являются относительно молодыми продуктами, и многие проблемы, описанные здесь, в будущем, скорее всего, будут исправлены. Но хотелось бы, чтобы это светлое будущее наступило скорее). Само собой, я буду говорить о том, что не нравится именно мне, и мое мнение может (и будет) не совпадать с вашим. И это нормально. Также не стоит принимать все близко к сердцу. Критика (надеюсь, конструктивная) ниже — только для того, чтобы подсветить и направить. Было бы здорово, если бы вы в комментариях поделились своими мыслями по этому поводу.
Глобально все «раздражение» я разделил на 3 основные части:
-
проблемный дизайн Dart как современного ЯП;
-
средства разработки (IDE и tooling), которые иногда не только не помогают, но и немножечко вредят;
-
ограничения Flutter, которых могло и не быть.
Проблемы в дизайне Dart, с которыми приходится жить
Dart как язык программирования можно отнести к современным. На данный момент ему всего 11 лет. Dart еще даже не подросток). Изначально Dart разрабатывался и позиционировался как язык, созданный для исправления фундаментальных изъянов JavaScript, которые иначе никак не возможно исправить. Несмотря на высокопарные заявления и благие цели, думаю, на сегодняшний день вполне очевидно, что со своей задачей (стать убийцей JavaScript) Dart не справился. Но Dart не был заброшен (как обычно это принято в Google) и в принципе стал развиваться абсолютно в другом направлении. Возможно, это и к лучшему.
Работая с Dart, невольно задаешься вопросом: а для чего он вообще существует? Что нового Dart привнес в нашу жизнь? Чем он так хорош? Почему было необходимо создавать новый ЯП? Чем сотни ранее существующих ЯП не угодили? И на эти вопросы довольно непросто получить удовлетворительный ответ. Каковы предпосылки в реализации нового ЯП? Было ли оправдано создание нового ЯП?
Новый ЯП создается обычно в следующих случаях:
-
исправление фундаментальных недостатков другого ЯП, исправить которые другим способом не представляется возможным (обратная совместимость и все такое);
-
реализация и апробация каких-то новых, зачастую инновационных идей и возможностей. А также продвижение этих идей и возможностей в массы;
-
ради обучения и получения соответствующего опыта;
-
десятки других причин...
Ни для кого не секрет, что хорошо спроектировать и реализовать ЯП — довольно непростая задача. Разработка ЯП во многих аспектах фундаментально отличается от разработки бизнес софта. Если рассматривать идеальный мир, то, кроме стандартных сложностей, присущих разработке любого софта, можно выделить еще два сложных и важных момента, на которых легко оступиться:
-
инновационность;
-
работа над ошибками.
Инновационность
Инновационность подразумевает реализацию каких‑то новых фич и возможностей языка. Другими словами: ради чего весь шухер? Разрабатывать новый ЯП ради нового ЯП — это даже звучит бессмысленно. Новый ЯП должен не только уметь что‑то делать хорошо, но и иметь какую‑то киллер‑фичу (а лучше и не одну). И вот с этим пунктом у Dart‑а изначально не все пошло хорошо. Основной причиной рождения языка было убийство JS посредством исправления его фундаментальных недостатков. Но, как мы уже знаем, Dart не сдюжил. Хотя не он первый, не он последний. По сути, сейчас Dart — это ООП язык с довольно низким порогом входа, который, по большей части, позволяет делать плюс‑минус то же самое, что и все остальные аналогичные языки. Только, по ощущениям, в среднем хуже. Очень похоже на Java образца года этак 2005. Да, есть некоторый синтаксический сахар, пара мелких идей‑улучшений, да, собственно, и все. Несмотря на наличие сахара, все равно довольно многословен и не особо поворотлив. Где та самая киллер‑фича языка? Непонятно... Пожалуй, разве что, Flutter. Но, строго говоря, Flutter — это всего лишь фреймворк, и, наверное, не совсем корректно называть его киллер‑фичей. Нас же больше интересуют именно языковые возможности.
Работа над ошибками
Второй момент — работа над ошибками. При проектировании чего‑то нового обычно стараются учесть предыдущий опыт. Особенно с учетом основной причины появления Dart на свет. Здесь Dart тоже не блистает, и умудрился наступить на грабли неоднократно. Dart версии 1.0 имел динамическую типизацию, что само по себе не является какой‑то проблемой. Но вот переход от сугубо динамической типизации к статической в версии 2.0, хоть и является приятным нововведением, говорит об изначальной непродуманности, недальновидности и ошибках проектирования. Фактически это разворот на 180 градусов. И Dart версии 1.0 улетел в помойку. Полный провал (особенно если вспомнить изначальные причины появления языка). Это так или иначе повлияло (и продолжает влиять) на весь язык в целом и его последующее развитие. Также стоит учитывать, сколько времени ушло на этот переход (можно считать, в никуда).
Еще один яркий пример — всем известная ошибка на миллиард долларов — null
. Об этой проблеме известно давным‑давно. И что это за проблема, и каковы ее последствия и цена. Но, судя по всему, грех не наступить на такие знаменитые грабли). На мой взгляд, код на современном ЯП не должен выкидывать NPE априори. И, в идеале, это должно быть статически гарантировано. Да, возможно, это вкусовщина, но, как минимум, если уж решили, что в Dart будет null
, то стоило реализовывать Sound null safety сразу (ах да, Google же не умеет в планирование и проектирование), а не делать второй по счету манёвр. И это при том, что Dart — всего лишь второй версии. Другими словами, всего за две мажорные версии Dart умудрился произвести два разворота на 180 градусов. Разве не так должна выглядеть жизнь хорошо спроектированного ЯП, цель которого — доминировать и вытеснить «богомерзкий» JS? Видимо, я что‑то не понимаю. Разработка с учетом null
и Sound null safety сразу сэкономила бы кучу времени. Причем не только разработчиков языка, но и всех остальных, кому сначала пришлось жить с проблемой (которой в принципе быть не должно), а потом мигрировать на null safety. Как говорится:
Better late, than never, but better never late...
Dart 3 и тотальная Sound null safety не за горами. Но, несмотря на это, мне кажется, что работы по доработке Sound null safety продолжатся дальше в том или ином виде, так как еще далеко не все гладко, и код наподобие такого сейчас не скомпилируется:
if (myString?.isNotEmpty == true) {
print(myString.length);
}
Или вот такой код:
OnboardingStep? step = getCurrentStep();
if (step == OnboardingStep.firstStep) {
print(step.name);
}
А на это потребуется время, которое будет позаимствовано у других фич. Напомню, что этой проблемы могло бы и не быть вовсе. Се ля ви...
Как уже выше было сказано, по большому счету Dart — приятный, простой, доступный ЯП. Программировать на нем — далеко не самый плохой опыт. Но все же есть некоторое послевкусие, чем‑то похожее на программирование на Java после C#: вроде все есть, но могло бы быть и лучше. Несмотря на разного рода синтаксический сахар, местами Dart ощущается многословным. Отсутствие мета‑программирования и других языковых выразительных возможностей (DU, Tuples, Traits, Pattern Matching) делают повседневную работу довольно монотонной, скучной и утомительной. А это явно или неявно влияет на все остальные аспекты разработки. Dart — «я его слепила из того, что было». От каждого языка по парадигме, но все не до конца. Даже ООП возможности местами печалят, хотя, казалось бы, Dart — в первую очередь ООП язык.
Чем занимается среднестатистический мобильный разработчик? Правильно: скачивает JSON из сети и сохраняет его в локальную БД (или вариации на ту же тему). Другими словами, парсинг JSON — это довольно частая операция. В Dart у нас два пути:
-
парсить ручками, что не только неудобно, но и небезопасно;
-
воспользоваться кодогенерацией.
С первым вариантом, думаю, и так все понятно. Второй вариант в принципе неплох, если бы не скорость работы. Она ну о‑очень медленная. А, как известно, любые задержки влекут за собой потенциальное переключение контекста и все сопутствующие проблемы.
Кодогенерация
Вот мы и дошли до кодогенерации. Dart обделен всем по чуть‑чуть: то одного не хватает, то другого. Но жить как‑то нужно. И очевидно, что ЯП создаются (в основном) для упрощения жизни и решения конкретных проблем. В том числе и для того, чтобы можно было писать код быстрее и эффективнее. Пускай и в ущерб каким‑то другим аспектам. Как мы уже знаем, возможности Dart в мета‑программировании очень скудные: отсутствие рефлексии, макросов и других выразительных особенностей приводят к тому, что довольно остро встает вопрос ручного труда. И, как следствие, приходится однотипный код (причем в довольно большом объеме) писать ручками. Мы попали в ситуацию, когда технический прогресс вроде бы должен упрощать работу, а по факту добавляется еще больше рутинной работы (жаль, нам не платят за количество символов, набранных ручками).
Напомню, что уже в C были макросы, да, не самые удобные и безопасные, но однозначно позволяющие довольно сильно сократить объемы рутинного кода. Но все, что мы имеем в Dart, — это кодогенерация. И проблема не в самой кодогенерации как таковой. Ведь многие другие ЯП также обладают кодогенерацией (например, тот же C#). Но реализована она на порядок лучше. Стоит начать с того, что кодогенерация в Dart — это внешняя команда (другими словами, с самим процессом компиляции / запуска приложения никак не связанная). И ее нужно запускать самому вручную. А это опять лишние телодвижения, задержки и, как следствие, чтение какой‑нибудь статьи.
Не поймите меня неправильно: читать статьи тоже нужно, но не целый же день?
Другой немаловажный момент — это скорость, с которой кодогенерация происходит. Если вы вдруг не в курсе, то это очень медленно. Я бы даже сказал, непозволительно медленно. А так как Dart ничего не может, то на любой чих стоит выбор: либо писать ручками, либо кодогенерация. Куча разного рода пакетов на pub.dev занимаются тем, что генерируют какой‑то код. И чем больше пакетов в проекте используется, тем дольше происходит процесс генерации. А подключать пакеты для генерации кода приходится буквально на каждый чих, начиная от парсинга пресловутого Json и Swagger, заканчивая стейт‑менеджерами, DU и роутингом. Ну а дальше все по классике: мы имеем N пакетов, генерирующих код. Но и часть пакетов может генерировать одно и тоже). Простой пример: для генерации Json вы используете json_serializable
, а для генерации Open API — openapi-generator
, который использует пакет built_value
. И — вуаля — у нас два разных пакета для работы с Json в одном проекте. Думаю, идея понятна.
Еще одна проблема с кодогенераций состоит в том, что написать свой, хоть сколько‑нибудь интересный, кодогенератор ой как не просто. Если вообще возможно. Практически полное отсутствие документации и невероятно маленькое количество примеров сделает первые попытки написать хоть что‑то стоящее незабываемыми. Единственный вариант — это смотреть, как реализованы другие пакеты, пытаться понять и реализовать самому. Как бы немножко не этого ждешь от компании с названием Google.
Нетрудно заметить, что Dart не блещет даже в такой повседневной и простой задаче, как парсинг JSON. Что уж говорить о большем...
Аналогичные ситуации получаются и в других местах. Другой до боли банальный пример — это copyWith
. Dart, как и все его современники, идет в сторону ФП. А там, как известно, правят чистые функции и иммутабельные типы данных. А там, где есть иммутабельность, сразу встает вопрос о том, как удобно «мутировать» наши иммутабельные объекты. В Dart для решения этой проблемы принято реализовывать метод copyWith
. И тут опять два пути: либо писать ручками, что утомительно и небезопасно, либо использовать генераторы. Да даже для такой простой штуки, как скопировать объект с небольшими изменениями, необходима генерация. Ну, а с ней и все соответствующие проблемы...
Грабли
Наступать на свои грабли гораздо интереснее. Dart — не первый ЯП, разрабатываемый компанией Google. Есть у него старший (и более успешный) брат — Go. Прославился он тем, что его создатели (кто бы мог подумать) тоже не очень в проектировании (отсутствие дженериков и все такое). Но мне больше всего запомнился другой интересно спроектированный момент: в Go нельзя удалить ключ со значением NaN
(ну и другие аналогичные проблемы, по типу того, что может быть несколько ключей со значением NaN
) из словаря. И для решения этой проблемы вводят новую функцию или есть предложения запретить использование вещественных чисел в качестве ключей словаря. Ну разве это не прекрасно?) Да, безусловно, Equality Is Hard. Ну все таки не до такой же степени? Другие языки как‑то справляются. Не то чтобы это критично. Но все же выглядит как огромный неповоротливый костылище.
Мы немного отошли от темы. Так что же с вещественными ключами в словарях в Dart? В Dart все есть Object
. А Object
в свою очередь реализует контракт для hashCode
и ==
(нужно ли говорить, что это не самый лучший подход к решению данной проблемы?). Таким образом мы можем использовать вещественные числа как ключи в словаре. Попробуйте предположить, какой результат получится, если мы выполним следующий код:
void main() {
final map = <double, int>{double.nan: 5};
print(map);
map.remove(double.nan);
print(map);
}
Возможно это будет для вас сюрпризом, но результат будет такой:
{NaN: 5}
{NaN: 5}
Другими словами, в Dart есть аналогичные языку Go проблемы со словарями и вещественными числами в частности (и с эквивалентностью в целом). Конечно, все не так плохо, как в Go. И никто не реализует отдельный специальный метод для очистки словаря, но все же реализация далека от идеала. Не могу точно сказать, было ли это поведение заранее спланировано (так сказать, by design) или нет, и это получилось само собой. Не смог найти соответствующей информации. Если вдруг вы в курсе, был бы рад почитать про этот момент. Конечно же, нельзя не упомянуть, что в фундаментально испорченном JS аналогичный код работает корректно. Или именно это — один из тех изъянов, который авторы Dart хотели исправить?)
Раз уж речь зашла про словари, то грех не упомянуть о другой неприятной особенности Dart — hashcode
. Объективно говоря, это проблема присуща не только Dart, но и другим мейнстрим языкам (но все же с Dart спрос выше). Я уже писал об этом здесь, в том числе с примерами из реальной жизни. В Dart плюс‑минус все то же самое. Если кратко, то при реализации иммутабельных классов чаще всего необходимо, чтобы они вели себя как значимые типы. Для этого переписывается метод equals
, и, как следствие, необходимо корректно реализовать и hashcode
. Ну а далее у нас опять‑таки есть два пути: мы либо реализуем все ручками (явно или используя пакет equatable
), либо используем кодогенератор. В первом варианте довольно легко пропустить какое‑то поле, что приведет к некорректной работе приложения. Со вторым вариантом и так все понятно — больше кодогенерации Богу кодогенерации... Почему нельзя было возложить столь рутинную операцию на компилятор, решительно непонятно...
Сравнение с современниками
Изначально я планировал сделать небольшое сравнение Dart с его современниками (TS, Kotlin, Swift, Rust и другими). Но, во‑первых, статья и так получилась большой, а, во вторых, смысла сравнивать нет. Ибо как бы это странно (и возможно обидно) ни звучало, но между ними и Dart огромная пропасть. Dart — явный аутсайдер, и находится в состоянии догоняющего. Печально, ведь у Google есть все возможности для реализации действительно качественного, современного и крутого во всех смыслах ЯП. Но, как ни странно, он ими не пользуется. Впрочем, все как всегда.
Итого
Высокоуровневые средства автоматизации (к которым Dart, безусловно, относится) создаются, кроме прочего, для ускорения процесса разработки. Но создатели Dart имеют свой взгляд на повседневные вещи. И хотя в общем нельзя назвать разработку на Dart медленной, все же кодогенерацию необходимо упрощать и ускорять. А языковые выразительные способности расширять. Но, как говорится, что имеем, на том и едем.
Мы рассмотрели далеко не полный список проблем Dart как ЯП. Но все же не могу не отметить, что совсем скоро положение дел улучшится с выходом Dart 3. Dart 3 находится в альфа‑версии. Мажорная версия богата на новый функционал. Обещают завести:
-
кортежи / дата классы;
-
размеченные объединения;
-
сопоставление с образцом;
-
новые модификаторы классов;
-
статическое мета‑программирование;
-
и другие улучшения.
Вот‑вот заживем.
Средства разработки, которые иногда не только не помогают, но и немножечко вредят
Безусловно, среда разработки очень сильно влияет на сам процесс разработки. В нее входит огромное количество различных средств, которые ускоряю и упрощают разработку. Таким образом, от качества (да и вообще от ее наличия) будет многое зависеть. Одно дело писать код в блокноте с подсветкой, и совсем другое — в полноценной IDE. Не мне вам рассказывать. Не так сложно написать новый ЯП, в сравнении с тем, сколько всего необходимо сделать, продумать, спроектировать и реализовать для качественной поддержки нового ЯП существующими средами разработки. По ощущениям, как Земля и Солнце. Официально Dart поддерживается двумя полноценными IDE. Это Android Studio / Intellij Idea и Visual Studio Code. Так как я в основном работаю с Intellij Idea, то речь дальше пойдет в большей степени о ней. А точнее, о языковом плагине Dart для этой IDE. Не знаю, как обстоят дела VSC: возможно, там поддержка находится на более высоком уровне. Поэтому имейте это ввиду.
По большому счету, все есть. Нет каких‑то явных критических проблем, мешающих работе, наподобие тех, что крешат IDE (постоянно приходится перезапускать и все такое). В целом, все далеко не плохо. Но, опять‑таки, это если сравнивать в среднем по больнице, не углубляясь в подробности. Но если углубиться и сравнить, например, с поддержкой Java в Intellij Idea, C# в Rider или JS/TS в WebStorm, то, конечно, все далеко не в пользу поддержки Dart. Да что уж там говорить, даже если делать сравнение с поддержкой ActionScript 3, то и ему проигрывает.
Code completion
Начать стоит с того, что печалит больше всего: Code completion. Подсказки, конечно, есть, но, во‑первых, далеко не всегда, а, во вторых (что еще хуже), работают они невероятно медленно. Периодически вообще возникает ситуация, когда, вроде бы, не делал каких‑то кардинальных изменений в коде (было сделано что‑то простое, по типу: добавил символ или убрал), и вдруг пропадает или ломается подсветка (как будто сдвигается), и, само собой, не работают подсказки кода. Такое ощущение, что сервис анализа кода падает или перезапускается. Зависание это может происходить довольно долго, где‑то секунд по 20. Хорошо, то что такие зависания происходят нечасто. Эти зависания, да и вообще, скорость работы подсказок не особо зависят от размера проекта. Даже в пустом новом проекте все работает неспешно и, бывает, пропадает подсветка. Окошко с подсказками кода может показываться не сразу. И непонятно, в чем причина: то ли долго ищутся подсказки, то ли просто что‑то мешает. При показе довольно часто видна желтая иконка с восклицательным знаком. Иногда мне кажется, что это она мешает, но, скорее всего, это просто совпадение.
Кроме того, что подсказки кода работают медленно, в некоторых местах они вообще не показываются. Самый банальный пример — это при объявлении поля. Подсказки для типа не работают ровно до тех пор, пока не будет написано имя поля. Дальше все начинает работать как надо. Есть и другие подобные случаи. Например, с функцией max
. Поведение очень похоже.
Еще один момент с подсказками кода — это импортирование каких‑то классов. И тут сразу несколько проблем:
-
необходимого класса по каким‑то причинам нет в списке. Но после нескольких попыток вызова подсказок класс может появиться. Иногда даже это помогает;
-
необходимый класс есть, и полное имя подставляется, а вот импорт — нет. И повторный вызов подсказки с уже полностью введенным именем класса тоже далеко не всегда срабатывает;
-
не подсказываются стандартные классы. Например, если набрать
EdgeIn
в надежде увидетьEdgeInsets
, то все, что мы увидим, — этоEdgeInsetsGeometryTween
иEdgeInsetsTween
. Не самые часто используемые классы. Но если подождать (или попытаться вызвать подсказку еще пару раз), то может произойти чудо, иEdgeInsets
появится вместе с еще парой других. Но даже так в подсказках будут не все доступные конструкторы. -
аналогичная проблема с методами расширения. Раньше подсказки вообще не работали, теперь стало лучше, но, опять‑таки, далеко не всегда.
А ведь подсказки кода — это самая часто используемая функция IDE...
Отладка
Плагин Dart имеет отладчик. Но функционал его довольно ограничен, и работает отладка очень неспешно. Поначалу так вообще отладка и соединение с приложением прерывались после срабатывания точки остановки. Видимо, происходило какое-то исключения. Сейчас такого нет.
-
Сам по себе процесс работает довольно медленно. Точка остановки срабатывает, а данные для всех окошек подгружаются с приличной задержкой (Collecting data). Сначала отображаются панели со значениями переменных, а после еще одной задержки (Loading) стектрейс;
-
геттеры и геттеры расширения не отображаются в панели со значениями переменных;
-
аналогично нет подсказок со значениями переменных при наведении на геттеры и геттеры расширения в коде;
-
подсказки по наведению на выражения показывают полный результат выражения вместо значения конкретной переменной;
-
не показываются подсказки для констант;
-
step out работает очень странно. Ощущение, что скорее не работает вообще, и больше похоже на step in.
Рефакторинг
Список возможностей по рефакторингу в Dart довольно скромен. Да и те функции, что реализованы, работают не всегда корректно. Самый яркий пример — это переименование. Здесь много разных проблем, начиная от невозможности переименовать вообще, заканчивая тем, что не все переименуется как должно быть. Как результат — некомпилируемый код с некорректными импортами.
Аналогичная ситуация происходит и при перемещении файлов. Особенно часто это происходит с Bloc
. Так как файлы блока являются частью основного (part of
), то перемещение или переименование может просто оставить неработающий код, который потом придется вручную доправлять. Сообщения о невозможности выполнить рефакторинг не редкость.
Кучи часто используемых рефакторингов просто нет. Рефакторинг, генерирующий колбек, не учитывает обязательность полей, из‑за чего сгенерированный код некорректен. С этой частью все очень грустно.
Тесты
import 'package:test/test.dart';
void main() {
test('String.split() splits the string on the delimiter', () {
var string = 'foo,bar,baz';
expect(string.split(','), equals(['foo', 'bar', 'baz']));
});
test('String.trim() removes surrounding whitespace', () {
var string = ' foo ';
expect(string.trim(), equals('foo'));
});
}
Плагин Dart поддерживает запуск тестов. Само собой, есть нюансы:
-
Во‑первых (да и в главных) тесты в Dart (см. код выше) — это обычная программа на Dart, которая использует пакет для тестирования. И тесты представляют собой вызов функции, принимающей название теста, и функцию, в которой, собственно, и происходит выполнение какой‑то проверки. Таким образом, это позволяет при запуске программы определить список тестов, не выполняя их. А также выполнять отдельные тесты по имени. Опять сказывается скудость языковых средств Dart (отсутствие рефлексии). И, вроде, на первый взгляд это все выглядит неплохо. Но теперь давайте рассмотрим несколько проблем. Тестовая панель в IDE, используемая для запуска тестов, запускает их не сама, а используя глобальную конфигурацию. Отсюда довольно частая проблема: вместо запуска приложения запускается тест (из‑за того, что до этого запускались тесты), но ты не обращаешь на это внимание и ждешь некоторое время. А приложение так и не запустится, так как запустился тест. И приходится выбирать ручками конфигурацию приложения, запускать снова и снова ждать. Довольно сильно раздражает. В других языках я такого поведения не встречал. Запуск тестов из панельки не влияет на глобальную конфигурацию.
-
Плагин Dart анализирует файлы тестов и добавляет иконку их запуска напротив соответствующего вызова функции
test
, благодаря чему можно запускать тесты по одному. Но проблема здесь в том, что довольно часто все или часть иконок пропадает во время редактирования файла. И получается, что запустить один тест из всех ты не можешь. -
Отладка тестов тоже хромает. Неоднократно сталкивался, что точки срабатывают и для других тестов, несмотря на то, что был запущен один конкретный. Это довольно неприятно. Приходится комментировать тесты, чтобы отладить какой‑то конкретный.
Работа со встроенными Dev Tools тоже не всегда гладкая. Из моей практики: чаще всего происходит сбой в работе при использовании инспектора виджетов. Из чужой практики: это креши профайлера.
Естественно, это далеко не полный список проблем, а лишь те, что пришли на ум при написании статьи. Думаю, описывать полный список смысла нет. Общая ситуация должна быть понятна и так.
Flutter
Flutter на данный момент использует Dart как основной ЯП для разработки. Думаю, можно сказать, что Flutter является одной из основ становления Dart как ЯП. Скорее всего, если бы Flutter не появился, то и Dart бы умер. И на данный момент Flutter — это единственный причина существования Dart. Более того, многие изменения, происходящие с Dart, явно или неявно исходят от команды Flutter. Таким образом, можно сказать, что Flutter регулирует то, как Dart развивается сейчас и в будущем. По этому можно прикинуть примерные перспективы его развития.
Сам по себе Flutter как фреймворк вполне можно было бы реализовать на любом другом языке программирования. Но звезды сошлись так, как сошлись. И теперь, чтобы разрабатывать мультиплатформенные приложения, придется изучать Dart, который нигде больше не используется. А тенденций его использования в других сферах пока не наблюдается. Думаю, немало причин его непопулярности мы уже обсудили выше. Но поживем — увидим, что будет дальше.
Flutter — это фреймворк для разработки кроссплатформенных приложений. И зарекомендовал он себя довольно неплохо. Если смотреть поверхностно, то и придраться особо не к чему. Основная идея — это возможность декларативно описывать интерфейс пользователя, при этом не используя никакой DSL для этого. т. е. только чистый Dart код и подход, в котором все есть виджет. Получилось модно и современно. На практике ощущается все так же очень и очень не плохо.
Кастомизация
По большому счету. к Flutter не так и много претензий. Пожалуй, главная — это практически полное отсутствие каких бы то ни было стандартных компонентов. Во Flutter фактически существует два (встроенных) набора компонентов. Это виджеты Material и виджеты Cupertino. Для андроида и айОС приложений соответственно. Позиция авторов этих виджетов такая, что они строго следуют Material дизайну и позволяют кастомизировать только то, что позволяет спецификация. На самом же деле все несколько сложнее, и многие компоненты из группы Material могут быть изменены вопреки спецификации. Но, в любом случае, все подобные изменения делаются через стандартные темы и очень сильно ограничены. Буквально взяв простейший CheckBox, ты не можешь как‑то легко изменить галочку на какую‑то слегка другую. т. е. механизма наподобие скинов нет. И выход один: писать свой компонент. И так практически со всеми компонентами. Шаг влево, шаг вправо — расстрел. Как‑то негласно считается, что реализовывать виджеты во Flutter ну очень легко. И вроде как это не является проблемой. На самом же деле все не так просто. Есть простые компоненты, которые легко сверстать и реализовать, а есть куда более сложные. Flutter — это, пожалуй, первый UI фреймворк (с которыми я работал), в котором просто нет каких‑то базовых компонентов. Например, той же кнопки. Ведь кнопка — это не просто картинка с текстом. Это довольно сложный виджет. И во Flutter нет базового компонента. Но есть много виджетов кнопок, которые реализуются каждый раз снова. т. е. кнопки из Material и кнопки из Cupertino — это две абсолютно разные кнопки. Несмотря на то, что в них много общего. Пример аналогичных фрустраций.
Вообще, вся эта ситуация еще сильнее усугубляется, если попытаться выйти за пределы уютного мирка мобильных приложений. Например, в веб или на десктоп (Flutter — это же кроссплатформенные приложения). И тут оказывается, что компонентов нативно (похоже на аналогичные нативные компоненты) работающих просто нет. И придется реализовывать их самому. Ну, либо использовать мобильные виджеты. Таким образом, количество виджетов, которые необходимо реализовать (а в последующем и поддерживать), увеличивается. Логично встает вопрос: если стандартные компоненты приходится переписывать для кастомизации под конкретные проекты, то зачем Material и Cupertino виджеты гвоздями прибиты к Flutter SDK? Не логичнее ли было вынести их как отдельные пакеты и подключать по необходимости? А в самом SDK оставить только те, которые являются базовыми в некоторых аспектах? Такие, как базовая кнопка, которой не хватает всего лишь скина? В таком случае аналогично можно было бы сделать и компоненты — пакеты для других ОС. В том числе и сторонние. Сейчас работа с кастомизацией выглядит как один большой костыль. И это печалит.
Hot Reload
Одной из полезных фич Flutter (и Dart, соответственно) является Hot Reload. Эта фича позволяет обновлять код без потери состояния приложения, что, несомненно, звучит очень многообещающе. Но на практике работает это все не сильно хорошо. Есть разного рода проблемы. По разным причинам обновления не всегда подхватываются, особенно если в коде много статики. Не работает обновление ассетов (возможно, в будущем это поправят). Используя Hot Reload, неоднократно сталкивался с ситуацией, что что-то не подхватилось или не заработало и только из-за Hot Reload. Не сразу становится понятно, что изначальная причина именно в ней, а не в чем-то еще. Выяснение требует времени. Как итог, пока что стараюсь не использовать эту функцию, хотя большинство коллег пользуются ею на ура. А вы используете эту возможность? Были ли с ней проблемы?
pub.dev
pub.dev — это официальный репозиторий пакетов для Dart и Flutter проектов, фактически, полный аналог npm, только для Dart/Flutter экосистемы. Работает это все довольно неплохо. Но есть некоторые нюансы, так сказать, по содержанию. По многим популярным аспектам разработки существуют как официальные, так и сторонние пакеты. Хоть выбор не особо большой, но чаще всего что‑то найти можно. Совсем другой вопрос — это качество этих самых пакетов. Конечно, стоит держать в уме, что одна из основных причин — это относительная молодость технологии, и в будущем, скорее всего, все выправится, но указать все же стоит.
Многие пакеты, скажем так, достаточно посредственного качества. И если претензии по этому поводу к сторонним пакетам не особо обоснованы (открытый исходный код, и все дела), то же самое сказать про официальные пакеты уже нельзя. Самым ярким примером я бы назвал go_router. Это официальный пакет для реализации декларативного роутинга. Очень сильно выделяется по качеству на фоне остальных. И, несмотря на очень высокую популярность (согласно pub.dev — 2.5к лайков), на мой скромный взгляд, это — худший пакет (сторонняя либа), с которым мне приходилось иметь дело. Это одна большая головная боль.
Нестабильный API, большое количество багов, проблемный генератор роутов, сложности в вебе. И все это подается под соусом лучшего решения для Flutter — проекта (особенно кроссплатформенного с вебом). Конечно, в какой‑то момент этот пакет будет доведен (я надеюсь) до более или менее вменяемого состояния, и им можно будет пользоваться. А пока точно не стоит.
Аналогичная ситуация происходит и с самим Flutter. Нет‑нет, да появляются явно сырые виджеты, которые работают из рук вон плохо. Один из таких виджетов — это SelectableArea
. И бог с ними, с конкретными виджетами, но даже целый функционал релизится, несмотря на то, что он далек от релизного состояния. Как пример, та же поддержка веб. Другими словами, имеют место количество и частота релизов в угоду качеству.
Конечно, в общем проблема не в конкретном пакете, а, скорее, в целом в том, что многие вещи командой флаттера отдаются на откуп комьюнити. Но на данном этапе комьюнити не настолько большое, и не поспевает за закрытием всех дыр. По моему скромному мнению, Flutter стоило расходовать силы на более важные вещи, нежели освоение новых ОС. Ситуация выглядит так, что официального решения довольно многих проблем нет, а сторонние решения — это чаще всего просто какой‑то код, худо‑бедно доведенный до состояния пакета, который решает проблему только частично. Смотришь базовые вещи, такие как валидация ввода, а нормального решения из коробки нет, и только десятки разных пакетов в непонятно каком состоянии работоспособности.
Заключение
Кроме прочего, можно выделить ряд других проблем:
-
не нативные UI виджеты;
-
уровень производительности (особенно в вебе);
-
отсутствие динамического обновления с бека;
-
state management ecosystem is a mess;
-
нет возможности подгружать динамические библиотеки в run-time;
-
нет официальной поддержки аудио (только ограниченные сторонние пакеты);
-
нет стандартной поддержки векторной графики. Работа с SVG отвратительная;
-
нет нормальных точек расширения (хуков) компилятора и других программ из SDK;
-
нет официальной поддержки работы с оповещениями;
-
довольно высокий риск, что Google послезавтра убьет и Dart и Flutter;
-
и другие...
Но останавливаться на них подробнее смысла нет. Все и так понятно.
На этом, пожалуй, все. Было бы интересно узнать, что конкретно вас не устраивает в Dart и Flutter. Возможно, вы всем довольны? Чтобы было бы здорово улучшить или исправить? Имеет ли Dart право на жизнь, или стоило использовать уже существующий ЯП? Спасибо за внимание, и до скорых встреч.
Ссылки
Автор: Журат Максим