Flutter Friendly
751 subscribers
110 photos
52 videos
1 file
100 links
Канал Friflex о разработке на Flutter. Обновления, плагины, полезные материалы — превращаем знания в реальный опыт, доступный каждому разработчику.

🔗 Наш канал для разработчиков: @friflex_dev
🔗 Канал о продуктовой разработке: @friflex_product
Download Telegram
This media is not supported in your browser
VIEW IN TELEGRAM
Привет, с вами Роза, Flutter Dev Friflex!

Когда только начинаешь разрабатывать на Flutter, многие возможности языка остаются незамеченными. А с опытом начинаешь глубже разбираться в деталях и повышать свою экспертность. С модификаторами классов у меня было так же: изначально в моем арсенале был лишь abstract... и все, наверное. А со временем я узнала и про sealed, и про base. Делюсь этим и с вами!

Зачем нужны модификаторы?

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

Основные модификаторы:

🔸abstract
Если вам не нужна реализация всех методов, а вы хотите создать класс-шаблон, используйте abstract.
🔴Запрещает создавать экземпляры этого класса напрямую (new AbstractClass() не сработает).
🔴Часто используется как базовый класс, определяющий интерфейс и частичное поведение для наследников.

🔸base
Разрешает наследование (extends) и реализацию (implements), но только в пределах текущего пакета.
🔴За пределами пакета base-класс нельзя реализовать (implements).
🔴Полезно, если нужно предотвратить реализацию, но оставить возможность наследования.

🔸interface
Принуждает использовать класс только через implements, запрещая наследование (extends).

Полезно, если хотите создать чистый контракт, без возможности переиспользовать реализацию.

🔸final
Запрещает любое наследование (extends) или реализацию (implements) класса за пределами текущей библиотеки. 

Гарантирует, что класс — конечная точка в иерархии. Его нельзя расширить или изменить поведение через подклассы вне вашего контроля.

🔸mixin
Позволяет переиспользовать код без наследования.
🔴Класс с mixin можно добавлять к другим классам через with.
🔴Миксины не могут иметь конструкторов и не могут быть инстанцированы напрямую.

🔸sealed
Позволяет создавать закрытый набор подтипов.
🔴Все подклассы должны быть в той же библиотеке, что и sealed-класс.
🔴Полезно для switch, так как компилятор проверяет, что все случаи учтены (exhaustiveness).
🔴Отлично подходит для описания состояний (Loading, Success, Error), событий и других строго определенных иерархий.

Как использовать модификаторы?
Добавьте перед классом нужное ключевое слово. Например:

sealed class GameState {
  // ...
}


🔖Важно! Модификаторы можно комбинировать (abstract base class), создавая тонкие правила для классов. Подробнее — в таблице в комментариях.

📎 Официальная документация по модификаторам

А какие модификаторы используете чаще всего? Делитесь в комментариях! 👀
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥154👍3💅21
This media is not supported in your browser
VIEW IN TELEGRAM
👾Дайджест постов за март

Сначала главное: нас уже больше 500. Спасибо за интерес, доверие и любовь к Flutter. А теперь — топ постов, которые вас особенно зацепили:

🔴Библиотека dartx и что она умеет

🔴Рисование кастомных фигур, линий с CustomPainter

🔴Императивная и декларативная навигация: в чем отличие

🔴Магия FutureOr: когда использовать

🔴gRPC во Flutter: эффективная коммуникация между клиентом и сервером

🔴Hero: создание эффектного перехода между экранами

В апреле будет еще больше полезного! Хорошого всем настроения и кода без ошибок💜

P.S. Если есть темы, которые вам особенно интересны, пишите — учтем.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥65😍3
Привет, это Катя, Flutter Dev Friflex.

В предыдущем своем посте я рассказала о том, как настроить и использовать фреймворк gRPC. Сегодня хочу поделиться особенностями работы с ним.

1️⃣ Потоковая передача данных
gRPC поддерживает несколько типов RPC:
🔴Унарные (один запрос — один ответ)
🔴Серверные потоки (один запрос —поток ответов)
🔴Клиентские потоки (поток запросов — один ответ)
🔴Двунаправленные потоки (поток запросов — поток ответов)

2️⃣ Обработка ошибок
gRPC использует статусные коды для индикации ошибок. Основные статусы:
OK (0) — успех
CANCELLED (1) — операция отменена
UNKNOWN (2) — неизвестная ошибка
INVALID_ARGUMENT (3) — неверные аргументы
DEADLINE_EXCEEDED (4) — превышено время ожидания
NOT_FOUND (5) — ресурс не найден
PERMISSION_DENIED (7) — нет прав
UNAUTHENTICATED (16) — не аутентифицирован

3️⃣ Аутентификация
gRPC поддерживает несколько механизмов аутентификации:
🔴SSL/TLS с сертификатами
Пример: ChannelCredentials.secure(certificates: certs)
🔴JWT (JSON Web Tokens)
Пример: CallOptions(metadata: {'authorization': 'Bearer $token'})
🔴Basic Auth
Пример: 'Basic ${base64Encode('login:pass')}'
🔴OAuth2
Пример: AuthInterceptor(OAuthCredentials())
🔴API Keys
Пример: CallOptions(metadata: {'x-api-key': key})
🔴Интерсепторы
Пример: Кастомные ClientInterceptor
🔴mTLS (Mutual TLS)
Пример: ChannelCredentials.secure(clientCertificates: clientCert)

Преимущества:
Высокая производительность
Кросс-языковая поддержка
Встроенная генерация клиентского и серверного кода
Поддержка потоковой передачи данных
Нативные механизмы аутентификации и шифрования

Ссылки на библиотеки: grpc, probuf, protoc_plugin.

gRPC может показаться сложным на первых этапах, но его преимущества окупаются в средних и крупных проектах, особенно когда важны производительность и типобезопасность. Был ли у вас опыт работы с gRPC?
Please open Telegram to view this post
VIEW IN TELEGRAM
5🔥5👍2👏1🎉1
Привет! На связи Анна, Friflex Flutter Team Lead.

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

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

Один из способов реализовать поддержку ссылок со схемами https:// и http:// — использовать функционал Firebase Dynamic Links. Наверняка многие из вас с ним знакомы или даже использовали в своих проектах.

С помощью Firebase Dynamic Links очень просто настраивать ссылки и управлять ими в кроссплатформенных приложениях. Но сейчас эта фича в документации Firebase помечена как deprecated. Уже почти год интегрировать в новые проекты ее нельзя, а поддержка в старых проектах закончится 25 августа 2025.

На замену динамическим ссылкам Firebase приходят Universal links для iOS и App Links для Android. Разберемся, как настроить поддержку для каждой из платформ.

🔖Настройка ссылок на Android очень проста. Для этого Android Studio предоставляет инструмент App Links Assistant (Tools -> App Links Assistant). Он пошагово дает инструкцию и подсказки, какие действия необходимо выполнить, а также сам генерирует код в необходимых местах.
🔴в AndroidManifest.xml добавить поддержку схем и хоста, которые будет содержать ссылка
🔴в MainActivity добавить логику нативной обработки ссылок, поступающих извне приложения
🔴сформировать файл связанных доменов для связи с конкретным сайтом

В итоге вы должны получить сгенерированный файл assetlinks.json.

🔖На iOS подобного инструмента нет, но настройка не должна вызвать трудностей.
🔴Для начала в файле Info.plist добавим флаг FlutterDeepLinkingEnabled с значением true. Важно, если используете в приложении сторонние библиотеки для обработки ссылок (например, app_links), значение флага нужно установить false.

<key>FlutterDeepLinkingEnabled</key>
<true />


🔴Затем в Xcode переходим в Runner, в разделе Signing and Capabilities добавляем новую Capability и в списке выбираем Associated Domains. Здесь мы указываем, с каким именно доменом должно быть связано наше приложение. Добавляем строку:

applinks:www.example.ru


🔴Теперь необходимо создать файл apple-app-site-association. Название файла не должно содержать расширения. В самом файле в формате json необходимо добавить поле «applinks» и «details» для описания того, какое именно приложение связывается с сайтом и какие пути обрабатывает. Полный формат файла можно найти здесь.

Важно правильно составить appIDs. Строка идентификатора должна быть из двух частей:

<Application Identifier Prefix>.<Bundle Identifier>


Эти данные можно достать из вашей карточки приложения в App Store Connect.

🔴Теперь последний шаг, общий для обеих платформ — созданные файлы apple-app-site-association и assetlinks.json необходимо загрузить на сайт, поддержку хоста которого вы добавили в приложении. После загрузки файлы должны быть доступны по путям:

https://www.example.ru/.well-known/apple-app-site-association
https://www.example.ru/.well-known/assetlinks.json

🔴После загрузки в том же App Links Assistant нужно подтвердить загрузку, чтобы проверить подключение. Для iOS необходимо выждать 24 часа, чтобы Apple получил доступ к файлу.

Вуаля, теперь ваше приложение будет открываться по ссылке https://www.example.ru!

📎Посмотреть инструкции для Flutter, для Android и для iOS
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥126👍3❤‍🔥1👌1
Привет, с вами Роза, Flutter Dev Friflex 👋

В прошлый раз мы обсудили, почему логирование важно и как оно помогает находить проблемы. Сегодня я хочу поделиться хорошими (и не очень) практиками и ответить на главный вопрос: какими должны быть логи?

Когда только начинаешь логировать, легко запутаться: Что писать? Как часто? Что включать в сообщение?

Конечно, многое зависит от проекта, но есть универсальные советы, которые подойдут почти всем.

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

Не логируйте конфиденциальную информацию. Пароли, номера карт, токены — все это не должно попадать в логи. Если нужно — хешируйте или маскируйте данные.

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

Не пишите логи «просто чтобы были». Пишите так, чтобы при возникновении ошибки вы могли точно понять, в чем проблема и где она произошла.

Не логируйте все подряд: если вам нужно проверить наличие одного ключа — не нужно выводить весь массив.

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

Не используйте `.runtimeType` для имен классов. При обфускации они превращаются в непонятный набор символов. Лучше явно указывать имя класса или использовать собственные идентификаторы.

Копируйте не только ошибки. Фиксируйте также важные события, которые помогут отследить поведение приложения.

Общие советы
🔴Форматируйте логи с помощью библиотек — это упростит чтение и анализ
🔴Фильтруйте логи во время отладки — сосредоточьтесь на ключевых событиях.
🔴Продумайте систему логирования заранее — особенно если проект будет масштабироваться
🔴Храните логи безопасно — используйте защищенные директории и сервисы
🔴Очищайте старые логи — настройте автоматическую ротацию логов
🔴Не оставляйте логи из разработки в проде — перед релизом все нужно почистить
🔴Используйте облачные сервисы для мониторинга логов — Sentry, Firebase Crashlytics и другие
🔴Не игнорируйте ошибки в логах — лучше разобраться с ними сразу

Если у вас есть свои лайфхаки или боли, связанные с логированием — делитесь в комментариях💬
Или... все-таки писать везде print? 😅
Please open Telegram to view this post
VIEW IN TELEGRAM
7👍6🔥1
Привет, это Катя, Flutter Dev Friflex. Сегодня расскажу об организации файлов и папок в проекте. Здесь есть несколько основных подходов.

Стандартная структура (по типам файлов)

lib/
├── models/
├── services/
├── widgets/
├── screens/
├── utils/
└── main.dart

Плюсы:
▫️Простота понимания
▫️Быстрый старт для небольших проектов

Минусы:
▫️Может превратиться в беспорядок, если проект большой
▫️Сложнее находить связанные файлы

Функциональная структура (по фичам)

lib/
├── feature_a/
│ ├── models/
│ ├── widgets/
│ ├── screens/
│ └── bloc/
├── feature_b/
│ ├── models/
│ ├── widgets/
│ ├── screens/
│ └── bloc/
├── core/
│ ├── app/
│ ├── constants/
│ ├── services/
│ └── utils/
└── main.dart

Плюсы:
▫️Лучшая масштабируемость
▫️Четкое разделение ответственности
▫️Удобство для командной работы

Минусы:
▫️Сложнее для новичков
▫️Избыточность для маленьких проектов

Гибридная структура
Сочетает оба подхода: начинаете с типа файлов и переходите к фичам по мере роста проекта.

Что входит в директории?

Core-директория содержит общие элементы приложения:
▫️app — основная конфигурация приложения
▫️constants — константы, стили, строки
▫️services — API, хранилища, сервисы
▫️utils — вспомогательные функции, extensions
▫️routes — маршрутизация

Feature-директории содержат:
▫️data — модели, DTO, репозитории
▫️domain — бизнес-логика (BLoC, Cubit, Provider)
presentation - UI (виджеты, страницы)
▫️feature.dart — экспорт всех файлов фичи

Что стоит делать?

⚡️Используйте barrel-файлы (feature.dart) для упрощения импортов.

// В папке feature_a/feature_a.dart
export 'models/model_a.dart';
export 'widgets/widget_a.dart';
export 'screens/screen_a.dart';


⚡️Следуйте соглашениям об именовании. В разных командах могут быть свои правила, я покажу на примере, как это заведено у нас:
*_screen.dart для полноценных страниц
*_model.dart для моделей данных
*_event.dart, *_state.dart для BLoC

⚡️Избегайте глубокой вложенности — старайтесь не превышать 3-4 уровня.

⚡️Разделяйте по ответственности, а не по типам, когда проект растет.

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

А какой подход используете вы?
🔥125👍2🥰2
Привет, это Юра Петров, руководитель отдела разработки Friflex👋

Хочу лично пригласить вас на конференцию по кроссплатформенной мобильной разработке, которая пройдет 11 апреля в Москве @omp_ru.

Поговорим про Flutter, PWA и KMP — обо всем, что нужно знать, если вы в теме или хотите в нее влиться. Я расскажу, как мы портировали Flutter-приложения на ОС Аврора: покажу кейсы «Дикси», ЭНЕРГОГАРАНТ, idChess и «Мобильный агент». Продемонстрирую, как делить приложение на отдельные сервисы.

Если вам интересно, как запускать приложения на ОС Аврора с помощью привычных инструментов — приходите!

Где: Москва, пр-т Вернадского, 41, БЦ Академик, 4 этаж
Когда: 11 апреля 2025
Регистрация: timepad

Увидимся🙌
🔥115
Привет, это Анна, Flutter Team Lead Friflex!

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

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

Здесь на помощь придет библиотека rxdart. Разберемся с ее возможностями.

Подключение к проекту у библиотеки стандартное — достаточно добавить зависимость в pubspec.yaml.

1. Классы потоков
rxdart дает доступ к множеству дополнительных Stream-классов. Вот некоторые из них:
▫️TimerStream — выдает заданное значение только по окончании заданного промежутка времени

TimerStream('событие', Duration(minutes: 1))
.listen((i) => print(i)); // выводит 'событие' через 1 минуту


▫️MergeStream — объединяет события нескольких потоков в один

MergeStream([
TimerStream(1, Duration(days: 10)),
Stream.fromIterable([2])
])
.listen(print); // выводит 2, 1


▫️RangeStream — возвращает поток int-значений по указанному диапазону

RangeStream(1, 3).listen((i) => print(i)); // выводит 1, 2, 3


2. Расширения
Кроме классов библиотека дает возможность использовать у стандартных экземпляров Stream дополнительные функции с помощью расширений:

▫️delay() — делает задержку выдачи событий на заданный период Duration

Stream.fromIterable([1, 2, 3, 4])
.delay(Duration(seconds: 1))
.listen(print); // [через секунду] выводит 1, 2, 3, 4 одномоментно


▫️debounce() — при отсутствии заданного Duration паузы между событиями игнорирует их, дает доступ только к собятиям с паузами

Stream.fromIterable([1, 2, 3, 4])
.debounce((_) => TimerStream(true, Duration(seconds: 1)))
.listen(print); // выводит 4


▫️mapTo() — выдает константное значение каждый раз, когда поступает событие

Stream.fromIterable([1, 2, 3, 4])
.mapTo(true)
.listen(print); // выводит true, true, true, true


▫️takeLast() — пропускает только те события, которые были сгенерированы после получения какого-то конкретного значения

Stream.fromIterable([1, 2, 3, 4, 5])
.takeLast(3)
.listen(print); // выводит 3, 4, 5


3. Объекты Subjects
Subjects в rxdart — это те же стандартные объекты StreamController, но с дополнительными функциями. Всего их два:

▫️BehaviorSubject — контроллер, который кэширует последнее полученное значение. В момент подписки на поток, управляемый этим контроллером, подписчик получает первым то событие, которое было сгенерировано последним перед его подключением. Этот объект как раз прекрасно позволяет решить кейс, описанный в начале поста.

▫️ReplaySubject — тоже кеширует события, как и BehaviorSubject. Если вам необходимо сохранять не только последнее событие, а еще и другие, этот объект прекрасно справится с этой задачей.

4. Объект Observable
Observable — аналог Stream, в большинстве случаев работает идентично стандартным Stream. Но команда fluttercommunity.dev предупреждает, что в некоторых ситуациях поведение может сильно отличаться. С этими отличиями перед использованием стоит ознакомится в документации.

Делитесь в комментариях своим опытом использования rxdart и работы с потоками во Flutter-приложениях💬
Please open Telegram to view this post
VIEW IN TELEGRAM
11🔥7😍3
Привет, это Роза, Flutter Dev Friflex 👋

Уверена, многие из вас знакомы с Dart DevTools и уже использовали его для анализа своих Flutter-приложений. Но пробовали ли вы создать собственные расширения?

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

Первым делом создаем новый пакет для нашего расширения. Вы сможете это сделать при помощи команды flutter create --template=package my_dev_tools_ext, либо же вручную.

Далее подключим пакет devtools_extensions — он содержит весь необходимый набор инструментов для создания собственного расширения: доступ к виртуальной машине, темам, виджетам и другим возможностям DevTools.

Например, вы получаете доступ к менеджерам:
▫️extensionManager — взаимодействие с DevTools
▫️serviceManager — доступ к VM (если подключена)
▫️dtdManager — для связи с Dart Tooling Daemon

Следующим шагом необходимо создать папку devtools в корне вашего пакета и в ней папку build, а также добавить файл конфигурации config.yaml.


my_dev_tools_ext/
extensions/
devtools/
build/
config.yaml
lib/
src/
...


Файл config.yaml:


name: my_dev_tools_ext # Имя пакета-расширения
issueTracker: <ссылка_на_трекер>
version: 0.0.1
materialIconCodePoint: '0xe0b1' # Иконка из Material Icons
requiresConnection: true # Нужно ли подключение к VM (по умолчанию true)



Теперь создаем UI. Кроме devtools_extensions, очень полезным будет пакет `devtools_app_shared`. Он содержит готовые компоненты и утилиты, которые используются в оригинальных DevTools.

Например (для кнопки):


DevToolsButton(
onPressed: () async {
await someService.saveData();
extensionManager.showNotification('Данные успешно сохранены!');
},
icon: Icons.save,
label: 'Сохранить',
)



👉extensionManager.showNotification покажет уведомление (в симулированной среде это будет лог в консоли).

Подключение расширения
Добавим обертку DevToolsExtension, которая инициализирует расширение:


void main() {
runApp(const LocalizationDevToolsExtension());
}

class LocalizationDevToolsExtension extends StatelessWidget {
const LocalizationDevToolsExtension({super.key});

@override
Widget build(BuildContext context) {
return const DevToolsExtension(
child: LocalizationSnapshotterWidget(), // ваш основной виджет
);
}
}



Чтобы протестировать расширение, вы можете воспользоваться симулированной средой, запустив команду из корня вашего пакета расширения:

dart run -d chrome --dart-define=use_simulated_environment=true


Если все работает корректно, соберите расширение:

dart run devtools_extensions build_and_copy --source=. --dest=extension/devtools


После этого в devtools/build/ появится сборка, готовая к публикации или локальному использованию.

Как работают расширения DevTools?
Все просто: расширение — это обычный Dart-пакет. Вы можете встроить его в другой pub-пакет или создать отдельный. Чтобы расширение появилось в интерфейсе DevTools, его нужно подключить как зависимость в проекте, где DevTools используются.

Это только верхушка айсберга. Но уже можно поэкспериментировать и начать знакомство с основными пакетами!
🔥10👍7🥰4
Вторая попытка🤫 Какой state-менеджмент вы используете?
Anonymous Poll
1%
Redux 🧓
5%
MobX 🧠
7%
GetX
1%
yx_scope 🧸
13%
Riverpod 🧙‍♂️
73%
Bloc 👩‍💼
Привет, это Катя, Flutter Dev Friflex. Сейчас расскажу про три решения: Bloc, Riverpod и yx_scope, и еще немного про альтернативные подходы.

Bloc
Bloc — это предсказуемый state-менеджмент, основанный на концепции Unidirectional Data Flow (однонаправленный поток данных).

Основные концепции
Events — действия, которые триггерят изменения
States — иммутабельные объекты, описывающие состояние приложения
Bloc — класс, который обрабатывает Events и эмитит States

Плюсы
◽️Четкое разделение логики и UI
◽️Хорошая документация и большое сообщество
◽️Поддержка Cubit (упрощенная версия Bloc)

Минусы
◽️Высокая шаблонность (много повторяющегося кода)
◽️Избыточность для простых сценариев — если состояние приложения простое, Bloc может быть слишком мощным

Пример использования

class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0);

@override
Stream<int> mapEventToState(CounterEvent event) async* {
if (event is Increment) yield state + 1;
if (event is Decrement) yield state - 1;
}
}

Riverpod
Riverpod — это улучшенная версия Provider, созданная тем же автором (Remi Rousselet). Он решает проблемы Provider (например, Null safety и тестируемость).

Основные концепции
Provider — источник данных (может быть StateProvider, FutureProvider или другой)
Consumer — виджет, который читает провайдер
AutoDispose — автоматическая отписка от провайдеров

Плюсы
◽️Нет зависимости от BuildContext
◽️Лучшая поддержка тестирования
◽️Гибкость (можно использовать как DI или state-менеджмент)

Минусы
◽️Неочевидная работа с асинхронностью — AsyncValue требует дополнительной обработки ошибок и загрузки
◽️Меньше документации по сравнению с Bloc

Пример использования

final counterProvider = StateProvider<int>((ref) => 0);

class CounterWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: Text('Count: $count'),
);
}
}

yx_scope
yx_scope — это легковесная библиотека для управления состоянием, вдохновленная ScopedModel и InheritedWidget.

Основные концепции
Scope — контейнер для состояния
InheritedScope — автоматически обновляет виджеты при изменении состояния

Плюсы
◽️Простота использования.
◽️Хорошо подходит для небольших приложений.
◽️Низкая шаблонность (минимум повторяющегося кода).

Минусы
◽️Меньше возможностей, чем у Bloc/Riverpod.
◽️Меньше документации
◽️Плохая масштабируемость — в больших проектах библиотека может стать непредсказуемой

Пример использования

class CounterScope extends Scope {
var count = 0;
void increment() => notifyListeners(count++);
}

class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) => TextButton(
onPressed: CounterScope.of(context).increment,
child: Text('Count: ${CounterScope.of(context).count}'),
);
}


Добавляю в комментарии табличку сравнений state-менеджментов. Давайте обсудим!
10🔥6👍4🤡3
Всем привет! С вами Анна, Friflex Flutter Team Lead.

Любой Flutter-разработчик, создавая свой первый проект задавался хоть раз вопросом — с чего начать, что делать и, главное, как упростить себе работу? Сегодня разберу основные шаги, которые позволят подготовить базу.

1 шаг. Создание проекта
Здесь все очень просто - создать проект можно всего одной командой через терминал. Достаточно выполнить:

flutter create new_app

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

О других вариантах можно прочитать здесь.

2 шаг. Настройка запуска приложения

Все знают: чтобы запустить Flutter-проект, достаточно вызвать метод main() и запустить функцию runApp() с виджетом приложения внутри. Здесь вы также можете выполнять любые настройки, которые потребуются перед запуском вашего приложения — например, устанавливать ориентацию экрана и базовую локализацию.

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

Здесь нужно инициализировать FlutterError.onError и PlatformDispatcher.instance.onError, указать, как именно приложение должно реагировать на ошибки фреймворка и платформы.

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

3 шаг. Настройка флаворов
Здесь на помощь вам придет библиотека flutter_flavorizr.

Подключаете пакет в зависимости вашего приложения, а дальше дело за малым — по примеру из документации нужно указать, какие именно флаворы необходимы, какое название и bundleId должно иметь приложение и какие плаформы оно будет поддерживать:

flavorizr:
flavors:
prod:
app:
name: "New App"
android:
applicationId: "com.example.prod"
ios:
bundleId: "com.example.prod"
dev:
app:
name: "New App Dev"
android:
applicationId: "com.example.dev"
ios:
bundleId: "com.example.dev"

Далее остается только запустить кодогенерацию.

flutter pub run flutter_flavorizr


Немного подробнее про флаворы можно почитать здесь.

4 шаг. Реализовать DI
Здесь конкретных рекомендаций нет, каждый разработчик самостоятельно выбирает подход — можно сделать самописную реализацию, без кодогенерации и сторонних библиотек, можно интегрировать самые популярные пакеты, например, get_it и injectable.

Flutter тоже дает свое видение DI, можно ознакомиться с ним в документации.

5 шаг. Интегрировать роутер
Именно на этом этапе, когда в приложении нет как таковых экранов, удобно продумать подход к роутингу в проекте. В зависимости от подхода можно использовать как навигацию из коробки, так и сторонние библиотеки.

Наиболее популярные и стабильные — go_router и auto_route. Если есть необходимость и желание попробовать полноценный декларативный подход — вам подойдет octopus.

Готово! Приложение полностью подготовлено к написанию самой первой фичи.

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

Делитесь, каким было ваше первое приложение?
11🔥6❤‍🔥1
Всем привет, это Роза, Flutter Dev Friflex! 👋  

В прошлый раз мы обсудили, как можно создать собственное расширение для DevTools и какие пакеты для этого пригодятся. Я показала вам пример простенького расширения, оформленного как отдельный Dart-пакет.  

Сегодня расскажу, как встроить такое расширение прямо в существующий pub-пакет.

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

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

💡Как это реализовать?
Все довольно просто. В вашем Dart-пакете, который предоставляет расширение DevTools, нужно добавить каталог extension на верхнем уровне структуры:

some_package/   
  extension/   
  lib/   
  ...


А структура папки extension будет выглядеть следующим образом:

extension/
  devtools/
    build/
    config.yaml


Вы получите примерно такую структуру:

some_app/   

  packages/
   some_package/   
      extension/
       devtools/
         build/
           ...   
          config.yaml
   some_devtools_extension/
     lib/ 


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

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

🔖Теперь вы знаете еще больше о создании расширений DevTools! В следующий раз я расскажу, как можно взаимодействовать со сторонним кодом с помощью Eval — до встречи.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍85🔥1
This media is not supported in your browser
VIEW IN TELEGRAM
Привет, это Катя, Flutter Dev Friflex.

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

💡Что такое Process?
Класс dart:io → Process позволяет запускать внешние процессы и взаимодействовать с ними: передавать аргументы, получать стандартный вывод (stdout), ошибки (stderr) и код завершения.

Как использовать?
Рассмотрим на примере, в котором будем выводить результат flutter --version в консоль.


import 'dart:io';

void main() async {
  final result = await Process.run('flutter', ['--version']);

  if (result.exitCode == 0) {
    print('Flutter version: ${result.stdout}');
  } else {
    print(Error: ${result.stderr}');
  }
}


Что происходит:
▫️Process.run — запускает процесс и возвращает результат после его завершения
▫️'flutter' — команда, которую мы хотим выполнить
▫️['--version'] — список аргументов для команды
▫️result.stdout — стандартный вывод
▫️result.stderr — ошибка
▫️result.exitCode — код завершения (0 — успех, иначе ошибка)

Методы Process
▫️run(executable, arguments) — запускает процесс, дожидается его завершения и возвращает результат
▫️start(executable, arguments) — запускает процесс и возвращает объект Process, не дожидаясь завершения
▫️killPid(pid) — завершает любой процесс по его PID, даже если нет объекта Process
▫️kill() — завершает процесс, запущенный через start.
▫️getter exitCode — возвращает код выхода

Важно помнить
▫️
Команда должна быть доступна в окружении (PATH)
▫️Всегда оборачивайте вызовы в try-catch на случай ошибок
▫️Будьте осторожны с пользовательским вводом: не передавайте его напрямую в Process без фильтрации

Мой опыт
Я использовала Process в проекте, где нужно было обрабатывать видео с помощью ffmpeg. Команды нарезки и склейки видео выполнялись прямо из Flutter-приложения, и Process стал отличным способом обернуть это взаимодействие с CLI.

Вывод
Process во Flutter (точнее, в Dart) — это мощный инструмент для расширения возможностей приложения: от автоматизации до взаимодействия с внешними утилитами.

Вы когда-нибудь использовали Process в Flutter или Dart? Для чего он вам понадобился?
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥136👍2
Всем привет! На связи Анна, Friflex Flutter Team Lead👋

🔑Как часто вы используете ключи виджетов в ваших приложениях? Сегодня поговорим, какие ключи бывают, чем отличаются и как могут быть полезны на практике.

Каждый виджет во Flutter имеет nullable поле key. Сюда мы можем передавать различные имплементации класса Key.

Под капотом у Flutter — дерево виджетов (Widget tree) и дерево элементов (Element tree). Здесь кратко разберемся в отличиях.

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

Чтобы глубже разобраться в том, как работает дерево виджетов и дерево элементов, рекомендую статью на Хабре.

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

Ключи бывают двух видов — глобальные и локальные. К глобальным относится GlobalKey, у локальных есть три вариации — ValueKey, ObjectKey и UniqueKey.

Локальные ключи сохраняют состояние виджета на своем одном уровне дерева, только в рамках текущего контекста.
▫️UniqueKey — не принимает никакого значения, он уникален сам по себе.
▫️ValueKey и ObjectKey — уникальны за счет своего значения. Отличаются механизмом сравнения под капотом (ValueKey сравнивает значение value, а ObjectKey выполняет сравнение по ссылке).

▫️Глобальные ключи GlobalKey могут управлять состоянием по всему дереву, поэтому дают доступ к текущему контексту виджета.

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

Делитесь в комментариях своим опытом использования ключей🙌
Please open Telegram to view this post
VIEW IN TELEGRAM
8🔥5🥰1🤡1
Проверка на внимательность. Какой ключ будете использовать для управления декларативной навигацией в приложении?
Anonymous Quiz
4%
ValueKey
11%
ObjectKey
77%
GlobalKey
8%
UniqueKey
This media is not supported in your browser
VIEW IN TELEGRAM
Привет, это Роза, Flutter Dev Friflex, и я продолжаю серию постов про расширения DevTools.

В прошлый раз рассказала, как встроить расширение прямо в существующий pub-пакет. Сегодня разберем, как взаимодействовать с кодом приложения извне и динамически выполнять Dart-код. Поговорим об EvalOnDartLibrary.

EvalOnDartLibrary — это класс из пакета devtools_app_shared, который позволяет выполнять Dart-код прямо из вашего расширения DevTools.  
Благодаря нему можно:
▫️управлять состоянием приложения
▫️вызывать методы
▫️получать значения из рантайма

Как это возможно?
1. Инициализация

Future<void> initEval() async {
  await serviceManager.onServiceAvailable; // Убедимся, что vmService доступен

  _controllerEval = EvalOnDartLibrary(
    'package:some_package/src/controller.dart', // Путь к библиотеке, где находится нужный код
    serviceManager.service!,                   // Передаем vmService
    serviceManager: serviceManager,
  );

  evalDisposable = Disposable(); // Обязательно создаем Disposable
}


Пояснения:
◽️ 'package:...' — путь до нужного кода
◽️ serviceManager.service! — экземпляр VmService
◽️ Disposable — защищает от утечек памяти при закрытии DevTools

2. Выполнение кода
EvalOnDartLibrary предоставляет три метода:
▪️asyncEval — асинхронное выполнение кода
▪️ eval — синхронное выполнение
▪️ evalInstance — получение экземпляра объекта (для дальнейшей работы с его полями и методами)
▪️ safeEval — безопасная обертка над eval, которая дополнительно обрабатывает ошибки выполнения
    
Ограничение: у eval и asyncEval есть лимит на размер возвращаемых данных.

Рекомендуется:
1. Сначала вызвать метод через asyncEval или eval
2. Потом получить значение через evalInstance.
Future<String?> _getValue() async {
  await _controllerEval.asyncEval(
    'await SomeController.instance.calculateValue()', // Запуск асинхронной функции
    isAlive: evalDisposable,
  );

  final result = await _controllerEval.evalInstance(
    'SomeController.instance.sum.value', // Получение значения после выполнения
    isAlive: evalDisposable,
  ); // Результатом является объект типа Instanse

  return result?.valueAsString; // Возвращаем строковое значение
}



Не забывайте передавать isAlive: evalDisposable — без этого могут быть утечки памяти!

Что такое Instance?

Метод evalInstance возвращает Instance — ссылку на результат выполнения кода. Вы можете его преобразовать в  строку через valueAsString. Но убедитесь, что значение действительно можно привести к строке (иначе может быть ошибка).  

Пример:
final result = await _controllerEval.evalInstance(
  'SomeController.instance.sum.value.toString()',
  isAlive: evalDisposable,
);

final value = result?.valueAsString;


Мини-памятка
✔️Используйте Disposable для управления жизненным циклом
✔️ Следите за ограничением на размер результата
✔️ Передавайте только валидные Dart-выражения
✔️ При изменениях в проекте обновляйте пути в EvalOnDartLibrary

Для меня это был неплохой опыт с DevTools — я даже не знала, что так можно. А еще через serviceManager можно получить код приложения через id изолята (но там свои нюансы).

На этом мы заканчиваем нашу мини-серию по расширениям DevTools! Я рассказала далеко не все, но теперь вам будет гораздо проще ориентироваться🚀

Подробнее о методах EvalOnDartLibrary — в комментариях👇
Please open Telegram to view this post
VIEW IN TELEGRAM
5👍3🔥2👌1
Привет, это Катя, Flutter Dev Friflex.

При разработке на Flutter важно не просто уметь писать рабочий код, но и понимать, как работает язык Dart, на котором он основан. Одна из базовых тем — ключевые слова final, const и var. Они отвечают за то, как переменные создаются и ведут себя в процессе выполнения программы. Давайте разберем и повторим базу😁

var (переменная)

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

Пример:
void main() {   
   var name = 'Alice';  // Тип определяется как String   
   print(name);  // Alice      
   name = 'Bob';  // Допустимо, так как тип остаётся String   
   print(name);  // Bob 
}

Если тип данных переменной var известен при создании, Dart сам выводит тип переменной (например, String, int и т.д.).
Переменная, объявленная с var, может изменять свое значение, но не тип.

Пример попытки изменить тип переменной:
var age = 30; age = "thirty";  // Ошибка: String нельзя присвоить переменной типа int.

​​
final (константа во время выполнения)

Используется для переменных, значение которых можно установить только один раз. После инициализации переменной значение нельзя изменить. В отличие от const, переменная с final может быть инициализирована значением, которое становится известно только во время выполнения программы.

Пример:
void main() {   
    final name = 'Charlie';  // name будет неизменяемым   
    print(name);  // Charlie    
    name = 'Dave';  // Ошибка, нельзя изменить значение переменной 
}

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

Пример использования переменной, которая зависит от выполнения программы:
void main() {   
    final currentTime = DateTime.now();  // Значение определяется во время выполнения   
    print(currentTime); 
}

В этом примере время будет определено при выполнении программы и больше не изменится.

const (константа времени компиляции)

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

Пример:
void main() {   
    const pi = 3.1415;   
    print(pi);  // 3.1415   
    pi = 3.14;  // Ошибка, нельзя изменить значение константы 
}

◽️Переменные, объявленные с const, являются неизменяемыми и создаются во время компиляции программы.

◽️Если переменная const имеет сложный тип (например, список), то она становится полностью неизменяемой (не только ссылка, но и сам объект).

Пример со списком:
void main() {   
    const numbers = [1, 2, 3];   
    // numbers[0] = 10;  // Ошибка, элементы списка изменить нельзя
    print(numbers); 
}


Различия между final и const
▫️final позволяет установить значение переменной один раз, но это значение может быть вычислено во время выполнения программы.
▫️const требует, чтобы значение было известно на этапе компиляции.
▫️const можно использовать для создания неизменяемых объектов, которые становятся доступными еще до выполнения программы, в отличие от final.

Пример разницы:
final currentTime = DateTime.now();  // Работает, так как значение вычисляется во время выполнения. 
const timeConst = DateTime.now();  // Ошибка: const переменные не могут быть вычислены во время выполнения.


Итоговое сравнение:
▪️var — изменяемая переменная, тип выводится автоматически.
▪️final — неизменяемая переменная, значение можно присвоить один раз, но это может произойти во время выполнения.
▪️const — неизменяемая переменная, значение которой должно быть известно во время компиляции.
🔥117
Привет, это Анна, Friflex Flutter Team Lead!👋

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

Обфускация простыми словами — некоторое запутывание кода с помощью определенного набора символов. При обфускации код сборки становится нечитаемым для человека. Названия всех методов и классов подменяются другими символами.

Какую проблему решает? Обфускация сильно затрудняет процесс понимания кода, полученного в процессе реверс-инжиниринга. Посмотрим, как это работает.

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

Результат — на карточке👆
Вывод — код сборки с обфускацией прочитать почти невозможно.

Обфусцировать код Flutter-приложения очень просто – достаточно запустить команду сборки следующим образом:

flutter build <build-target> \ 
--obfuscate \
--split-debug-info=/<symbols-directory>


Здесь очень важно использовать опцию --split-debug-info, она по указанному пути выгрузит символы обфускации, с помощью которых можно будет в дальнейшем расшифровывать важные логи, например, стектрейсы ошибок.

При обфускации нельзя в коде использовать сравнение строковых представлений runtime-типов.

Попробуйте для примера сделать подобную реализацию в сборке с обфускацией и без.

class User {}

Text(user.runtimeType.toString());

Text ("${user.runtimeType.toString() == "User"");

Без обфускации на экране появятся значения User и true. С обфускацией в первом виджете будет набор рандомных символов, во втором — false.

🔥 — если используете обфускацию в своих проектах.
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥12👍74❤‍🔥2
Please open Telegram to view this post
VIEW IN TELEGRAM
10🔥5👍4