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

🔗 Наш канал для разработчиков: @friflex_dev
🔗 Канал о продуктовой разработке: @friflex_product
Download Telegram
Какой баланс у пользователя из примера⬆️
Anonymous Quiz
4%
100.0
88%
70.0
5%
0.0
2%
– 30.0
5👍5
Привет! Это Анна, Friflex Flutter Team Lead👋

Продолжаем разбирать основные принципы объектно-ориентированного программирования. В предыдущих постах мы уже поговорили о наследовании и инкапсуляции. Сегодня рассмотрим еще один принцип — абстракцию.

Абстракция как принцип ООП подразумевает выделение только самых важных методов и полей объекта, при этом все детали внутренней специфической реализации скрыты. В таком подходе разработчик создает некоторый интерфейс, абстрактный класс, который описывает только необходимые извне свойства.

Чем помогает абстракция?
1. Упрощает работу с кодом. Код становится более структурированным и понятным для восприятия
2. Позволяет обезопасить реализации. Все специфические детали реализации скрыты от внешнего воздействия. Это позволяет их сохранить в первозданном виде и снизить вероятность проблем по месту использования
3. Дает возможность безопасно масштабировать проект. Абстракция позволяет снизить зависимость некоторых модулей кода, что упрощает его расширение

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

Подведем итог в виде простого примера реализации абстракции в Dart-коде.
У нас есть задача создать абстракцию оплаты и две реализаци — оплаты по карте и наличными. В самом простом виде получим такой результат.

/// Абстрактный класс — интерфейс оплаты
abstract class Payment {
void pay(double amount);
}

/// Реализация оплаты картой
class CardPayment implements Payment {
@override
void pay(double amount) {
final fee = _calculateFee(amount);
print(
'Оплата $amount руб. с помощью карты. Комиссия составила $fee руб. Итого — ${amount + fee} руб.',
);
}

/// Расчет комиссии за оплату картой - 2%
double _calculateFee(double amount) {
return amount * 0.02;
}
}

/// Реализация оплаты наличными
class CashPayment implements Payment {
@override
void pay(double amount) {
print('Оплата $amount руб. наличными');
}
}

void main() {
final Payment payment1 = CardPayment();
final Payment payment2 = CashPayment();

payment1.pay(
500,
); // Оплата 500 руб. с помощью карты. Комиссия составила 10 руб. Итого — 510 руб.
payment2.pay(200); // Оплата 200 руб. наличными
}


❤️
если было полезно
15❤‍🔥4🔥2
Всем привет! Это Роза, Flutter Dev в Friflex!👋

Продолжаем наше путешествие по миру объектно-ориентированного программирования. Мы уже рассмотрели наследование, инкапсуляцию и абстракцию. Теперь настало время для полиморфизма

Когда я только начинала разбираться в ООП, полиморфизм был для меня самым непонятным: «Объекты, которые могут принадлежать к разным классам, но имеют одинаковое поведение, определенное через общий интерфейс или базовый класс». О как! Давайте попробуем разобраться!

Полиморфизм буквально означает «много форм». Представьте: у вас есть базовый класс, который описывает общее поведение, а в классах-наследниках вы можете переопределить это поведение в соответствии с конкретной реализацией. Это и есть полиморфизм.

Полиморфизм в Dart
В Dart можно выделить два основных типа полиморфизма:

1. Полиморфизм на основе наследования. Дочерние классы наследуют поведение родителя и могут его переопределять

2. Полиморфизм через интерфейсы. Класс реализует интерфейс и обязан описать все его методы. Это позволяет задать единое поведение для разных реализаций

Помните пример со статьями из поста про наследование? Тогда я создала базовый класс с общими методами. Представим, что материалов стало больше. Теперь хочется разделить их по категориям.

class Article {
  const Article(this.title, this.author);

  final String title;
  final String author;

  void write() => print('Пишем статью: "$title"');
  void publish() => print('Публикация статьи "$title", автор: $author');
  void section() => print('Общий раздел');
}


Теперь создадим подклассы с разными разделами:

class OOPArticle extends Article {
  const OOPArticle(String title, String author) : super(title, author);

  @override
  void section() => print('🔹 Раздел: Объектно-ориентированное программирование');
}

class PatternsArticle extends Article {
  const PatternsArticle(String title, String author) : super(title, author);

  @override
  void section() => print('🔸 Раздел: Паттерны проектирования');
}



Теперь мы можем использовать их полиморфно:

void printArticleInfo(Article article) {
  article.write();
  article.section();
  article.publish();
}

void main() {
  final articles = [
    OOPArticle('Что такое полиморфизм?', 'Роза'),
    PatternsArticle('Паттерн Singleton', 'Роза'),
  ];

  for (final article in articles) {
    printArticleInfo(article);
    print('---');
  }
}

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

Кроме классического полиморфизма существуют и другие его формы:
▫️Полиморфизм подтипов (Subtype polymorphism)
Это наиболее привычный вариант, когда один класс наследуется от другого и переопределяет его поведение. Именно его чаще всего имеют в виду, говоря о полиморфизме в ООП.

▫️Параметрический полиморфизм (Generics)
Позволяет писать универсальный код, работающий с разными типами данных. Например:

T identity<T>(T value) =>  value;

void main() {
  print(identity<int>(10));       // 10
  print(identity<String>('Да'));  // Да
}


Здесь функция identity не зависит от конкретного типа — она просто возвращает то, что получила. Тип указывается как параметр <T>.

▫️Ad hoc полиморфизм
Это перегрузка методов, когда у функции одно имя, но разные параметры. Но в Dart такая перегрузка не поддерживается напрямую.

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

Но есть важное правило:
Не объединяйте все подряд в общее поведение. Интерфейс или базовый класс должен описывать действительно общее поведение. Иначе вы рискуете нарушить принцип единственной ответственности (SRP).

На этом все! Если понравилась статья, то ставьте 💙
8🔥2
This media is not supported in your browser
VIEW IN TELEGRAM
Располагаемся ближе к экрану — Friflex выпустили Flutter Starter. И это —🔥

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

Что вы получаете?
Чистая архитектура: Presentation / Domain / Data — без лишнего хаоса
Локализация, темы, модульность — ready
Стиль кода, который не стыдно показать на ревью
Разделение на сервисы для портирования на разные платформы
Простой DI без лишних пакетов, настроенный навигатор

Для кого?
Для всех, кто хочет собирать приложения быстрее, чище, удобнее.

🔗Качайте, меняйте название пакета и начинайте добавлять фичи:
GitHub Friflex Flutter Starter

Пусть меньше времени уходит на настройку — и больше на создание крутых проектов💜
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥239👍8❤‍🔥2
Привет, это Катя, Flutter Dev в Friflex 👋 Сегодня поговорим про Observer.

Что такое Observer?
Паттерн «Наблюдатель» (Observer) — это поведенческий паттерн проектирования, суть которого в том, что один объект (наблюдаемый) может автоматически уведомлять другие объекты (наблюдатели) о своих изменениях.

Это похоже на подписку: один объект «подписывается» на другой, чтобы быть в курсе изменений. Когда наблюдаемый объект изменяется — все подписчики получают уведомление и могут отреагировать.

Где это полезно?

Этот паттерн часто используется, когда:
▫️у нас есть модель данных, и мы хотим автоматически обновлять UI
▫️нужно разделить логику обработки и отображения данных
▫️необходимо уведомить несколько частей системы об одном событии

Как реализовать Observer?
✔️
Создаем абстракцию Observable (или Subject) — объект, за которым наблюдают
✔️Создаем абстракцию Observer — наблюдатель
✔️Реализуем логику хранения списка наблюдателей в Observable
✔️Реализуем метод update в Observer, чтобы обрабатывать изменения

Пример:
abstract class Observer {
  void update(int data);
}

class Observable {
  final _observers = <Observer>[];
  int _value = 0;

  void addObserver(Observer observer) => _observers.add(observer);
  void removeObserver(Observer observer) => _observers.remove(observer);

  set value(int newValue) {
    _value = newValue;
    _notify();
  }

  void _notify() {
    for (final o in _observers) {
      o.update(_value);
    }
  }
}

class LoggerObserver implements Observer {
  @override
  void update(int data) => print('Updated value: $data');
}

void main() {
  final observable = Observable();
  final observer = LoggerObserver();

  observable.addObserver(observer);

  observable.value = 1;
  observable.value = 2;

  observable.removeObserver(observer);
}


Что тут происходит:
▪️Observer — абстрактный класс с методом update, который вызывается при изменении состояния
▪️Observable — содержит список наблюдателей и уведомляет их при изменении значения переменной value
▪️LoggerObserver — конкретный наблюдатель, который просто печатает обновленное значение

В main() мы добавляем наблюдателя, изменяем значение, и он автоматически получает уведомления. Потом мы его удаляем из списка — и уведомления больше не приходят.

Плюсы:
Слабая связанность — наблюдатель не зависит от внутренней логики наблюдаемого
Гибкость — можно динамически добавлять и удалять наблюдателей
Масштабируемость — несколько объектов могут реагировать на одно событие

Минусы:
⚠️ Перегрузка сообщений — большое количество наблюдателей может вызвать перегрузку
⚠️ Циклические зависимости — если наблюдатель и наблюдаемый зависят друг от друга
⚠️ Порядок уведомлений — не гарантируется порядок вызова update()

А во Flutter?
▫️
ChangeNotifier и ChangeNotifierProvider в Provider
▫️ValueNotifier + ValueListenableBuilder
▫️StreamBuilder, который реагирует на события стрима
▫️InheritedWidget и InheritedNotifier — это тоже про наблюдение за изменениями

Какой паттерн разобрать в следующий раз?🔍
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥114👍3
Всем привет! Это Анна, Friflex Flutter Team Lead💭

В предыдущем посте мы узнали о том, что такое паттерн Observer (Наблюдатель) и как его применять в Dart-коде. Сегодня продолжим обсуждение поведенческих шаблонов проектирования и поговорим о шаблоне Mediator (Посредник).

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

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

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

А с чем сравнить посредника во Flutter-приложении? Представим, что нам надо на трех разных экранах приложения сделать отображение значения счетчика. При этом счетчик должен быть общим.

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

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

class CounterMediator {
int _counter = 0;
final List<Function(int)> _listeners = [];

/// Значение счетчика
int get counter => _counter;

/// Добавляет слушателя, который будет уведомлен при изменении счетчика
void addListener(Function(int) listener) {
_listeners.add(listener);
}

/// Изменяет значение счетчика и уведомляет всех слушателей
void increment() {
_counter++;
for (final listener in _listeners) {
listener(_counter);
}
}
}


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

class HomeScreen extends StatefulWidget {
const HomeScreen({super.key, required this.mediator});

final CounterMediator mediator;

@override
State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
int _counter = 0;

@override
void initState() {
super.initState();
// Подписываемся на изменения посредника-счетчика
widget.iss.onediator.addListener((newCounter) {
setState(() => _counter = newCounter);
});
}

@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Счетчик: $_counter'),
ElevatedButton(
onPressed: widget.iss.onediator
.increment, // Вызываем метод увеличения значения счетчика
child: const Text('+'),
),
],
);
}
}


❤️
если пост был полезен
Please open Telegram to view this post
VIEW IN TELEGRAM
18🔥2❤‍🔥1💩1
Привет, это Роза, Flutter Dev Friflex!😉

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

Какие были варианты решения: 
☠️ if-case
Просто перечислить все форматы и вызвать нужный метод:

if (format == FileFormat.arb) {
  validateArb(file);
} else if (format == FileFormat.csv) {
  validateCsv(file);
}
// ...
// И это только validate, а еще есть convert...


В целом это будет работать, но немного отдает болью и будущими проблемами в виде сложно поддерживаемого кода. 
Добавить новый формат — +1  условие. 
Протестировать — сложновато. 
Расширять — ну такое.  

Использовать паттерн проектирования
Я решила, что Стратегия — то, что нужно.

Что говорит теория?
Стратегия — поведенческий паттерн, который:
▫️ выделяет семейство алгоритмов
▫️ выносит каждый из них в отдельный класс
▫️ делает их взаимозаменяемыми

Проще говоря: если у тебя есть несколько вариантов поведения, которые могут меняться, стратегия позволяет:
▪️ вынести каждый вариант в отдельный класс
▪️ использовать их через единый интерфейс
▪️заменять поведение в рантайме без изменения основного кода

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

Вернемся к задаче
Я определила общий интерфейс FileConversionStrategy, который описывает, как работать с файлом:

abstract class FileConversionStrategy {
  bool supports(FileFormat format);
  FutureOr<ImportFileModel> convert(
    ImportFileModel file,
    List<String> editedResult,
  );
  void validate(ImportFileModel file);
}


Затем написала реализации под каждый формат. Например, ArbConversionStrategy:

class ArbConversionStrategy implements FileConversionStrategy {
  @override
  bool supports(FileFormat format) => format == FileFormat.arb;

  @override
  FutureOr<ImportFileModel> convert(
    ImportFileModel file,
    List<String> editedResult,
  ) {
    // логика конвертации
  }

  @override
  void validate(ImportFileModel file) {
    // логика валидации
  }
}


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

Но это еще не все. У нас есть реализации, но как выбрать необходимую в нужный момент?

Для этого я написала менеджер стратегий. Он перебирает доступные реализации и возвращает ту, что подходит:

class FileConversionStrategyManager {
  final List<FileConversionStrategy> strategies;

  FileConversionStrategyManager(this.strategies);

  FileConversionStrategy getStrategy(FileFormat format) {
    return strategies.firstWhere(
      (strategy) => strategy.supports(format),
      orElse: () => throw UnsupportedError('No strategy for format $format'),
    );
  }
}


А теперь разберем, как все это работает вместе:

1. Контекст — запрашивает подходящую стратегию и работает с ней через единый интерфейс
2. Интерфейс — объединяет все алгоритмы в общую структуру (validate, convert)
3. Конкретные стратегии — реализуют обработку разных форматов (CSV, ARB, JSON и других)
4. Во время выполнения мы подбираем нужную стратегию
5. Можно легко заменить стратегию на новую

Но применять паттерны ради паттернов — плохая идея. Их сила не в модности, а в том, что они решают конкретные проблемы.
Поэтому используйте стратегию:
✔️ Когда у вас разные сценарии поведения, и хочется вынести их из основного кода
✔️ Когда поведение часто меняется или дополняется
✔️ Когда хочется разделить ответственность и не мешать все в одном классе

Это уже третий паттерн. Здесь подробнее про:
🔗Observer
🔗Mediator
Please open Telegram to view this post
VIEW IN TELEGRAM
8👍6🔥4💅1
Привет, с вами снова Катя, Flutter Dev Friflex!👋

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

Что такое typedef?
Typedef
позволяет дать имя какому-то типу. Это особенно удобно, когда тип громоздкий, например, функция с дженериками, списки функций или колбэки. Сложный тип описывается один раз и дальше используется по имени — лаконично и понятно.

Пример
При работе с БД часто встречаются такие сигнатуры:

Expression<bool> Function(MyTable tbl)

List<OrderingTerm Function(MyTable tbl)>


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

// Вынесенная фильтрация
typedef FilterExpression<Q extends BaseTable> = Expression<bool> Function(Q tbl);

// Вынесенный список для сортировки
typedef OrderByExpressions<Q extends BaseTable> = List<OrderingTerm Function(Q tbl)>;


Как использовать?
Вместо длинных сигнатур теперь в универсальных функциях можно использовать понятные имена:

Future<List<T>> fetchFilteredData<T extends BaseTable>({
  required FilterExpression<T> where,
  OrderByExpressions<T>? orderBy,
}) async {}


Гораздо чище и легче воспринимается при чтении и навигации по коду.

Вывод
Typedef
— небольшой, но полезный инструмент, который помогает писать аккуратный и поддерживаемый код. Особенно полезен при работе с абстракциями, как в случае с Drift, или при проектировании универсальных репозиториев.
Please open Telegram to view this post
VIEW IN TELEGRAM
12🔥5👍4
Привет! Это Анна, Friflex Flutter Team Lead💫

Каждый Flutter-разработчик знаком с понятием состояния (state) в приложении. Сегодня мы поговорим о тех состояниях, которыми можно управлять, разберемся в классификации и рассмотрим их отличия.

Управляемые состояния Flutter-приложения можно разделить на два вида — эфемерное состояние (Ephemeral state) и состояние приложения (App state).

➡️ Эфемерное состояние еще можно назвать локальным состоянием. Это понятие очень точно описывает саму суть — такой вид состояния управляет только одним виджетом и не распространяется вне его контекста. Оно «живет» только вместе с конкретным виджетом, к которому привязано. Официальная документация в пример приводит текущую страницу PageView или выбранную вкладку BottomNavigationBar.

Самая простая реализация эфемерного состояния — использование Stateful-виджета и метода setState().

Для еще более простого понимания рассмотрим пример кастомного текстового поля с кнопкой очистки, которая должна отображаться, когда поле заполняется текстом. Создадим Stateful-виджет CustomTextField, State которого будет выглядеть следующим образом:

class _CustomTextFieldState extends State<CustomTextField> {
late final TextEditingController controller;
bool showClearButton = false;

@override
void initState() {
super.initState();
controller = TextEditingController();
controller.addListener(
() => setState(() => showClearButton = controller.text.isNotEmpty),
);
}

@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
decoration: InputDecoration(
suffixIcon: showClearButton
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
controller.clear();
setState(() {
showClearButton = false;
});
})
: null,
),
);
}

@override
void dispose() {
controller.dispose();
super.dispose();
}
}


Здесь мы видим очень простой пример эфемерного состояния. А что же такое состояние приложения?

➡️App state — это состояние, которое влияет на несколько виджетов. Может затрагивать и разные части приложения, например, разные экраны. Часто управление такими состояниями реализуется с помощью стейт менеджеров, например, Bloc или Redux.

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

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

📎Еще больше примеров и деталей можно найти в официальной документации.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥126👍4❤‍🔥1
🗣️Привет, это Роза, Flutter Dev Friflex!

Если вы хоть раз думали о том, как открывать ссылки на Flutter, то точно видели пакет url_launcher. Но знаете ли вы, что его возможности этим не ограничиваются?

С помощью этого плагина можно:
✔️ Открывать URL-ссылки во внешнем браузере или прямо внутри приложения
✔️ Запускать почтовый клиент с уже заполненным письмом
✔️ Совершать звонки прямо из приложения
✔️ Открывать мессенджеры и другие приложения с поддержкой кастомных URL-схем

url_launcher – это плагин Flutter, предназначенный для взаимодействия с внешними приложениями с помощью стандартных URL-схем:  
tel:, mailto:, https:, sms:
и других.

➡️  Совершаем звонок при помощи url_launcher

Future<void> launchCall({required String phoneNumber}) async {
  final Uri url = Uri.parse('tel:$phoneNumber');
  if (await canLaunchUrl(url)) {
    await launchUrl(url);
  } else {
    throw 'Не удалось запустить вызов на: $phoneNumber';
  }
}


➡️Отправляем e-mail

Future<void> launchEmail({required String email}) async {
  final Uri url = Uri.parse('mailto:$email');
  if (await canLaunchUrl(url)) {
    await launchUrl(url);
  } else {
    throw 'Не удалось открыть почтовый клиент для: $email';
  }
}


➡️Открываем сайт

Future<void> launchWeb({required String url}) async {
  final Uri uri = Uri.parse(url);
  if (await canLaunchUrl(uri)) {
    await launchUrl(uri, mode: LaunchMode.externalApplication);
  } else {
    throw 'Не удалось открыть ссылку: $url';
  }
}


Но при подключении пакета не забываем о настройках Info.plist (iOS) и AndroidManifest.xml (Android), иначе canLaunchUrl может возвращать false.  
Подробнее — в документации пакета.

Если вы нацелены на веб-платформу, стоит помнить:
— Ссылки можно открывать только в ответ на действие пользователя
— Все ссылки открываются во внешнем браузере
— Некоторые LaunchMode не поддерживаются и игнорируются

Основные методы пакета:
🔵 canLaunchUrl(Uri) — проверяет, может ли система обработать URL
🔵 launchUrl(Uri, {LaunchMode, ...}) — открывает указанный URL  
🔵 launchUrlString(String urlString, {LaunchMode, ...}) — альтернатива, если не хочется вручную парсить Uri
🔵 LaunchMode — перечисление, управляющее способом
🔵 platformDefault — по умолчанию (решает сама платформа)
🔵inAppWebView — открытие во встроенном WebView
🔵inAppBrowserView — встроенный браузер (Chrome Custom Tabs / Safari VC)
🔵externalApplication — открытие во внешнем приложении
🔵 externalNonBrowserApplication — открытие во внешнем не браузерном приложении

💡Но и, конечно, не забываем:
— Проверять URL через canLaunchUrl, чтобы избежать ошибок
— Использовать Uri.parse() вместо обычной строки — это безопаснее и надежнее
— Тестировать поведение на разных платформах (iOS, Android, Web) — оно может отличаться

На этом все! Наши ссылки все активны!🔗
Please open Telegram to view this post
VIEW IN TELEGRAM
👍124🔥1
This media is not supported in your browser
VIEW IN TELEGRAM
Нам очень нравится с вами — хотим пообщаться в прямом эфире

Встречаемся на онлайн-митапе с Сережей, Friflex Flutter Team Lead.

Тема: «Способы темизации и кастомизации мобильных приложений».

О чем будем говорить:
🔵Различные подходы к темизации UI: от простых до сложных
🔵Когда какой метод лучше применять
🔵Основные проблемы с которыми сталкиваются разработчики

🖥Когда: 9 июля, 16:00 (по Мск)
📍Где: здесь, в канале @flutterfriendly
Please open Telegram to view this post
VIEW IN TELEGRAM
11🔥5👍2
Live stream scheduled for
Live stream started
Live stream finished (37 minutes)
Media is too big
VIEW IN TELEGRAM
Как и договаривались — ловите запись вчерашнего митапа с Сережей, Friflex Flutter Team Lead ⬆️

Если кратко:
🔸Темизация должна быть системной — не только цвета, но и шрифты, отступы, состояния, анимации и взаимодействие между компонентами
🔸Общение с дизайнерами критично — обсуждайте реализуемость решений заранее, чтобы избежать затрат и конфликтов
🔸Разработчик — не просто исполнитель — можно и нужно предлагать улучшения, инициировать создание UI-кита, структурировать код
🔸Theme Extensions — лучший компромисс — подходят большинству проектов, позволяют гибко кастомизировать без отказа от Material Design

Что еще почитать на эту тему:
📎Flutter:
•ThemeExtensions
•MaterialApp/ Cupertino
•InheritedWidget
•Theme и ThemeData
 
📎Design:
•Material Design + Human Interface Guidelines
•Стили, токены в Figma
 
Презентация — в комментариях⬇️

Повторим такой онлайн-формат?💜
Please open Telegram to view this post
VIEW IN TELEGRAM
14🔥9👍6
💭Привет, это снова Катя, Flutter Dev Friflex!

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

▪️Горячая перезагрузка на вебе (экспериментально)
Наконец-то можно использовать hot reload в браузере! Это сильно ускоряет цикл разработки — больше не нужно каждый раз ждать полной перезагрузки страницы.

Чтобы включить, достаточно запустить:
bash
flutter run -d chrome --web-experimental-hot-reload


В VS Code можно настроить в launch.json, просто добавив аргумент --web-experimental-hot-reload. Работает так же, как и в мобильных проектах — r в терминале для быстрой перезагрузки, R для полного рестарта. А в DartPad теперь даже появилась кнопка «Reload».

▪️Улучшения доступности
Семантическое дерево теперь строится примерно на 80% быстрее — полезно для экранных дикторов и других assistive-технологий.

Появился новый API SemanticsRole, позволяющий явно указывать роли элементов (button, header, textField и другие), и это работает на уровне поддерева.

▪️Инструменты разработчика
В DevTools появился Flutter Property Editor. С ним можно редактировать свойства виджетов прямо в интерфейсе и сразу видеть изменения.
Обновили интерфейс DevTools, улучшили отслеживание памяти и работу с CPU-профайлером. Все стало быстрее и нагляднее.

▪️Новые виджеты и возможности темизации
Появился новый виджет Expansible и контроллер к нему. Он удобнее, чем старый ExpansionTile, и легко кастомизируется.

Поддержка Material 3 расширилась:
▫️Новый эффект InkSparkle
▫️Улучшенные SegmentedButton и Divider с поддержкой радиусов
▫️Новые колбэки для TabBar на hover и focus

▪️Поддержка десктопа и multi-window
Canonical помогает улучшать поведение окон: события мыши, фокус, текстовые поля теперь работают стабильнее
— На Linux отрисовка вынесена в отдельный поток
— На Windows и macOS вызовы к платформе через Dart-FFI — теперь быстрее благодаря объединенным потокам.

▪️Dart 3.8
Новые возможности языка, в том числе:
— Null-aware элементы в коллекциях (if, for внутри списков и map'ов)
— Улучшенный форматтер — теперь, например, запятая в конце не исчезает
— Поддержка кросс-компиляции: можно собирать бинарники под Linux с других платформ

Кто тоже уже обновился?👀
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥11👍52
Всем привет! С вами Анна, Friflex Flutter Team Lead💬

Сегодня поговорим об одном мощном инструменте, который обязательно вам пригодится — WidgetsBindingObserver.

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

Подключить его очень просто. Для примера создадим Stateful-виджет Example. К состоянию этого виджета нужно подключить миксин, затем в initState() необходимо обратиться к экземпляру WidgetsBinding и подключить текущий виджет как наблюдателя. При этом важно в методе dispose() отсоединять наблюдателя. Подобная реализация state-виджета Example будет выглядеть следующим образом:

class _ExampleState extends State<Example> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}

@override
Widget build(BuildContext context) {
return const Placeholder();
}

@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
}


Что же дает нам такая реализация? WidgetsBindingObserver позволяет отслеживать:

✔️Жизненный цикл приложения (активно, работает в фоне и другие параметры)

 @override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
print('Жизненный цикл приложения изменился: $state');
}


✔️ Изменения размеров приложения (например, при перевороте экрана)

 @override
void didChangeMetrics() {
super.didChangeMetrics();
final size = View.of(context).physicalSize;
print('Размер экрана изменился: ${size.width}x${size.height}');
}


✔️ Изменения темы системы

 @override
void didChangePlatformBrightness() {
super.didChangePlatformBrightness();
print('Изменилась тема системы');
}


✔️Изменения системной локализации

  @override
void didChangeLocales(List<Locale>? locales) {
super.didChangeLocales(locales);
print(
'Изменилась локализация: ${locales?.map((locale) => locale.toString()).join(', ')}');
}


✔️ Возникновения нехватки памяти в системе

@override
void didHaveMemoryPressure() {
super.didHaveMemoryPressure();
print('Обнаружена нехватка памяти!');
}


✔️Изменения системного размера шрифта

  @override
void didChangeTextScaleFactor() {
super.didChangeTextScaleFactor();
print('Изменился размер шрифта системы');
}


📎Еще больше возможностей WidgetsBindingObserver можно найти в документации

❤️ — если было полезно
Please open Telegram to view this post
VIEW IN TELEGRAM
20🔥6❤‍🔥3
Привет, это Роза, Flutter Dev Friflex👋

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

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

Для этого используем три компонента:
1️⃣ Overlay — для отрисовки поверх текущего интерфейса
2️⃣ CompositedTransformTarget — якорь для привязки
3️⃣ CompositedTransformFollower — виджет, который будет следовать за якорем

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

Чтобы связать два виджета в пространстве, используем:
✔️ CompositedTransformTarget — задает якорь
✔️ CompositedTransformFollower — следует за якорем, даже если его позиция меняется

CompositedTransformTarget(
  link: _layerLink,
  child: ..., // ваш виджет
)


CompositedTransformFollower(
  link: _layerLink, 
  offset: Offset.zero,
  targetAnchor: Alignment.bottomLeft,
  followerAnchor: Alignment.topLeft,
  child: ..., // всплывающее окно
)


Где:
🔵 link — объект LayerLink, соединяющий Target и Follower
🔵 showWhenUnlinked — показывает Follower, если связь потеряна
🔵 offset — смещение относительно якоря
🔵 targetAnchor и followerAnchor — точки привязки

Реализация
Для начала создадим StatefulWidget для управления окном. В State определим:
➡️ LayerLink — для связи кнопки и окна
➡️ OverlayEntry — элемент, который добавим в Overlay
➡️ OverlayState — текущий Overlay из контекста

    final LayerLink _layerLink = LayerLink();
OverlayEntry? _overlayEntry;
OverlayState? _overlayState;


В методе build оборачиваем кнопку в CompositedTransformTarget:

@override
Widget build(BuildContext context) {
  return CompositedTransformTarget(
    link: _layerLink,
    child: LocalizationFilledIconButton(
      icon: widget.icon ?? const FilterIcon(),
      onPressed: _toggleDialog,
    ),
  );
}


Добавим логику открытия/закрытия:

void _toggleDialog() {
  // Если оверлей ещё не создан – создаем его
  if (_overlayEntry == null) {
    _overlayEntry = _createOverlayEntry(); // Создаём OverlayEntry
    _overlayState = Overlay.of(context);   // Получаем текущее состояние Overlay
    _overlayState?.insert(_overlayEntry!); // Вставляем OverlayEntry в Overlay
  } else {
    // Если оверлей уже открыт – удаляем его
    _overlayEntry?.remove(); // Удаляем OverlayEntry из Overlay
    _overlayEntry = null;    // Обнуляем ссылку на OverlayEntry
  }
}


Создаем само окно через CompositedTransformFollower:

OverlayEntry _createOverlayEntry() {
  return OverlayEntry(
    builder: (context) {
      return GestureDetector(
        behavior: HitTestBehavior.translucent, // Позволяет "ловить" тап вне окна
        onTap: _toggleDialog, // Закрываем окно при тапе вне его области
        child: Stack(
          children: [
            Positioned(
              width: MediaQuery.sizeOf(context).width / 3, // Ширина окна – треть экрана
              child: CompositedTransformFollower(
                link: _layerLink, // Связь с CompositedTransformTarget
                showWhenUnlinked: false, // Скрываем окно, если связь потеряна
                targetAnchor: Alignment.bottomLeft, // Привязываемся к нижнему левому углу кнопки
                child: FilterDialog(toggleDialog: _toggleDialog), // Само окно фильтра
              ),
            ),
          ],
        ),
      );
    },
  );
}


Не забываем удалить OverlayEntry при закрытии:

@override
void dispose() {
  _overlayEntry?.remove();
  super.dispose();
}


Готово!
Теперь у нас есть кастомное окно с фильтрами, которое:
✔️ Открывается по нажатию на кнопку
✔️ Позиционируется рядом с ней
✔️ Закрывается при клике вне области

❤️ — если было полезно
Please open Telegram to view this post
VIEW IN TELEGRAM
12👍7🔥4
Привет, это снова Катя, Flutter Dev Friflex!💫

Иногда одной только документации (README) на pub.dev недостаточно — особенно, когда библиотека ведет себя странно или хочется понять, как она работает «под капотом». В такие моменты приходится читать исходный код библиотеки. Давайте разберем, куда смотреть и на что можно не тратить время, на примере популярной библиотеки intl_utils.

Шаг 1. Открываем репозиторий

1️⃣
Идем на pub.dev
2️⃣В поиске вводим нужную библиотеку, например, intl_utils
3️⃣В карточке справа жмем Repository (GitHub) — нас перебросит на GitHub-репозиторий проекта

Шаг 2. Что стоит смотреть

▪️Папка bin/
Часто используется для CLI-скриптов. В intl_utils, например, есть исполняемый файл генератора локализаций. Он как раз запускается при выполнении команды dart run intl_utils:generate

▪️Папка lib/
Это сердце библиотеки. Тут обычно:
✔️логика импорта (intl_utils.dart)
✔️основной код библиотеки
✔️вспомогательные утилиты

▪️Файл генератора
Если есть генерация кода, как у intl_utils, стоит посмотреть, как он парсит pubspec.yaml, обрабатывает ключи и какие шаблоны использует. В intl_utils, например:
✔️generator.dart отвечает за запуск логики
✔️pubspec_config.dart — за чтение конфигурации
✔️templates/ — за шаблоны, по которым создаются dart-файлы с переводам

Иногда полезно заглянуть в build.yaml — он описывает, как работает генерация с build_runner.

Шаг 3. Что можно пропустить

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

Конфигурационные файлы
gitignore, analysis_options.yaml, metadata, vscode/ и прочие технические файлы не помогают в понимании логики работы.

Советы

Начинайте с точки входа — файла, указанного в pubspec.yaml → executables: или lib/
Ищите ключевые слова: generate, parse, template, config. Они помогут быстрее найти нужную часть
Если запутались — зайдите в example/, если он есть. Там обычно видно, как библиотеку используют в реальном коде.

Часто ли вы читаете сторонние библиотеки?
Please open Telegram to view this post
VIEW IN TELEGRAM
9🔥6👍3
👋Всем привет! С вами Анна, Friflex Flutter Team Lead.

Однажды Роза в своем посте уже делилась лайфхаками, как сделать скролящиеся списки красивыми, плавными и высокопроизводительными. Там же упоминались три основных виджета для реализации списков с прокруткой — SingleChildScrollView, ListView и CustomScrollView. Сегодня чуть глубже погрузимся в специфику работы каждого из них.

✔️SingleChildScrollView — самый простой виджет для прокрутки. Принимает в качестве «ребенка» только один виджет, поэтому при необходимости вложить несколько объектов необходимо обернуть их в Column.

SingleChildScrollView(
child: Column(
children: [
Child1()
Child2()
Child3()
],
),
)


Плюсы:
▪️максимально прост в использовании
▪️ отлично подходит для отрисовки статичных объектов

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

✔️ListView — наиболее часто используемый виджет для скроллящихся списков. Имеет несколько конструкторов builder, separated и custom, каждый из которых в разных ситуациях может помочь максимально сократить количество кода.

ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return Child();
},
)

ListView(
children: [
Child1(),
Child2(),
],
)


Плюсы:
▪️ поддерживает высокую производительность
▪️ работает по принципу ленивой загрузки (вложенные виджеты билдятся не одновременно, а по мере прокрутки)

Минусы:
позволяет вложить простой контент
не дает сильно кастомизировать прокрутку

✔️CustomScrollView — идеальное решение для сложных интерфейсов. Работает не на стандартных виджетах, а на сливерах, что без проблем позволяет вкладывать разнообразный контент.

CustomScrollView(
slivers: [
SliverAppBar(
title: Child1(),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => Child2(),
childCount: 20,
),
),
SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
delegate: SliverChildBuilderDelegate(
(context, index) => Child3(),
childCount: 10,
),
),
],
)


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

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

Каждый из вариантов имеет место быть. Например, для простого короткого списка статичных контейнеров нет смысла создавать CustomScrollView, так как с этой целью прекрасно справится SingleChildScrollView. А при необходимости добавить на экран несколько сеток и вложенных списков со сложной анимацией оптимальным будет именно CustomScrollView. Здесь важно понимать различия и с умом подходить к выбору инструмента.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥86💯3👍2