Читал как-то книжку по Dart/Flutter на русском и она мне не понравилась: сухо, скучно. А местами ну прям вообще, напомнило адскую черепашку с урока информатики:
В общем, может от нее польза и была, но читать было трудно. Называть автора не буду т.к. на вкус и цвет не всем мидии нравятся. Кто-то предпочитает улиток или слизней 😂 Глотать сразу 😂
Я не особо любитель что-то делать, а потом узнавать что так не делают. Например писать код, который потом придется переписывать. Потому временами ныряю в отечественную и зарубежную литературу. А чтобы оседало в голове, запускаю код из примеров, перевожу, делаю заметки. Ну а как иначе-то?
На днях доперевел небольшую часть книжки, которая мне понравилась. Хочу поделиться, вдруг кому-то поможет. Не все же могут перевести? Или могут?
Могут или не могут, вот первая глава без воды, если будет полезно, то с вас как минимум плюсик: ссылка
Позже еще допереведу, если не забуду, то скину остальные главы и всю книжку целиком с автором и названием (еще глав 10, если не ошибаюсь). Можете угадать автора? Может кто читал, хотя именно эту вряд ли.
-- По вечерам разрабатываю сервис для общения на Go & Flutter. Кому интересно, можете подписаться куда-нибудь на меня, попробуете его в числе первых. Постепенно буду продолжать делиться успехами разработки сервиса.
Неделю не было новостей по сервису для общения. Пора это исправить!
В моих планах было перейти к обмену сообщениями: отправка, прием, запись в БД, отображение пришедшего сообщения. А еще была маленькая задачка - зафиксировать иконку даты в истории переписки. До этого я просто добавил даты и вроде как дело осталось за малым: просто зафиксировать одну из дат в верхней части истории. Ну и началось. Кто же знал что будет непросто 😂
Вот так в итоге работает отображение дат в окне переписки:
Вообще, в Flutter есть такая вещь как виджеты и благодаря им даже новички из коробки могут создавать достаточно сложные вещи в Flutter. Это что-то типа куба в Unity: открываешь сцену, кидаешь куб и думаешь, что сейчас как сделаешь супер-игру, как она всем понравится... А потом оказывается, что это была рекламная кампания и чтобы сделать действительно что-то производительное, придется в тонкостях разбираться что и как работает:
Пустая сцена с кубом
Это уже по опыту говорю, писал TotalReload, знаю как и что там работает:
Скрин сцены Total Reload
Виджеты предоставляют широкий спектр возможностей и с их помощью можно было бы сделать систему дат, но с ограничениями. Есть такая вещь как Sliver, он позволяет удобно реализовывать сложные интерфейсы взаимодействия. Sliver-ов великое множество и вроде как они покрывают почти все потребности разработчиков.
Возникла проблема. Готового решения я не нашел. Да и нет его похоже. Пытался прикрутить имеющееся стандартное решение (помним, что визуально это не то, что нужно), но и готовое решение приколхозить нормально не получилось из-за имеющихся багов. Чтобы не быть голословным, вот: Ссылка №1 на Git с багом Ссылка №2 на Git с костылем Ссылка №3: разработчики поднимают эту тему
Прозрачный AppBar и исправление бага - это уже уникальный интерфейс 😂😂😂
Про костыль: при открытии чата нужно пролистать список сообщений в самый низ. Понятно что список загружаю не весь, а только N последних сообщений. Вот их-то и требуется пролистать вниз.
Но из-за бага нельзя узнать сразу значение низа, то есть высоты всего списка. А получить его можно через кадр. В итоге разработчики сделали такой незаметный костыль: список пролистывается до максимального значения(но не до конца), а потом анимацией список докручивается до низа и по мере прокрутки Flutter обновляет максимальное значение для пролистывания. Чудо-код, который это умеет и работает стабильно:
Но это решение, очевидно, плохое. Открываешь чат, а он что-то листается куда-то... В итоге пришлось разбираться как решить эту проблему. Ведь если нельзя, но очень нужно, то все же что-то придумать можно 😂
А советчики этого костыля могут сами им и пользоваться:
Ну и пользуйтесь своим решением
Решение
Решение достаточно веселое. Flutter - это некая кроссплатформенная система рендера, которая управляет процессом отрисовки. Очень грубо, очень по рабоче-крестьянски, но в самом первом приближении это так. Система работает на основе деревьев. И на любой вопрос типа: почему используются деревья, зачем системе рендера кэш и границы, зачем система разделена на элементы и переиспользует их... ? На все это примерно один ответ - чтобы для пользователя интерфейс быстро работал.
Еще стоит отметить, что управлять тем, что выводится на экране можно с нескольких абстракций.
(1) Самый верхний уровень - виджеты. Это буквально готовые кирпичики, берешь и используешь. Глубокого погружения в тонкости рендера не требуется. Первое приложение можно написать ничего не зная.
(2) Далее идет CustomPaint. Это виджет, который умеет на себе рисовать. Что-то типа приложения Paint: берешь холст и рисуешь примитивы. Им отрисованы градиенты окошек сообщений. По сути это продвинутый виджет, сложность понимания принципа работы примерно как с виджетами.
(3) RenderObject - это уже интереснее, это то, как все рисуется в Flutter. И сложность понимания внезапно поднимается до "нужно понять как все тут устроено". Отмечу, виджеты уже сами в себе содержат механизм RenderObject, но от пользователей он скрыт. Если вдруг потребуется сделать такой виджет, которого нет среди стандартных виджетов, то придется разбираться, как устроена система рендера и используя ее собирать свой собственный виджет. Собственно этим я и занимался. Пришлось сделать виджет, который визуально умеет делать все то, что делает стандартный SliverStickyHeader от Flutter. Но важно: костыли нужно убрать, AppBar нужен прозрачный.
Теперь даты красиво ездят туда-сюда, вроде эстетично и глаз радуется :)
Далее работаю над отправкой и приемом сообщений.
-- По вечерам разрабатываю сервис для общения. Кому интересно, можете подписаться куда-нибудь на меня, попробуете его в числе первых. Постепенно буду продолжать делиться успехами разработки сервиса.
Привет, Пикабу. Мне 23 года и недавно я решил начать учить польский. Я с детства учу английский, владею им на хорошем уровне и знаю, насколько мне тяжело дается учеба. Просто зубрить слова и правила - это не мое, становится скучно моментально и, что самое главное, не запоминается практически ничего. Основываясь на этом опыте, я понимал, что мне нужно погружение в контекст. Так как польский (также как и русский) относятся к славянским языкам и в них довольно много похожих по звучанию и значению слов, я стал сразу пытаться начать читать.
И тут начались первые проблемы. На сайтах, на которых есть тексты, адаптированные для уровня А1-А2 читалка без встроенного переводчика. Импортировать тексты в Apple Books это совсем не то и неудобно. Я начал искать альтернативы.
Приложение, которое мне понравилось по функционалу, - это LingQ. У него есть встроенная библиотека текстов, перевод слов по тапу, карточки и, казалось бы, вот он, идеальный вариант. И это он, если бы не цена в 13 фунтов ежемесячно. На хобби тратить столько денег на подписку я посчитал не целесообразно.
Помимо изучения языков, я увлекаюсь программированием на пайтон и вайбкодингом.
Так и родился LinguaVibe.
Что вообще получилось
LinguaVibe - это Flutter-приложение для чтения иностранных текстов. Сейчас поддерживает 5 языков: польский, испанский, немецкий, французский и английский. Интерфейс - на русском.
Основная фича: ты вставляешь текст (или импортируешь статью по URL), читаешь, и когда встречаешь незнакомое слово - тапаешь на него. Приложение отправляет всё предложение в LLaMA через Groq API и получает перевод с учётом контекста. Не просто «слово → перевод», а полноценный разбор: начальная форма, перевод, грамматика и даже пример предложения.
Вторая по важности фича: ты можешь вставить свой ключ Groq API, который получается совершенно бесплатно и получить практически весь функционал приложения совершенно бесплатно, с условием, что у вас установлено приложение с названием из 3 букв.
После этого сохраняешь слово в словарь, а потом учишь его через встроенные карточки с системой интервального повторения (SRS, метод Лейтнера).
Главное меню приложения
Стек и архитектура
Тут всё просто, без enterprise-булщита:
Фронтенд (мобилка):
- Flutter (Dart) - потому что кроссплатформа и горячая перезагрузка
- Provider - стейт-менеджмент, без Riverpod-магии
- Isar - локальная NoSQL база. Быстрая, асинхронная, и главное - у меня отдельная база на каждый язык (`isar_pl`, `isar_es` и т.д.)
- flutter_tts - озвучка слов. Польский, испанский, немецкий - всё произносит
Бэкенд:
- Node.js + Express - лёгкий сервер на соседнем репозитории
- Docker - контейнеризация, потому что я не хочу настраивать сервер руками в 3 часа ночи
- Caddy - reverse proxy с автоматическими HTTPS-сертификатами. Lets Encrypt сам всё делает, красота
AI:
- Groq API - LLaMA 3.1 8B для быстрых переводов по тапу, LLaMA 3.3 70B для генерации примеров и переводов заголовков. Groq быстрый - ответы за 200-300мс, это важно когда ты тапаешь на слова в тексте
Словарь
Как я это кодил
Буду честен - я вайб-кодил с ИИ-помощниками. Opencode, Cursor, Claude - всё было. Я Python-разработчик, Dart я до этого практически не трогал. Но с ИИ-ассистентами это оказалось не так страшно.
Архитектуру продумал сам: сервисы, репозитории, провайдеры. А детали реализации делал с ИИ. Это реально работает, если ты понимаешь что хочешь получить.
Вот так выглядит перевод по тапу
Грабли, на которые я наступил (и закрыл)
Вот тут самое интересное для технарей. Перед релизом пришлось закрывать несколько серьёзных дыр.
1. APP_SECRET_KEY в коде
Изначально ключ бэкенда был захардкожен в `server_config.dart`. Это ужасно - любой может декомпилировать APK и получить ключ.
Решение: перенёс ключ в `--dart-define=APP_SECRET_KEY` при билде. Ключ хранится в `.env` файле (который в `.gitignore`), а билд-скрипт подставляет его через `dart-define`. В коде - `const String.fromEnvironment('APP_SECRET_KEY')`. При декомпиляции ключ не найти в plaintext.
2. SSRF-атаки в веб-парсере
У меня есть фича импорта текстов по URL. Пользователь вставляет ссылку, приложение парсит статью и вытаскивает текст. Проблема: если пользователь вставит `http://localhost:3000/admin` или `http://169.254.169.254/latest/meta-data/` (AWS metadata)?
Решение: в `WebParserService` добавил валидацию URL:
- Проверяю что схема - только `http` или `https`
- Блокирую localhost, 127.0.0.1
- Блокирую приватные сети (10.x.x.x, 172.16-31.x.x, 192.168.x.x)
- Блокирую AWS/GCP metadata (`169.254.169.254`)
// Блокируем SSRF
if (uri.isLoopback || uri.isPrivate) throw Exception('Blocked');
if (uri.host == '169.254.169.254') throw Exception('Blocked');
3. Накрутка лимитов
У меня freemium-модель: бесплатно 10 переводов в день и 3 импорта URL. Premium - безлимит. Но что если пользователь будет спамить запросы?
Решение: на бэкенде - rate limiting по userId. На клиенте - счётчики в SharedPreferences с ежедневным сбросом. Перед каждым запросом - проверка лимита. Если превышен - кидается `SubscriptionTranslationLimitException` и показывается диалог с предложением купить Premium.
4. Вебхуки ЮКассы и идемпотентность
Для Android-оплаты использую ЮКассу. Бэкенд создаёт платёж, пользователь оплачивает в WebView, ЮКасса шлёт вебхук. Проблема: вебхук может прийти дважды (сеть моргнула, таймаут, retry).
Решение: на бэкенде - идемпотентная обработка вебхуков. Каждый платёж имеет уникальный ID, и если вебхук уже обработан - повторный игнорируется. Плюс проверка HMAC-подписи от ЮКассы, чтобы никто не мог подделать вебхук.
5. WebView для платежей
На Android оплата идёт через WebView с ЮКассой. Нужно было убедиться, что пользователь не уйдёт со страницы оплаты и что deep link сработает корректно.
Решение: `webview_flutter` с `NavigationDelegate`. Разрешаю навигацию только на домены `yookassa.ru` и `yandex.ru`. Ловлю deep link `linguavibe://payment-success` через `onNavigationRequest` и `onUrlChange` - как только редирект на наш scheme, закрываем WebView и обновляем статус Premium.
Самый сок в том, что все эти уязвимости (включая защиту от SSRF в парсере и дыру в WebView) мне подсветил не платный аудит и не Claude, а бесплатный Qwen 3.6 в OpenCode, когда я ради прикола скинул ему структуру проекта и спросил: „Где я тут обосрался?“. Нейронка буквально ткнула меня носом в безопасность, расписала, что у меня не так и предложила фиксы.
Читалка
Что умеет приложение сейчас
Чтение:
- Вставка текста вручную
- Импорт статьи по URL (парсинг с фильтрацией мусора - рекламы, privacy policy и прочего)
- Тап на слово → AI-перевод с контекстом
- TTS-озвучка каждого слова
Словарь:
- Все сохранённые слова с поиском
- Ручное добавление слов (с AI-генерацией перевода и примера)
- Редактирование переводов
Карточки (SRS):
- Метод Лейтнера: 6 коробок с интервалами 5 мин → 4 ч → 1 день → 3 дня → 7 дней → 14 дней
- 3D-анимация переворота карточки
- Настраиваемый лимит слов за сессию (10-100)
- Слова на 5-й коробке считаются освоенными
Настройки:
- Groq API ключ (вставил свой кюч - тогда безлимит)
- Темы: светлая, тёмная, системная
- Стиль плиток: градиент или монохром
- Экспорт/импорт словаря в JSON (Premium)
- Серия дней (streak) - мотивация не бросать
Premium:
- iOS: In-App Purchase, 449 ₽/мес
- Android: ЮКасса, 349 ₽/мес
- Безлимитные переводы и импорты
- Если вставишь свой Groq API ключ - лимиты снимаются и без подписки
Бонус: приложение весит всего 20 МБ в RuStore. Оптимизировал сборку, убрал лишнее, и получилось довольно компактно для Flutter-приложения.
Карточки с интервальным повторением
Где скачать и потестить
На данный момент приложение доступно только в RuStore под Android. В будущем планируется релиз под iOS в AppStore.
Есть такое англоязычное приложение Fantasy Hike - превращает твои шаги в путешествие по фэнтези-миру. Идея огонь, я хотел пользоваться, но там странное ограничение: бесплатно можно пройти только полтора километра в день. Сделал 10 000 шагов, а в приложении продвинулся на полтора километра. Хочешь больше - плати за подписку.
Понятно, что ребятам надо как-то зарабатывать (оплачивать серверы, разработку, продвижение, дизайнеров и тд). Но 1500 метров в день для бесплатного пользователя слишком строгое ограничение. Оно скорее про то что "не дадим нормально пользоваться, пока не заплатишь".
В итоге сел и за пару вечеров завайбкодил свой Lord of the Steps
Суть примерно та же:
Маршрут Фродо из Шира до Роковой Горы 2 350 км по канону
При 10 000 шагов в день полный путь займет около 9 месяцев
Шаги читаются через Health Connect (Android) и HealthKit (iOS), всё с Mi Band/Apple Watch/Garmin/Wear OS подтягивается само
Виджет на главный экран (шаги, км и где ты сейчас на карте)
Можно собраться компанией объеденившись в общие группы
Кстати, у Fantasy Hike реально классная анимация. В нем человек ходит по карте, ландшафт меняется и время суток (видно что вложились). Я такое повторять не стал, сделал проще - просто прогресс по маршруту.
Опыта в Android и iOS у меня вообще не было. Просто давно хотелось попробовать опубликовать что-нибудь в сторах, посмотреть как это устроено. С сlaude сode это оказалось реально и не за пол года, а за несколько вечеров. Собрал быстро базовую часть, потом полировал интеграцию с Health Connect и фоновую работу (там свои приколы), виджеты на ios и android, группы и тд.
Приложение есть в Google Play, App Store и RuStore, потихоньку набирает пользователей без рекламы и продвижения (сейчас около 700 активных пользователей). Видеть, как живые люди реально идут от Шира до Мордора приятно.
Добрался до работы над чатами и все еще работаю над созданием персональных чатов между пользователями. Параллельно пробрасываю логику груповых чатов и каналов. Но это не все, еще и интерфейсом занимаюсь:
На вырвиглазные сочетания цветов пока не обращайте внимания. Подбираю специально контрастные цвета, чтобы можно было легко заметить просчеты. Иконки тоже почти случайно раскидал, взял первое попавшееся что было в "стандартном наборе разработчика".
Странное дело, на экране монитора интерфейс воспринимается иначе, не так как на мобильном устройстве (расположение и размер элементов). Из-за этого в последнее время смотрю как выглядит сборка прямо на устройстве.
В окно поиска контактов/создания чата внизу добавил размытие и пару кнопочек:
Размытие экрана унифицировал немного, теперь его можно подключать к каждому окну отдельно. Работает хорошо, но насколько понимаю, неоптимально. Потом может быть вернусь еще к нему.
Сверху сделал заготовку с областью для сторис (красная область), снизу меню с кнопками для быстрого доступа к функциям приложения. Над звонками еще не работал, но планы под это дело имеются.
Над размытием пришлось много возиться. Очень много. Если хочешь сделать что-то не похожее на интерфейс 2000-х, то для этого нужно использовать передовые технологии 😂😂😂 Например размытие по градиенту 😂😂😂
Не знаю в чем проблема сделать эффективное размытие в Flutter, но проблема у них есть. Есть и куча костыльных плагинов и куча заплаток для Flutter. Для Flutter есть куча всего созданного разными разработчиками и подключается каждый пакет отдельно. А вот на Unity3D размытие можно было бы добавить в пару кликов: кидаешь Canvas, пишешь шейдер какой хочешь, позиционируешь слой размытия где хочешь, настраиваешь очередность рендера, ползунками настраиваешь парамтеры размытия. Учитываешь в шейдере особенности железа и вуаля, все замечательно работает. Знаю о чем говорю, в Total Reload работал над графикой:
Разработчики Flutter мне, наверно, бы сказали: нравится Unity? Ну и катись:
В направлении удобно пристроить 5-ю точку 😂
В Unity3D как хочешь, так и делаешь. Хотя вру. В Unity3D работает немного иначе: ты не делаешь баги (если не делаешь), но за тебя делает баги Unity3D 😂 Опыт работы в ней лет 10, так что, наверно, знаю о чем говорю. Да и правка и репорт на баги в Unity3D выглядят примерно так:
В ощем не соскучишься
Есть интересный момент. Минимальный вес приложения на Unity - около 15 мб. Это связано с тем, что в проект добавляется физ. движок. Это все же инструмент разработки для игр. В том числе и из-за веса решил создавать проект не на Unity, а на Flutter. На днях узнал что минимальный вес приложения на Flutter тоже около 15 мб 😂😂😂
Структура клиента
Ранее в посте сетовал что я сам себе злобный буратино и мне не нравится то, как организовал структуру проекта. В итоге сегодня мои руки были по локоть в крови от усиленного рефакторинга кода. В какой-то момент решил проиндексировать каталоги проекта, чтобы они располагались в том порядке в котором проиндексированы а не по алфавиту:
Не знаю можно создавать имена каталогов начинающиеся с цифр или нельзя. Вообще у программистов нельяз. Но если очень хочется, то, наверно, все же льзя.
Системные вызовы в приложении
Приложению очень важно быть в курсе того, что сейчас с ним происходит: свернуто оно или пользователь его закрывает.
Ранее писал код под настольные компьютеры, веб и мобильные устройства. Так вот, у всех свой набор методов, которые вызываются при наступлении того или иного события. На телефонах за эти события отвечает WidgetsBindingObserver, на компьютерах WindowListener, на web-версии уже не вспомню что, но я вовремя психанул и сделал общий интерфейс на все и вынес его в отдельный файл чтобы не сойти с ума от поисков где что вызывается:
Теперь в одном месте все события. Не знаю почему в Flutter не унифицировали все это дело. В Unity3D, вроде как, было проще. OnApplicationQuit прописываешь и понимаешь что если он вызвался, то приложение закрывается и не имеет значения на чем работает приложение: планшет, мобильный телефон или еще что-то.
Работа над чатами
Вообще все эта работа проходила на фоне работы над созданием чатов. Я промахнулся, не рассчитал 😂 В общем как-то так получилось, что в процессе работы меня переклинило и решил что нормальная тема когда человек нажимает кнопку типа "Создать чат" и создает чат с человеком.
Чат должен создаваться просто: нажал на пользователя и отправил сообщение. Если отправил сообщение, то чат создался. Не отправил - не создался. А вот каналы и групповые чаты можно создавать через нажатие на кнопку "Создать ***".
В итоге переписывал немного серверную логику с учетом замечаний, клиента немного переписал, познакомился с парой людей на улице (которые недовольны мессенджерами), скинул им ссылку на группы чтобы подписались на меня. На данный момент добавляю в клиентское приложение вебсокеты, работаю над созданием чатов, обменом сообщениями, откладываю копейку на серверное железо 😁.
Пожалуй все на сегодня, отпишусь как появятся новости 😁.
-- Кому интересно, можете поддержать (лайком) или подписаться куда-нибудь на меня, попробуете сервис в числе первых. Постепенно буду продолжать делиться успехами разработки.
Я мобильный разработчик, и у меня накипело. Недавно искал себе обычное Судоку порешать в дороге — и просто обалдел от того, что сейчас лежит в топах. Либо интерфейс, застрявший в эпохе Android 2.3, либо всё настолько перегружено кнопками и вырвиглазными цветами, что логику развивать уже не хочется.
Решил, что пора исправить ситуацию. Взял Flutter и собрал "Судоку Prime"
Что я вложил в этот проект:
Современный UX/UI: Никакого визуального мусора. Только ты, цифры и чистый, плавный интерфейс.
Скорость: Flutter дает ту самую плавность 60 FPS, которой так не хватает старым Java-аппкам.
Автономность: Приложение полностью оффлайновое. Работает в самолете, в метро и там, где связь не ловит.
Для людей: Сделал несколько уровней сложности и постарался вылизать управление, чтобы не промахиваться по клеткам.
Для меня это был своего рода вызов — сделать классическую игру приятной визуально. Сейчас проект уже в RuStore, но стор пока индексирует его неохотно, поэтому найти через поиск «в лоб» бывает квестом.
Буду рад, если сообщество оценит результат. Особенно интересен фидбек от коллег-разработчиков: как вам плавность переходов и общая верстка?