Flutter Friendly
Відкрити в Telegram
Канал Friflex о разработке на Flutter. Обновления, плагины, полезные материалы — превращаем знания в реальный опыт, доступный каждому разработчику. 🔗 Наш канал для разработчиков: @friflex_dev 🔗 Канал о продуктовой разработке: @friflex_product
Показати більше1 015
Підписники
-124 години
-17 днів
-430 день
Архів дописів
1 015
Всем привет! С вами Анна, Friflex Flutter Team Lead👋
Одна из самых распространенных болей многих Flutter-проектов — это отсутствие тестов в коде. И хоть тема не новая, поговорим сегодня о том, какие виды тестов бывают, для чего они нужны и почему их наличие может спасти ваш проект.
Существуют три вида тестов:
🔸 Unit-тесты
Их основная задача —протестировать работоспособность какой-то одной конкретной функции, метода, класса по всем возможным сценариям
🔸 Widget-тесты
Этот тип тестов призван проверять именно отдельные виджеты. Widget-тесты позволяют проверить, действительно ли тот или иной виджет корректно располагается на экране и правильно себя ведет при взаимодействии с ним пользователя
🔸 Integration-тесты
Интеграционные тесты — самый сложный в настройке, но очень полезный тип тестов. Он позволяет проверить большие участки вашего приложения, протестировать взаимодействие разных модулей и виджетов между собой. Если юнит- и виджет-тесты призваны проверять работу отдельно только одного объекта в проекте, то интеграционные проверяет все в совокупности. Кроме этого, они также позволяют проверить производительность всего приложения.
Почему же отсутствие тестов является проблемой? Многие разработчики скажут, что написание тестов — лишняя трата времени, человеческих ресурсов и вообще занятие монотонное и скучное. Но здесь есть пара весомых доводов.
✅Когда проект маленький, простой, не нагружен сложной логикой, его ручное тестирование занимает немного времени и усилий QA-специалистов. Но с ростом приложения, с течением времени многие нюансы той или иной фичи забываются, их ручная проверка становится все менее эффективной.
✅Сюда же можно отнести и человеческий фактор — разработчики, тестировщики могут меняться, а знания — теряться.
Здесь на помощь приходят тесты в коде проекта. Стоит всего один раз при разработке того или иного функционала покрыть тестами всевозможные сценарии, и в будущем вероятность пропустить в продакшн сломанный функционал сильно снижается.
💡Совет: интегрируйте в свой CI/CD процесс проверки всех написанных тестов в проекте. Если тесты не проходят, сборке или пул-реквесту лучше не попадать на следующий этап жизненного цикла задачи. Так вы не будете забывать вовремя править тесты в случае изменения логики работы приложения, а также сэкономите время коллег на проверку.
1 015
Привет, с вами вновь Катя, Flutter Dev Friflex.
Сегодня поговорим про библиотеку equatable, которая помогает упростить сравнение объектов.
Установка
Yaml dependencies: equatable: ^2.0.7Зачем нужна библиотека? В Dart по умолчанию два объекта считаются равными, только если они ссылаются на один и тот же экземпляр в памяти. Но часто требуется сравнивать объекты по их полям. Equatable автоматически реализует методы == (оператор равенства) и hashCode, что избавляет разработчиков от написания шаблонного кода. Без Equatable:
class Person {
Person(this.name, this.age);
final String name;
final int age;
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Person &&
other.name == name &&
other.age == age;
}
@override
int get hashCode => name.hashCode ^ age.hashCode;
}
С Equatable:
import 'package:equatable/equatable.dart';
class Person extends Equatable {
Person(this.name, this.age);
final String name;
final int age;
@override
List<Object?> get props => [name, age]; // Поля для сравнения
}
Основные возможности
▫️Equatable переопределяет == и hashCode на основе списка полей (props)
▫️Можно использовать Equatable в иерархии классов
▫️Рекомендуется использовать с неизменяемыми (final) полями
▫️Часто применяется в State Management для эффективного сравнения состояний
Использование EquatableMixin
Если ваш класс уже наследуется от другого класса, но вам нужна функциональность Equatable, можно использовать
EquatableMixin:
import 'package:equatable/equatable.dart';
class Person extends SomeOtherClass with EquatableMixin {
Person(this.name, this.age);
final String name;
final int age;
@override
List<Object?> get props => [name, age];
}
Преимущества EquatableMixin
▫️Позволяет добавить сравнение объектов без изменения иерархии наследования
▫️Сохраняет все возможности Equatable, но в виде миксина
Пример использования
void main() {
final person1 = Person("Alice", 30);
final person2 = Person("Alice", 30);
print(person1 == person2); // true (без Equatable было бы false)
}
📎 Ссылка на библиотеку
❤️ — если используете Equatable в ваших проетах1 015
Repost from Friflex Dev
🔥Статья нашей Flutter-разработчицы Кати — в шорт-листе «Технотекста» Хабра
Норм или стрем использовать сторонние библиотеки под любую проблему? А выносить виджеты в методы вместо создания отдельных классов? Ответы — в Катиной статье «Антитренды в мобильной разработке на Flutter».
Она разобрала решения, которые кажутся удобными, а на деле раздражают и замедляют работу, и предложила, чем их стоит заменить.
🔜 Читать на Хабре
💜 Поддержать автора лайком
1 015
Привет! Это Роза, Flutter Dev Friflex! 👋
Когда я только начинала работать, почти не пользовалась горячими клавишами. У меня была мышка. И этого вроде бы хватало, но со временем мне захотелось ускорить свою работу, поэтому я целенаправленно начала внедрять шорткаты в свою разработку. И это было лучшим решением!
Хочу поделиться и с вами самыми полезными из них — вдруг пригодятся.
🔴Начнем с навигации и поиска:
⌘ + P — открыть файл по имени
⌘ + Shift + F — поиск по всему проекту
⌘ + G — перейти к строке
⌘ + Shift + O — перейти к функции или символу в файле
🔴Редактирование кода:
Control + Space — автодополнение
⌘ + / — закомментировать или раскомментировать строку
Option + ↑/↓ — переместить строку вверх или вниз
Shift + Option + ↑/↓ — скопировать строку вверх или вниз
⌘ + Shift + K — удалить строку
Shift + Option + F — отформатировать документ
Option + Click — добавить множественный курсор
⌘ + Shift + L — выделить все вхождения слова
⌘ + F, затем ⌘ + Option + Enter — быстрая замена
🔴Мультикурсор и выделения:
Option + Click — добавить курсор в точку клика
⌥⌘ + ↑/↓ — вставить курсор выше / ниже
⌘ + U — отменить последнее действие с курсором
⇧⌥ + I — вставить курсор в конец каждой выбранной строки
⌘ + L — выделить текущую строку
⇧⌘ + L — выделить все вхождения текущего выделения
⌘ + F2 — выделить все вхождения текущего слова
Control + ⇧⌘ + →/← — расширить/уменьшить выделение
🔴Блочное выделение:
⇧⌥ + drag — выделение прямоугольником (box selection)
⇧⌥⌘ + ↑/↓/←/→ — выделение вверх/вниз/влево/вправо
⇧⌥⌘ + PgUp/PgDn — постраничное вертикальное выделение
💡Конечно, вы можете настроить сочетания клавиш под себя и увидеть весь их список:
Code → Settings → Keyboard Shortcuts или нажмите ⌘ + K, затем ⌘ + S.
✏️Также полный список есть тут: PDF от VSCode.
1 015
👾Фьють — и апреля нет! Но крутые посты остались.
Топ-5 за этот месяц:
🔴Как интегрировать динамические ссылки во Flutter-приложение
🔴Как лучше организовать папки и файлы в проекте
🔴С чего начать создание своего собственного расширения
🔴Как создать свой проект
🔴Какой state-менеджмент выбрать для вашего проекта
Готовим на май еще больше полезного контента. Пусть ваш код летает, а баги обходят стороной 🚀
1 015
+1
Привет, это Анна, 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.
🔥 — если используете обфускацию в своих проектах.1 015
Привет, это Катя, 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 — неизменяемая переменная, значение которой должно быть известно во время компиляции.1 015
Привет, это Роза, 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 — в комментариях👇1 015
Проверка на внимательность. Какой ключ будете использовать для управления декларативной навигацией в приложении?
1 015
Всем привет! На связи Анна, 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. А если идентифицировать виджеты нужно только в рамках одной страницы, на помощь придут локальные виджеты.
Делитесь в комментариях своим опытом использования ключей🙌1 015
Привет, это Катя, 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? Для чего он вам понадобился?1 015
Всем привет, это Роза, 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 — до встречи.1 015
Всем привет! С вами Анна, 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-клиент для управления запросами. Но все эти пункты зависят от вашего проекта, поэтому в основную последовательность не включаем.
Делитесь, каким было ваше первое приложение?1 015
Привет, это Катя, 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-менеджментов. Давайте обсудим!1 015
Привет, разработчики! Есть вопрос. Какой state-менеджмент используете?
1 015
Привет, это Роза, 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 используются.
Это только верхушка айсберга. Но уже можно поэкспериментировать и начать знакомство с основными пакетами!1 015
Привет, это Анна, 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-приложениях💬1 015
Привет, это Юра Петров, руководитель отдела разработки Friflex👋
Хочу лично пригласить вас на конференцию по кроссплатформенной мобильной разработке, которая пройдет 11 апреля в Москве @omp_ru.
Поговорим про Flutter, PWA и KMP — обо всем, что нужно знать, если вы в теме или хотите в нее влиться. Я расскажу, как мы портировали Flutter-приложения на ОС Аврора: покажу кейсы «Дикси», ЭНЕРГОГАРАНТ, idChess и «Мобильный агент». Продемонстрирую, как делить приложение на отдельные сервисы.
Если вам интересно, как запускать приложения на ОС Аврора с помощью привычных инструментов — приходите!
Где: Москва, пр-т Вернадского, 41, БЦ Академик, 4 этаж
Когда: 11 апреля 2025
Регистрация: timepad
Увидимся🙌
1 015
Привет, это Катя, 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 уровня.
⚡️Разделяйте по ответственности, а не по типам, когда проект растет.
Выбор структуры зависит от размера и сложности вашего проекта. Начинайте с простого и рефакторите по мере роста приложения. Главное — соблюдать консистентность и следить, чтобы структура оставалась понятной для всех разработчиков в команде.
А какой подход используете вы?
Вже доступно! Дослідження Telegram за 2025 — головні інсайти року 
