paint-brush
Как создать серверный движок пользовательского интерфейса для Flutterк@alphamikle
966 чтения
966 чтения

Как создать серверный движок пользовательского интерфейса для Flutter

к Mike Alfa30m2024/06/13
Read on Terminal Reader

Слишком долго; Читать

История создания Nui — движка пользовательского интерфейса, управляемого сервером, для Flutter, который является частью более крупного проекта — Backendless CMS Nanc.
featured image - Как создать серверный движок пользовательского интерфейса для Flutter
Mike Alfa HackerNoon profile picture
0-item
1-item

Привет!

Сегодня я покажу вам, как создать супер-пупер движок для Server-Driven UI во Flutter , который является неотъемлемой частью супер-пупер CMS (именно так его создатель, то есть я, позиционирую его). У вас, конечно, может быть другое мнение, и я буду рад обсудить его в комментариях.


Эта статья — первая из двух (уже трёх) серии. В этой мы рассмотрим непосредственно Nui, а в следующей — насколько глубоко Nui интегрирован с Nanc CMS, а между этой и следующей статьей будет еще одна с огромным количеством информации о производительности Nui.


В этой статье будет много интересного о Server-Driven UI, возможностях Nui (Nanc Server-Driven UI), истории проекта, корыстных интересах и Докторе Стрэндже. Ах да, еще будут ссылки на GitHub и pub.dev, так что если вам понравится и вы не прочь потратить 1-2 минуты своего времени — буду рад вашей звездочке и лайку .


Оглавление

  1. вступление
  2. Причины развития
  3. Доказательство концепции
  4. Синтаксис
  5. Редактор IDE
  6. Производительность
  7. Компоненты и создание пользовательского интерфейса
  8. Детская площадка
  9. Интерактивность и логика
  10. Обмен данными
  11. Документация
  12. Планы на будущее

Небольшое введение

Я уже писал статью о Nanc, но с тех пор прошло больше года и проект значительно продвинулся по возможностям и «полноте», а главное — вышел с готовой документацией , и под MIT. лицензия.

Так что же такое Нанк?

Это CMS общего назначения, которая не тянет за собой свой бэкенд. В то же время это не что-то вроде React Admin, где, чтобы что-то создать, нужно писать тонны кода.


Чтобы начать использовать Nanc, достаточно:

  1. Опишите структуры данных, которыми вы хотите управлять через CMS с помощью Dart DSL.
  2. Напишите уровень API, который реализует связь между CMS и вашим сервером.


Причём первое можно сделать полностью через интерфейс самой CMS — то есть управлять структурами данных можно через UI. Второй можно пропустить, если:

  1. Вы используете Firebase
  2. Или вы используете Supabase
  3. Или вы хотите поиграться и запустить Nanc, не привязывая его к реальному бэкенду — с локальной базой данных (пока эту роль играет JSON-файл или LocalStorage)


Таким образом, в некоторых сценариях вам не придется писать ни строчки кода, чтобы получить CMS для управления любым вашим контентом и данными. В будущем количество этих сценариев, скажем так, увеличится — плюс GraphQL и RestAPI. Если у вас есть идеи, для чего еще можно было бы реализовать SDK — буду рад прочитать предложения в комментариях.


Nanc оперирует сущностями — они же моделями, которые на уровне хранения данных могут быть представлены как таблица (SQL) или документ (No-SQL). У каждой сущности есть поля — представление столбцов из SQL, или те же «поля» из No-SQL.


Одним из возможных типов полей является так называемый тип «Экран». То есть вся эта статья — это текст всего лишь одного поля из CMS. При этом архитектурно это выглядит так — существует совершенно отдельная библиотека ( фактически несколько библиотек ), которые вместе реализуют Server-Driven UI Engine под названием Nui. Этот функционал интегрирован в CMS, поверх которого накатывается множество дополнительных возможностей.


На этом я завершаю вступительную часть, посвященную непосредственно Нанк, и начинаю рассказ о Нуи.

Как это все началось

Отказ от ответственности: Все совпадения случайны. Эта история вымышленная. Мне это приснилось.

Я работал в одной крупной компании сразу над несколькими приложениями. Они были во многом схожи, но имели и множество различий.

Но что было в них совершенно одинаково, так это то, что я могу назвать движком статьи . Он состоял из нескольких (5-10-15, уже точно не помню) тысяч строк довольно скомканного кода, обрабатывавшего JSON с бэкенда. Эти JSON в конечном итоге пришлось превратить в UI, а точнее, в статью, которую можно было бы прочитать в мобильном приложении.


Статьи создавались и редактировались с помощью админ-панели, а процесс добавления новых элементов был очень, невероятно, крайне мучительным и долгим. Увидев этот ужас, я решил предложить первую оптимизацию — помиловать бедных контент-менеджеров и реализовать для них функционал предпросмотра статей в реальном времени прямо в браузере, в админке.


Сказано и сделано. Через некоторое время в админке крутился тощий кусок приложения, экономя контент-менеджерам кучу времени на предпросмотре изменений. Если раньше им приходилось создавать глубокую ссылку, а затем для каждого изменения открывать дев-сборку, переходить по этой ссылке, ждать загрузки, а потом все повторять, то теперь они могли просто создавать статьи и сразу их видеть.


Но на этом моя мысль не остановилась — меня слишком раздражал этот движок , да и другие разработчики, так как можно было определить, нужно ли им что-то в него добавить или просто почистить авгиевы конюшни .


В последнем случае разработчик всегда был в хорошем настроении на встречах, хотя запах… камера этого не уловит.

В первом случае разработчик часто болел, пережил землетрясения, сломанный компьютер, головные боли, удары метеорита, терминальную стадию депрессии или передозировку апатии.


Расширение функциональности движка также потребовало добавления множества новых полей в панель администратора, чтобы контент-менеджеры могли использовать новые функции.


Глядя на все это, меня осенила невероятная мысль: почему бы не создать общее решение этой проблемы? Решение, которое избавило бы нас от необходимости постоянно настраивать и расширять админку и приложение под каждый новый элемент. Решение, которое решит проблему раз и навсегда! И вот приходит...

Хитрый жадный маленький план

Я подумал — «Я могу решить эту проблему. Я могу спасти компанию многие десятки, если не сотни тысяч; но идея может оказаться слишком ценной для компании, чтобы просто отдать ее в дар ».


Под подарком я имею в виду, что соотношение потенциальной ценности для компании на порядки отличается от того, что компания мне заплатит в виде зарплаты. Это как если бы вы пошли работать в стартап на ранней стадии, но за зарплату меньшую, чем вам предлагают в какой-нибудь крупной компании, и без доли в компании. А потом стартап становится единорогом, и тебе говорят — «Ну чувак, мы тебе зарплату заплатили». И они будут правы!


Я люблю аналогии, но мне часто говорят, что они не моя сильная сторона. Это как будто ты рыба, которая любит плавать в океане, но ты пресноводная рыба.


А потом — я решил в свободное время сделать доказательство концепции (POC), чтобы не облажаться, предложив какие-то идеи, которые, возможно, даже невозможно будет реализовать.

Доказательство концепции

Изначально планировалось использовать уже существующую готовую библиотеку для отрисовки уценки, но расширить ее возможности, чтобы она могла рендерить не только стандартные элементы из уценочного списка, но и что-то гораздо более сложное. Статьи представляли собой не просто текст с картинками. Также было красивое визуальное оформление, встроенные аудиоплееры и многое другое.


Я потратил 40 часов, считая с вечера пятницы до утра понедельника, чтобы проверить эту гипотезу — насколько эта библиотека расширяема для новых возможностей, насколько хорошо всё работает в целом, и самое главное — сможет ли это решение свергнуть пресловутый движок с трона. Гипотеза подтвердилась - после разбора библиотеки до костей и небольшого патчинга появилась возможность регистрировать любые элементы пользовательского интерфейса по ключевым словам или специальным синтаксическим конструкциям, все это легко расширяется, а главное - действительно может заменить движок статей . Я пришел где-то через 15 часов. Остальные 25 я потратил на доработку POC.


Идея была не просто в замене одного двигателя на другой - нет. Идея была в том, чтобы заменить весь процесс! Панель администратора не только позволяет создавать статьи, но и управлять контентом, видимым в приложении. Первоначальная идея заключалась в создании полноценной замены, которая не была бы привязана к конкретному проекту, но позволяла бы им управлять. Самое главное — эта замена должна еще и предоставить удобный редактор этих самых статей, чтобы их можно было создавать и сразу видеть результат.


Для POC я думал достаточно будет просто сделать редактор. Это выглядело примерно так:

Редактор пользовательского интерфейса

Через 40 часов у меня был рабочий редактор кода, состоящий из бурной смеси уценки и кучи кастомных XML-тегов (например, <container> ), превью, отображающее UI из этого кода в реальном времени, а также самый большой мешки под глазами, которые этот мир когда-либо видел. Также стоит отметить, что используемый "редактор кода" - это еще одна библиотека, способная подсвечивать синтаксис, но беда в том, что она умеет подсвечивать markdown, она же умеет подсвечивать и XML, но подсветка сборной мешанины постоянно ломается. Так что за 40 часов можно добавить еще парочку для обезьянкокодирования химеры, которая обеспечит подсветку обоих в одном флаконе. Пришло время спросить – что было дальше?


Первая демонстрация

Дальше было демо. Я собрал пару топ-менеджеров, объяснил им свое видение решения проблемы, то, что я подтвердил это видение на практике, и показал, что и как работает, и какие у него есть возможности.


Работа ребятам понравилась. И появилось желание этим воспользоваться. Но была еще и гложущая жадность. Моя жадность. Разве я не мог просто так отдать его компании? Конечно, нет. Но я тоже не планировал. Демо было частью дерзкого плана, где я шокировал их своим мастерством, они просто не могли устоять и были готовы пойти на любые условия, лишь бы воспользоваться этой невероятной, эксклюзивной и потрясающей разработкой. Не буду раскрывать всех подробностей этой выдуманной (!) истории, скажу лишь, что мне хотелось денег. Деньги и отпуск. Оплачиваемый месячный отпуск, а также деньги. Сколько денег не так важно, важно лишь, чтобы сумма соответствовала моей зарплате и цифре 6.

Но я не был совсем безрассудным смельчаком.


Дормамму, я пришел поторговаться. А дело было в следующем — я работаю две полные недели в своем режиме ( сплю 4 часа, работаю 20 часов ), доводя POC до состояния «можно использовать для целей нашего приложения», и параллельно с этим реализую новая функция в приложении - целый экран, при использовании этой ультраштуки (на что изначально были отведены эти две недели). И в конце двух недель мы проводим еще одну демо-версию. Только на этот раз мы собираем больше людей, даже высшее руководство компании, и если то, что они видят, их впечатляет, и они захотят этим воспользоваться - сделка заключена, я получаю свои желания, а компания получает суперпушку. Если они ничего этого не хотят – я готов смириться с тем, что эти две недели я работал бесплатно.


Педра Фурада (около Урубичи)

Ну а поездка в Урубичи , которую я уже планировал на месячный отпуск, к сожалению, так и не состоялась. Ребята-менеджеры не решились согласиться на такую дерзость. И я, опустив взгляд в землю, пошел строгать новый экран «классическим способом». Но нет такой истории, в которой побежденный судьбой главный герой не встанет с колен и не попытается вновь приручить своего зверя.


Хотя нет... вроде есть: 1 , 2 , 3 , 4 , 5 .


Посмотрев все эти фильмы, я решил, что это знак ! А что так даже лучше - жалко продавать такую перспективную разработку за какие-то там вкусности ( кого я обманываю??? ), и я продолжу развивать свой проект дальше. И я продолжил. Но уже не 40 часов по выходным, а всего 15-20 часов в неделю, в относительно спокойном темпе.

Кодировать или не кодировать?

Сломать четвертую стену – задача не из легких. Точно так же, как пытаться придумать интересные заголовки, которые заставят читателя продолжать читать и ждать, чем закончится история с компанией. Закончу рассказ во второй статье. И вот, кажется, пришло время перейти к реализации, функциональным возможностям и всему тому, что, по идее, должно сделать эту статью технической, а HackerNoon — большей!

Синтаксис

Первое, о чем мы поговорим, — это синтаксис. Изначальная идея солянки подходила для POC, но как показала практика, с маркдауном все не так просто. Кроме того, объединение некоторых собственных элементов уценки с чисто Flutter не всегда согласовано.


Самый первый вопрос — будет ли изображение ![Description](Link) или <image> ?


Если первое - куда засунуть кучу параметров?

Если второе – почему тогда у нас первое?


Второй вопрос — тексты. Возможности Flutter по стилизации текстов безграничны. Возможности уценки «так себе». Да, можно выделить текст жирным или курсивом, и даже были мысли использовать эти конструкции ** / __ для стилизации. Потом были мысли запихнуть теги <color="red"> текст </color> посередине, но это настолько криво и криво, что кровь из глаз течёт. Получить какой-то HTML со своим маргинальным синтаксисом было вообще нежелательно. Плюс идея была в том, что этот код могли написать даже менеджеры без технических знаний.


Шаг за шагом я удалил часть химеры и получил уцененный супермутант. То есть мы получили пропатченную библиотеку для отрисовки маркдауна, но напичканную кастомными тегами и без поддержки маркдауна. То есть как если бы мы получили XML.


Я сел подумать и поэкспериментировать с тем, какие еще простые синтаксисы существуют. JSON — это шлак. Заставить человека писать JSON в кривом редакторе Flutter — получить маньяка, который захочет вас убить. И дело не только в этом, мне не кажется, что JSON подходит для набора человеком вообще, особенно для UI — он постоянно растёт вправо, куча обязательных "" , комментариев нет. ЯМЛ? Ну, возможно. Но код тоже будет ползти боком. Есть интересные ссылки, но с их помощью многого не добьешься. ТОМЛ? Пф-фф.


Хорошо, я все-таки остановился на XML. Мне казалось, да и сейчас кажется, что это довольно «плотный» синтаксис, очень хорошо подходящий для UI. Ведь HTML-верстальщики еще существуют, и здесь все будет даже проще, чем в сети ( наверное ).


Далее возник вопрос — было бы неплохо получить возможность некоторой подсветки/дополнения кода. А также логические конструкции, вроде {{ user.name }} . Потом я начал экспериментировать с Twig, Liquid, посмотрел еще какие-то шаблонизаторы, которые уже не помню. Но я столкнулся с другой проблемой — часть задуманного вполне можно реализовать на стандартном движке, скажем, Twig, но реализовать всё точно не получится. И да, хорошо, что будет автодополнение и подсветка, но они будут мешать только в том случае, если вы накатите свои новые возможности поверх стандартного синтаксиса Twig, которые понадобятся для Flutter. В итоге с XML всё получилось очень хорошо, эксперименты с Twig/Liquid не дали каких-то выдающихся результатов, а в определённые моменты я даже столкнулся с невозможностью реализации некоторых функций. Поэтому выбор все же остался за XML. О функциях мы поговорим подробнее, а пока сосредоточимся на автозаполнении и подсветке, которые были так заманчивы в Twig/Liquid.


IDE

Следующее, что хочу сказать, это то, что у Flutter кривый ввод текста. Они хорошо работают в мобильном формате. Также хорош в десктопном формате, если речь идет о чем-то, ну максимум 5-10 строк в высоту. Но когда речь идет о полноценном редакторе кода, где этот редактор реализован во Flutter — на него нельзя смотреть без слез. В Trello , где я отслеживаю все задачи, пишу заметки и идеи, есть такая «задача»:


Задача по изменению редактора кода пользовательского интерфейса


На самом деле, практически с самого начала работы над проектом я держал в голове идею заменить редактор кода Nui на что-то более адекватное. Скажем так — внедрить веб-представление с частью Open Source из VS Code. Но пока руки до этого не дошли, к тому же в голову пришло костыльное, но пока работающее решение проблемы кривизны этого редактора — использовать вместо него собственную среду разработки.


Достигается это следующим образом — создаём файл с UI-кодом (XML), в идеале с расширением .html / .twig , открываем этот же файл через CMS — Web/Desktop/Local/Deployed — не важно. И открыть этот же файл через любую IDE, хоть через веб-версию VS Code. И вуаля — вы можете редактировать этот файл в своем любимом инструменте и просматривать его в реальном времени прямо в браузере или где угодно.


Нанк + синхронизация IDE


В таком случае можно даже прикрутить полноценное автодополнение. В VS Code есть возможность реализовать это через пользовательские HTML-теги. Однако я не использую VS Code, мой выбор — IntelliJ IDEA и для этой IDE такого простого решения уже нет (ну по крайней мере не было, или по крайней мере я его не нашел). Но есть более общее решение, которое будет работать и там, и там — XML Schema Definition (XSD). Я потратил около 3-х вечеров, пытаясь разобраться в этом монстре, но успех так и не пришел, и в итоге я забросил это дело, оставив его до лучших времён.


Интересно еще и то, что в итоге после многих итераций экспериментов, обновлений, скажем так, движка, отвечающего за преобразование XML в виджеты, мы получили такое решение, для которого язык не особо важен. Так же, как носитель информации о структуре вашего UI, выбор в итоге пал на XML, но при этом вы можете спокойно скормить ему JSON и даже бинарную форму — скомпилированный Protobuf. И это подводит нас к следующей теме.


Производительность

В этом предложении размер статьи составит 3218 слов. Когда я начал писать этот раздел, чтобы сделать все качественно — нужно было написать множество тест-кейсов, сравнивающих производительность рендеринга Nui и обычного Flutter. Так как у меня уже был реализован демонстрационный экран, полностью созданный на Nui:


Демонстрация экрана Nalmart


нужно было изначально создать точное соответствие экрана (в контексте Flutter, конечно). В итоге ушло более 3-х недель, много переписывания одного и того же, улучшения процесса тестирования и получения всё более интересных цифр. А размер одного только этого раздела превысил 3500 слов. Поэтому я пришел к мысли, что имеет смысл написать отдельную статью, которая будет целиком и полностью посвящена работоспособности Нуи, как частного случая, и дополнительной цене, которую вам придется заплатить, если вы решите использовать Серверно-ориентированный пользовательский интерфейс как подход.


Но сделаю небольшой спойлер: я рассматривал два основных сценария оценки производительности — время первоначального рендеринга . Это важно, если вы решите реализовать весь экран в серверном пользовательском интерфейсе, и этот экран будет открываться где-то в вашем приложении.


Так что если этот экран очень тяжелый, то даже родной экран Flutter будет долго рендериться, поэтому при переключении на такой экран, особенно если этот переход сопровождается анимацией, будут видны лаги. Второй сценарий — это время кадра (FPS) с динамическими изменениями пользовательского интерфейса . Данные изменились — нужно перерисовать какой-то компонент. Вопрос в том, насколько это повлияет на время рендеринга, не повлияет ли настолько, что при обновлении экрана пользователь будет видеть лаги. И вот еще спойлер — в большинстве случаев вы не сможете сказать, что экран, который вы видите, полностью реализован на Нуи. Если вы встроите виджет Nui в обычный, родной экран Flutter (скажем, в какую-то область экрана, которая должна очень динамично меняться в приложении) — вы гарантированно не сможете этого распознать. Конечно, есть падения производительности. Но они таковы, что не влияют на FPS даже при частоте кадров 120FPS — то есть время одного кадра практически никогда не будет превышать 8ms . Это справедливо для второго сценария. Что касается первого - все зависит от уровня сложности экрана. Но даже здесь разница будет такой, что не повлияет на восприятие и не сделает ваше приложение эталоном для пользовательских смартфонов .


Ниже приведены три записи экрана с Pixel 7a (Tensor G2, частота обновления экрана была установлена 90 кадров (максимум для этого устройства), скорость записи видео 60 кадров в секунду (максимум в настройках записи). Каждые 500 мс положение элементов в список рандомизированный, по данным которого строятся первые 3 карты, а еще через 500мс статус заказа переключается на следующий. Сможете ли вы угадать, какой из этих экранов реализован полностью на Нуи?


PS Время загрузки изображений не зависит от реализации, так как на этом экране при любой реализации очень много Svg-изображений - все иконки, а также логотипы брендов. Все svg (как и обычные картинки) хранятся на GitHub, как хостинге, поэтому могут загружаться довольно медленно, что наблюдается в некоторых экспериментах.


YouTube:


Доступные компоненты — как создать пользовательский интерфейс

При создании Nui я придерживался следующей концепции — необходимо создать такой инструмент, которым, в первую очередь, разработчикам Flutter будет так же легко пользоваться, как и создавать обычные Flutter-приложения. Поэтому подход к именованию всех компонентов был простой — назвать их так же, как они называются во Flutter.


То же самое относится и к параметрам виджета — скалярам, таким как String , int , double , enum и т. д., которые в качестве параметра сами по себе не настраиваются. Эти типы параметров в Nui называются аргументами . И к сложным параметрам класса, таким как decoration в виджете Container , называемому свойством . Это правило не является абсолютным, поскольку некоторые свойства слишком многословны, поэтому их имена упрощены. Также для некоторых виджетов расширен список доступных параметров. Например, чтобы создать квадратный SizedBox или Container , вы можете передать только один size пользовательских аргументов вместо двух одинаковых width + height .


Я не буду приводить полный список реализованных виджетов, так как их довольно много (на данный момент 53). Короче говоря, вы можете реализовать практически любой пользовательский интерфейс, для которого в принципе имело бы смысл использовать Server-Driven UI в качестве подхода. Включая сложные эффекты прокрутки, связанные с Slivers .


Реализованные виджеты



Также относительно компонентов стоит отметить точку входа или виджет, которому вам придется передать облачный XML-код. На данный момент таких виджетов два — NuiListWidget и NuiStackWidget .


Первый по замыслу следует использовать, если вам нужно реализовать весь экран. Внутри это CustomScrollView , содержащий все виджеты, которые будут анализироваться из исходного кода разметки. Причем парсинг, можно сказать, «интеллектуальный»: поскольку содержимым CustomScrollView должны быть slivers , то возможным решением было бы обернуть каждый из виджетов в потоке в SliverToBoxAdapter , но это имело бы крайне негативное влияние. по производительности. Поэтому виджеты встраиваются в своего родителя следующим образом — начиная с самого первого идем по списку вниз, пока не встретим настоящую sliver . Как только встречаем sliver — добавляем все предыдущие виджеты в SliverList , и добавляем в родительский CustomScrollView . Таким образом, производительность рендеринга всего пользовательского интерфейса будет максимально высокой, поскольку количество slivers будет минимальным. Почему плохо иметь много slivers в CustomScrollView ? Ответ здесь .


Второй виджет — NuiStackWidget также можно использовать как полноэкранный — в этом случае стоит иметь в виду, что все, что вы создадите, будет встроено в Stack в том же порядке. А еще необходимо будет явно использовать slivers — то есть, если вы хотите список slivers — вам придется добавить CustomScrollView и уже реализовать список внутри него.


Второй сценарий — реализация небольшого виджета, который можно встроить в нативные компоненты. Скажем так — сделать карточку товара, которая будет полностью настраиваемой по инициативе сервера. Это кажется очень интересным сценарием, в котором вы можете реализовать все компоненты библиотеки компонентов с помощью Nui и использовать их как обычные виджеты. При этом всегда будет возможность полностью их изменить, не обновляя приложение.


Стоит отметить, что NuiListWidget также можно использовать как локальный виджет, а не на весь экран, но для этого виджета потребуется применить соответствующие ограничения, например, задать явную высоту родительского виджета.


Вот как бы выглядело counter app , если бы оно было создано с использованием Flutter:

 import 'package:flutter/material.dart'; import 'package:nui/nui.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Nui App', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const MyHomePage(title: 'Nui Demo App'), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({ required this.title, super.key, }); final String title; @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(widget.title), ), body: Center( child: NuiStackWidget( renderers: const [], imageErrorBuilder: null, imageFrameBuilder: null, imageLoadingBuilder: null, binary: null, nodes: null, xmlContent: ''' <center> <column mainAxisSize="min"> <text size="18" align="center"> You have pushed the button\nthis many times: </text> <text size="32"> {{ page.counter }} </text> </column> </center> ''', pageData: { 'counter': _counter, }, ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: const Icon(Icons.add), ), ); } }


А вот еще пример, только полностью на Нуи (включая логику):

 import 'package:flutter/material.dart'; import 'package:nui/nui.dart'; void main() { runApp(const MyApp()); } final DataStorage globalDataStorage = DataStorage(data: {'counter': 0}); final EventHandler counterHandler = EventHandler( test: (BuildContext context, Event event) => event.event == 'increment', handler: (BuildContext context, Event event) => globalDataStorage.updateValue( 'counter', (globalDataStorage.getTypedValue<int>(query: 'counter') ?? 0) + 1, ), ); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return DataStorageProvider( dataStorage: globalDataStorage, child: EventDelegate( handlers: [ counterHandler, ], child: MaterialApp( title: 'Nui App', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const MyHomePage(title: 'Nui Counter'), ), ), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({ required this.title, super.key, }); final String title; @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(widget.title), ), body: Center( child: NuiStackWidget( renderers: const [], imageErrorBuilder: null, imageFrameBuilder: null, imageLoadingBuilder: null, binary: null, nodes: null, xmlContent: ''' <center> <column mainAxisSize="min"> <text size="18" align="center"> You have pushed the button\nthis many times: </text> <dataBuilder buildWhen="counter"> <text size="32"> {{ data.counter }} </text> </dataBuilder> </column> </center> <positioned right="16" bottom="16"> <physicalModel elevation="8" shadowColor="FF000000" clip="antiAliasWithSaveLayer"> <prop:borderRadius all="16"/> <material type="button" color="EBDEFF"> <prop:borderRadius all="16"/> <inkWell onPressed="increment"> <prop:borderRadius all="16"/> <tooltip text="Increment"> <sizedBox size="56"> <center> <icon icon="mdi_plus" color="21103E"/> </center> </sizedBox> </tooltip> </inkWell> </material> </physicalModel> </positioned> ''', pageData: {}, ), ), ); } }


Отдельный код UI, чтобы была подсветка:

 <center> <column mainAxisSize="min"> <text size="18" align="center"> You have pushed the button\nthis many times: </text> <dataBuilder buildWhen="counter"> <text size="32"> {{ data.counter }} </text> </dataBuilder> </column> </center> <positioned right="16" bottom="16"> <physicalModel elevation="8" shadowColor="black" clip="antiAliasWithSaveLayer"> <prop:borderRadius all="16"/> <material type="button" color="EBDEFF"> <prop:borderRadius all="16"/> <inkWell onPressed="increment"> <prop:borderRadius all="16"/> <tooltip text="Increment"> <sizedBox size="56"> <center> <icon icon="mdi_plus" color="21103E"/> </center> </sizedBox> </tooltip> </inkWell> </material> </physicalModel> </positioned> 

Приложение Nui Counter с логикой Nui


Существует также интерактивная и подробная документация, в которой показана подробная информация о том, какие аргументы и свойства имеет каждый виджет, а также все их возможные значения. Для каждого из свойств, которые также могут иметь как аргументы, так и другие свойства, также имеется документация, с полной демонстрацией всех доступных значений. Помимо этого, каждый из компонентов содержит интерактивный пример, в котором вы можете увидеть реализацию этого виджета вживую и поиграть с ним, изменяя его по своему усмотрению.

Детская площадка Нанк

Nui очень тесно интегрирован с Nanc CMS. Для использования Nui не обязательно использовать Nanc, но использование Nanc может дать вам преимущества, а именно - такую же интерактивную документацию, как и Playground, где вы сможете увидеть результаты верстки в реальном времени, поиграться с данными который будет в нем использоваться. Причем не обязательно создавать свою локальную сборку CMS, вы вполне можете обойтись опубликованной демо-версией, в которой можно делать все необходимое.


Сделать это можно, пройдя по ссылке , а затем щелкнув поле Page Interface / Screen . Открывшийся экран можно использовать как игровую площадку, а нажав кнопку «Синхронизировать» , вы сможете синхронизировать Nanc со своей IDE через файл с исходниками, а вся документация доступна по нажатию кнопки «Справка» .


PS Эти сложности существуют потому, что я так и не нашел времени сделать явную отдельную страницу с документацией по компонентам в Nanc, а также невозможность вставить прямую ссылку на эту страницу.


Интерактивность и логика

Было бы слишком бессмысленно создавать обычный преобразователь XML в виджеты. Это, конечно, тоже может быть полезно, но вариантов использования будет гораздо меньше. Не одно и то же — полностью интерактивные компоненты и экраны, с которыми можно взаимодействовать, которые можно гранулярно обновлять (то есть не все сразу — а частями, требующими обновления). Кроме того, этому пользовательскому интерфейсу нужны данные. Что, учитывая наличие буквы S во словосочетании Server-Driven UI, можно подставить прямо в макет на сервере, а можно сделать и красивее. А не перетаскивать новую порцию макета из бэкенда при каждом изменении в UI (Nui — не машина времени, которая заставляет трепетать лучшие практики jQuery).


Начнем с логики: в макет можно подставлять переменные и вычисляемые выражения. Допустим, виджет определен как <container color="{{ page.background }}"> и будет извлекать свой цвет непосредственно из данных, передаваемых в «родительский контекст», хранящихся в переменной background . А <aspectRatio ratio="{{ 3 / 4}}"> установит соответствующее значение соотношения сторон для своих потомков. Существуют встроенные функции, средства сравнения и многое другое, которые можно использовать для создания пользовательского интерфейса с некоторой логикой.


Второй момент — шаблонизация . Вы можете определить свой собственный виджет непосредственно в коде пользовательского интерфейса, используя тег <template id="your_component_name"/> . При этом все внутренние компоненты этого шаблона будут иметь доступ к аргументам, передаваемым в этот шаблон, что позволит гибко параметризовать пользовательские компоненты и затем повторно использовать их с помощью тега <component id="your_component_name"/> . Внутри шаблонов можно передавать не только атрибуты, но и другие теги/виджеты, что дает возможность создавать повторно используемые компоненты любой сложности.


Пункт третий – «за петли». В Nui есть встроенный тег <for> , который позволяет использовать итерации для многократного рендеринга одного и того же (или нескольких) компонентов. Это удобно, когда есть набор данных, из которых нужно создать список/строку/столбец виджетов.


Четвертое — условный рендеринг. На уровне макета реализован тег <show> (была идея назвать его <if> ), который позволяет при различных условиях рисовать вложенные компоненты или вообще не встраивать их в дерево.


Пункт пятый – действия. Некоторые компоненты, с которыми может взаимодействовать пользователь, могут отправлять события . Которым вы можете полностью управлять по своему усмотрению. Скажем, <inkWell onPressed="something"> — при таком объявлении этот виджет становится кликабельным, и ваше приложение, а точнее некий EventHandler , сможет обработать это событие и что-то сделать. Идея в том, что все, что связано с логикой, должно быть реализовано непосредственно в приложении, но реализовать можно что угодно. Создайте несколько универсальных обработчиков, которые могут обрабатывать группы действий, например «перейти на экран» / «вызов метода» / «отправить событие аналитики». Есть планы реализовать и динамический код, но здесь есть нюансы. Для Dart есть способы выполнения произвольного кода, но это влияет на производительность, к тому же совместимость этого кода с кодом приложения вряд ли составляет 100%. То есть, создавая логику в этом динамическом коде, вы постоянно будете сталкиваться с какими-то ограничениями. Поэтому этот механизм необходимо очень тщательно проработать, чтобы он был действительно применимым и полезным.


Шестой пункт — локальное обновление пользовательского интерфейса. Это возможно благодаря тегу <dataBuilder> . Этот тег (Блок под капотом) может «смотреть» на конкретное поле, и при его изменении перерисовывать его поддерево.


Данные

Изначально я пошел по пути двух хранилищ данных — упомянутого выше «родительского контекста». А также «данные» — данные, которые можно определить непосредственно в пользовательском интерфейсе, с помощью тега <data> . Честно говоря, я не могу сейчас вспомнить аргументацию, почему нужно было реализовать два способа хранения и передачи данных в UI, но не могу как-то жестко покритиковать себя за такое решение.


Они работают следующим образом — «родительский контекст» — это объект типа Map<String, dynamic> , передаваемый непосредственно виджетам NuiListWidget / NuiStackWidget . Доступ к этим данным возможен через префиксную page :

 <someWidget value="{{ page.your.field }}"/>

Вы можете ссылаться на что угодно, на любую глубину, включая массивы — {{ page.some.array.0.users.35.age }} . Если такого ключа/значения нет, вы получите null . Списки можно перебирать с помощью <for> .


Второй способ — «данные» — это глобальное хранилище данных. На практике это некий Bloc , расположенный в дереве выше, чем NuiListWidget / NuiStackWidget . При этом ничто не мешает организовать их использование в локальном стиле, передав собственный экземпляр DataStorage через DataStorageProvider .


В то же время первый метод не является реактивным — то есть при изменении данных на page никакой пользовательский интерфейс не будет обновляться сам. Так как это, по сути, всего лишь аргументы вашего StatelessWidget . Если источником данных для page является, скажем, ваш собственный Bloc, который будет передавать набор значений Nui...Widget — то, как и в случае с обычным StatelessWidget , он будет полностью перерисован с использованием newdata.


Второй способ работы с данными — реактивный. Если вы измените данные в DataStorage , используя API этого класса — метод updateValue , то это вызовет метод emit класса Bloc, а если в вашем UI есть активные слушатели этих данных — теги <dataBuilder> , то их содержимое будет соответствующим образом изменено, но остальная часть пользовательского интерфейса не будет затронута.


Таким образом, мы получаем два потенциальных источника данных — очень простую page и реактивные data . За исключением логики обновления данных в этих источниках и реакции UI на эти обновления, разницы между ними нет.

Документация

Я намеренно не описывал все нюансы и аспекты работы, так как это получилась бы копия уже существующей документации . Поэтому, если вам захочется попробовать или просто узнать больше – добро пожаловать сюда. Если какие-то аспекты работы не ясны или документация что-то не освещает, то мне будет польщено Ваше сообщение с указанием проблемы:


Кратко перечислю некоторые возможности, которые не рассмотрены в этой статье, но доступны вам:

  • Создание собственных тегов/компонентов с возможностью создания для них точно такой же интерактивной документации, как и для их аргументов и свойств с предварительным просмотром в реальном времени. Так, например, реализован компонент для отрисовки SVG-изображений . Нет смысла запихивать его в ядро движка, потому что он нужен не всем, а как расширение, доступное для использования путем передачи всего одной переменной — легко и просто. Непосредственно —пример реализации .


  • Огромная встроенная библиотека иконок, которую можно расширить за счет добавления своих (тут я оказался непоследовательным, и "впихнул", логика была в том, чтобы сделать как можно больше иконок доступными для использования сразу и не было необходимости обновите приложение, чтобы использовать новые значки). Из коробки доступны: fluentui_system_icons , Material_design_icons_flutter и remixicon . Вы можете просмотреть все доступные значки с помощью Nanc , Page Interface / Screen -> Icons

  • Пользовательские шрифты, включая Google Fonts из коробки.


  • Преобразование XML в JSON/protobuf и использование их в качестве «источников» для пользовательского интерфейса.


Все это и многое другое можно изучить в документации .


Что дальше?

Главное — проработать возможность динамического выполнения кода с логикой. Это очень крутая функция, которая позволит вам очень серьёзно расширить возможности Нуи. Также можно (и нужно) добавить оставшиеся редко используемые, но иногда очень важные виджеты из стандартной библиотеки Flutter. Освоить XSD, чтобы в IDE появилось автодополнение для всех тегов (есть идея сгенерировать эту схему прямо из документации тега, тогда ее будет легко создать для кастомных виджетов и она всегда будет актуальна -date, а еще есть идея сделать в Dart сгенерированный DSL, который потом можно будет конвертировать в XML/Json/Protobuf). Ну и дополнительная оптимизация производительности - это сейчас неплохо, очень неплохо, но может быть еще лучше, еще ближе к родному Flutter.


Это все, что у меня есть. В следующей статье я очень подробно расскажу о производительности Нуи, как я создавал тест-кейсы, сколько десятков граблей я прошел в этом процессе и какие цифры в каких сценариях можно получить.


Если у вас возникнет желание попробовать Nui или узнать его поближе — подойдите к стойке документации . Также, если не сложно, то поставьте, пожалуйста, звездочку на GitHub и лайк на pub.dev — для вас это не сложно, а для меня, одинокого гребца на этой огромной лодке — невероятно полезно.