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

🔗 Наш канал для разработчиков: @friflex_dev
🔗 Канал о продуктовой разработке: @friflex_product
Download Telegram
Привет, с вами вновь Катя, Flutter Dev Friflex.

Сегодня я расскажу, как работать с Generics.

🔖Что это?
Generics позволяют создавать классы, методы и интерфейсы, которые могут работать с различными типами данных. Они позволяют определять параметры типа, которые можно заменить конкретными типами во время выполнения. Это особенно полезно для создания универсальных структур данных или методов, которые могут работать с различными типами, сохраняя при этом строгую типизацию.

🔖Как это работает?
Рассмотрим пример, где мы создаем класс, который будет работать с моделью Model. Мы определим класс Repository, который будет использовать дженерики для работы с различными типами моделей.

Создадим базовую модельку с параметром name.

class Model {
  Model(this.name);
  final String name;
}


Определим класс Repository, который принимает параметр типа T. Условие extends Model гарантирует, что T будет подтипом Model. Создадим два метода: один — для добавления элементов в список, а другой — для их получения.

class Repository<T extends Model> {
  final List<T> items = [];

  void addItem(T item) => items.add(item);

  List<T> getItems() => items;
}


Теперь добавим модели User и Product, которые будут наследоваться от базовой модели. У каждой модели есть свои свойства id и price. 

class User extends Model {
  User(this.id, String name) : super(name);
  
  final int id;
}

class Product extends Model {
  Product(String name, this.price) : super(name);
  
  final double price;
}


Теперь перейдем к реализации и посмотрим, как с этим работать. В примере я создала два репозитория: один — для управления пользователями, а другой — для управления продуктами.

void main() {
  // Репозиторий для пользователей
  final userRepository = Repository<User>();
  userRepository.addItem(User(1, 'Alice'));
  userRepository.addItem(User(2, 'Bob'));

  // Выводим информацию о пользователях
  for (var user in userRepository.getItems()) {
    print('User: ${user.name}, ID: ${user.id}');
  }

  // Репозиторий для продуктов
  final productRepository = Repository<Product>();
  productRepository.addItem(Product('Laptop', 999.99));
  productRepository.addItem(Product('Smartphone', 499.99));

  // Выводим информацию о продуктах
  for (var product in productRepository.getItems()) {
    print('Product: ${product.name}, Price: \$${product.price}');
  }
}


После запуска приложения выводятся следующие данные: 

консоль
User: Alice, ID: 1
User: Bob, ID: 2

Product: Laptop, Price: $999.99
Product: Smartphone, Price: $499.99


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

🔖Преимущества использования

🔴Типобезопасность: Generics обеспечивают строгую типизацию, что позволяет избежать ошибок времени выполнения
🔴Повторное использование кода: вы можете создавать универсальные классы и методы, которые можно использовать с различными типами.
🔴Читаемость кода: код становится более понятным и структурированным.

Теперь ты освоил суперсилу гибкости! Главное — применять ее, иначе это будет как шпагат, который уже не такой уж и поперечный🤪
Please open Telegram to view this post
VIEW IN TELEGRAM
9🔥7👍62
Привет, это Анна, Friflex Flutter Team Lead.

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

Вам нужна всего одна библиотека — dartx.

Пакет дает доступ к дополнительным расширениям классов String, int, num, Iterable, DateTime, File и других, которые используются буквально в каждом проекте. Разберем самые интересные функции.

🔸String
1️⃣ capitalize() и decapitalize() — изменят регистр только первой буквы предложения
2️⃣ isNotNullOrBlank и isNullOrBlank — проверят, есть ли читаемое содержимое, при этом не учитывая специальные символы по типу \n и пробелов
3️⃣ urlEncode и urlDecode — в строке преобразуют ссылку в формат application/x-www-form-urlencoded или обратно

final capitalizedText = 'пример заглавной буквы'.capitalize(); // Пример заглавной буквы
final isBlank = ' \n'.isNullOrBlank; // true
final decodedText = 'Пример%20декодирования'.urlDecode; // Пример декодирования


🔸Iterable
1️⃣ sortedBy() и thenBy() — позволят вам выполнить сортировку по нескольким признакам
2️⃣ chunkWhile() и splitWhen() — объединит в подсписки при выполнении или невыполнении условия

final dogs = [
Dog(name: "Charlie", age: 1),
Dog(name: "Bark", age: 3),
Dog(name: "Charlie", age: 6),
];
final sorted = dogs.sortedBy((dog) => dog.name).thenByDescending((dog) => dog.age); // Bark, Charlie (6), Charlie (3)
final chunckedList =[1, 2, 4, 9, 10, 11].chunkWhile((a, b) => a + 1 == b); // [[1, 2], [4], [9, 10, 11]]


🔸DateTime/Duration (эти функции можно подключить отдельно через пакет time)
1️⃣isAtSameYearAs(date) — проверит, находится ли текущая дата в рамках одного и того же года даты date в параметре (есть аналогичные проверки по месяцу и дню)
2️⃣ minutes.fromNow и minutes.ago — высчитает DateTime по указанной длительности в будущем и прошлом
3️⃣ hours — создаст Duration объект из целого числа

final tenMinutes = 10.minutes; // Duration(minutes: 10)
final isAtSameYear = DateTime(2025, 01, 01).isAtSameYearAs(DateTime(2020, 10, 05)); // false
final timeInFuture = 5.minutes.fromNow; // DatiTime.now() + 5 минут


Это только малая часть того, что умеет dartx!

❤️ — если уже пользуетесь пакетом
🔥 — если только сейчас открыли для себя его чудесные функции
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥1566🥰1
This media is not supported in your browser
VIEW IN TELEGRAM
Всем привет! Это Роза, Flutter Dev в Friflex! 👋

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

И я нашла решение!

Дело в том, что во время поиска каждое мое нажатие инициировало отправку события на бэкенд и изменение состояния виджетов, тем самым перегружая UI. Чтобы этого избежать, я использовала Debounce.

Что такое Debounce?

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

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

Как это работает?

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

 Как использовать Debounce в коде?

Достаточно создать объект Debouncer и указать время задержки:

final _debouncer = Debouncer(milliseconds: 500);


А еще лучше — создать отдельный класс для управления Debounce:

import 'dart:async';

class Debouncer {
  final int milliseconds;
  Timer? _timer;

  Debouncer({required this.milliseconds});

  void run(VoidCallback action) {
    _timer?.cancel(); // Отменяем предыдущий таймер
    _timer = Timer(Duration(milliseconds: milliseconds), action); // Запускаем новый
  }

  void dispose() {
    _timer?.cancel(); // Очищаем ресурсы
  }
}


Каждый раз при вызове run()  действие выполняется не сразу, а только спустя указанное время.

Где использовать Debounce?

▶️ В поиске, чтобы не отправлять запрос на сервер при каждом символе  
▶️ В кнопках, чтобы избежать дублирующих API-запросов  
▶️ В валидации форм, чтобы не проверять ввод на каждом символе  
▶️ В анимации и UI, чтобы сглаживать обновления интерфейса

Почему это важно?

🔴Меньше запросов — API загружается только тогда, когда это действительно нужно  
🔴Более плавный UI — интерфейс не дергается при быстром вводе  
🔴Оптимизированная работа приложения — снижается нагрузка на процессор  
🔴Экономия трафика — меньше ненужных запросов к серверу

📌Важно: не забывай освобождать ресурсы и вызывать dispose() при уничтожении Debounce.

Согласитесь, иногда Debounce не хватает и в жизни💜
Please open Telegram to view this post
VIEW IN TELEGRAM
👍19🔥94
Рисуем как Пикассо, только на Flutter

Привет, это Катя, Flutter Dev Friflex. Flutter предоставляет мощные инструменты для работы с графикой, один из которых — CustomPainter.

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

Основные принципы работы CustomPainter
CustomPainter работает в связке с CustomPaint, который отвечает за рендеринг на экране. 

CustomPainter переопределяет два метода:
🔴paint(Canvas canvas, Size size): содержит код отрисовки на canvas.
🔴shouldRepaint(CustomPainter oldDelegate): указывает, нужно ли перерисовывать объект при изменении состояния

Создание простого CustomPainter
Рассмотрим, как нарисовать круг с градиентной заливкой:
1️⃣ Наследуемся от CustomPainter, что позволяет переопределить метод paint, в котором выполняется отрисовка
2️⃣ Внутри метода paint создаем Paint — кисть для рисования
3️⃣ Используем shader для градиентной заливки — это задает радиальный градиент (от центра к краям), который переходит от синего к фиолетовому цвету
4️⃣ С canvasdrawCircle рисуем круг в центре с радиусом, равным половине ширины

import 'package:flutter/material.dart';

class GradientCirclePainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..shader = RadialGradient(
        colors: [Colors.blue, Colors.purple],
      ).createShader(Rect.fromCircle(
        center: Offset(size.width / 2, size.height / 2),
        radius: size.width / 2,
      ));

    canvas.drawCircle(
      Offset(size.width / 2, size.height / 2),
      size.width / 2,
      paint,
    );
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;
}


Теперь используем CustomPaint, чтобы отобразить рисунок:

class GradientCircleWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      size: Size(200, 200),
      painter: GradientCirclePainter(),
    );
  }
}


Улучшение производительности

Чтобы избежать ненужных перерисовок, важно:
🔸 Указывать shouldRepaint как false, если рисование не меняется
🔸 Использовать RepaintBoundary, чтобы ограничить область перерисовки

RepaintBoundary(
  child: CustomPaint(
    size: Size(200, 200),
    painter: GradientCirclePainter(),
  ),
)


CustomPainter открывает широкие возможности для создания сложных графических элементов в Flutter. Он полезен для кастомных UI-решений, диаграмм, анимаций и визуализаций. Используйте его, когда стандартные виджеты не дают нужного результата.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥1263👌2🏆1
Привет! С вами снова Анна, Friflex Flutter Team Lead.

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

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

Проще говоря, в императивном подходе маршруты собираются в единый стек. Для примера их можно представить стопкой тарелок. Когда роут добавляется, он складывается сверху (push). Когда вызывается возврат назад — самый верхний удаляется (pop).

Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondPage()),
);
Navigator.pop(context);


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

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

Для лучшего понимания различий можно выделить вопросы для каждого из подходов:
🔴императивный — как выполнить переход и какие методы нужно вызвать для этого?
🔴декларативный — что нужно показать и какое текущее состояние навигации?

А вы какой подход чаще всего используете в своих проектах?
Please open Telegram to view this post
VIEW IN TELEGRAM
10🔥6👍3❤‍🔥1
Привет, с вами Роза, Flutter Dev Friflex👋 И сегодня мы немного погрузимся в магию FutureOr!

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

Лучше, если вы объявите метод, как FutureOr. FutureOr<T> — это такой хитрый тип в Dart, который говорит: «Эй, результат моего метода может быть либо обычным значением типа T, либо Future<T>, если вдруг придется подождать».

Звучит пока не очень понятно? Давайте разберемся на примерах.

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

import 'dart:async';

abstract class SomeService {
  FutureOr<String> fetch();
}

class FirstImplService extends SomeService {
  @override
  Future<String> fetch() async {
    await Future.delayed(Duration(seconds: 2));
    return 'Данные из Future';
  }
}

class SecondImplService extends SomeService {
  @override
  String fetch() {
    return 'Простые данные';
  }
}


Aбстрактный класс SomeService объявляет метод fetch() с типом возвращаемого значения FutureOr<String>. Это значит, что fetch() может вернуть либо String, либо Future<String>.

⚙️Когда же использовать FutureOr?
FutureOr — ваш спаситель, когда вам нужно абстрагироваться от того, является ли результат операции асинхронным или синхронным.

🔧Как обрабатывать FutureOr?
Самый простой способ — использовать проверку типа с помощью is Future.

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

У меня с работой так же. Иногда мне нужен await, чтобы подумать, а иногда все складывается супер. А у вас?
Please open Telegram to view this post
VIEW IN TELEGRAM
👍13🔥1241👎1🙏1
Привет, с вами вновь Катя, Flutter Dev Friflex. Сегодня поговорим об extension.

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

Синатксис: основные правила

🔴Имя Extension — имя расширения (необязательно, но рекомендуется для читаемости)
🔴Тип — существующий тип, который расширяется (String, List, int и другие)

extension ИмяExtension on Тип {
  // Методы, геттеры, сеттеры
}


Пример без Extension
В этом случае мы создаем отдельную функцию для изменения строки:

String capitalize(String text) {
  if (text.isEmpty) return text;
  return text[0].toUpperCase() + text.substring(1);
}

void main() {
  print(capitalize('flutter'));  // Flutter
}


Минусы:
🔴Неинтуитивный вызов (capitalize(text)), несвойственный String
🔴Нужно передавать строку в функцию, что делает код менее читаемым
🔴Усложнение автокомплита в IDE, так как методы не привязаны к типу

С использованием Extension
Здесь метод capitalize становится частью String:

extension StringExtension on String {
  String capitalize() {
    if (isEmpty) return this;
    return this[0].toUpperCase() + substring(1);
  }
}

void main() {
  print('flutter'.capitalize());  // Flutter
}


Преимущества:
🔴Код становится лаконичным: text.capitalize() вместо capitalize(text)
🔴Лучшая читаемость и автодополнение
🔴Логика метода инкапсулирована в extension, а не в отдельной функции

Когда использовать Extension?
➡️Для расширения стандартных типов — когда нужно добавить удобные методы к String, List, DateTime и другим встроенным классам.

➡️Для инкапсуляции вспомогательной логики. Если часто используемая функция относится к конкретному типу, лучше оформить ее как метод через extension.

➡️Для упрощения работы с объектами — позволяет обращаться к данным через удобные геттеры или методы, избегая лишнего кода.

Ограничения Extension
🔸Нельзя добавлять новые поля в класс
🔸Нельзя переопределить существующие методы
🔸Расширения не наследуются, то есть нельзя создать extends для другого extension
🔸Конфликты: если два расширения имеют одинаковый метод, нужно явно указывать, какое расширение использовать

📎Если кратко: используйте расширения для инкапсуляции часто используемых методов и упрощения работы с базовыми типами в вашем проекте.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥11👍8🍓43👻1
Всем привет! Это Анна, Friflex Flutter Team Lead.

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

🔗Глубокие ссылки (deeplinks) — это ссылки с кастомной схемой, которые позволяют не только открыть именно ваше приложение на устройстве, но и осуществить переход на вложенные маршруты внутри него. Например, ссылка app://product/id123 позволит открыть ваше приложение сразу на странице продукта с идентификатором id123.

🔗Universal links — это универсальные ссылки iOS приложений, которые имеют формат стандартной веб-ссылки, например, https://www.example.com. В случае, если приложение установлено, ссылки открывают его. Если нет — в браузере открывается веб-сайт, который связан с этой же ссылкой.

🔗App Links — это ссылки для Android, которые работают по принципу, идентичному Universial links на iOS.

После настройки ссылок на проектах часто возникает вопрос — как же выполнить их обработку внутри приложения? Здесь на помощь нам придет библиотека app_links.

Для получения ссылок используется экземпляр класса AppLinks:

final _instance = AppLinks();


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

Future<void> handleInitialLink() async {
final initialLink = await _instance.getInitialLink();
// обработка начальной ссылки
}


Кроме этого, в момент работы приложения в него из платформы также могут поступать различные ссылки. В таком случае плагин дает возможность получать эти ссылки потоком строк или объектов Uri.

final uriSubscription = _instance.uriLinkStream.listen((uri) {
// обработка ссылки в Uri формате
});

final srtringLinksSubscription = _instance.stringLinkStream.listen((stringLink) {
// обработка ссылки в String формате
});


У библиотеки хорошая репутация: почти 1 тысяча лайков и более 800 тысяч скачиваний.

Делитесь в комментариях своим опытом работы с app_links и с ссылками приложения в целом.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍97🔥5🍓3
This media is not supported in your browser
VIEW IN TELEGRAM
Привет, с вами Роза, Flutter Dev Friflex👋

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

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

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

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

Чтобы лучше понять, как работают уровни логирования, представьте их как пирамиду, где каждый уровень отвечает за определенную степень важности:
🔸DEBUG — детализированные данные: переменные, выполнение функций, отладочные сообщения.
🔸INFO — ключевые этапы работы приложения.
🔸WARNING — потенциальные проблемы, которые пока не приводят к сбоям, но требуют внимания.
🔸ERROR — ошибки, влияющие на работу приложения.
🔸CRITICAL (или FATAL) — критические сбои, после которых приложение не может продолжать работу.

Чем ниже уровень, тем больше информации он содержит.
Чем выше, тем критичнее события.

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

Как логировать в Dart/Flutter?
Вы можете воспользоваться инструментами, представленными в самом Dart, написать собственный логгер или же использовать готовые решения.

1️⃣ Простейший способ — print() или debugPrint()

2️⃣ Использование dart:developer

3️⃣ Готовые библиотеки:
Logging — официальная библиотека от Dart. Простая, но мощная
Logger — удобный форматированный вывод, цветовые индикации, фильтрация
Talker — гибкая настройка логов, отличная поддержка Flutter

➡️Если вам нужно просто быстро вывести сообщение — print.
➡️Если хотите базовый контроль уровней логов — dart:developer.log
➡️Для продвинутого логирования лучше использовать logging, logger или talker

Оптимизация логирования: ленивые вычисления
Логирование может быть дорогостоящей операцией, особенно если записывать сложные данные. Чтобы избежать лишних вычислений, можно использовать ленивую инициализацию (closure).

logger.i(() => 'State: \$state');


Так выражение выполнится, только если лог действительно нужен.

Еще немного об инструментах логирования — в табличке в комментариях.

❤️ — обсудим в следующем посте удаленные хранилища логов, безопасное логирование и другие аспекты.
Please open Telegram to view this post
VIEW IN TELEGRAM
10🔥7❤‍🔥5👍1
Привет, это Катя, Flutter Dev Friflex. Сегодня поговорим о фреймворке gRPC и его реализации.

Что это?
gRPC — это фреймворк для удаленного вызова процедур (RPC), разработанный Google. Он использует HTTP/2 для транспорта и Protocol Buffers (protobuf) в качестве языка описания интерфейсов и формата сериализации данных.

Настройка и использование gRPC

1️⃣ Добавляем в pubspec.yaml:

dependencies:
  grpc: ^4.1.0
  protobuf: ^3.1.0

dev_dependencies:
  protoc_plugin: ^21.1.2


2️⃣ Создаем файл .proto
Сделаем на примере создания чата. Определяем сервис в файле .proto. Например, lib/protos/chat.proto:

syntax = "proto3";

package chat;

service ChatService {
  rpc SendMessage (Message) returns (MessageResponse);
  rpc ReceiveMessages (Empty) returns (stream Message);
}

message Message {
  string text = 1;
  string sender = 2;
  int64 timestamp = 3;
}

message MessageResponse {
  bool success = 1;
  string error = 2;
}


3️⃣ Генерируем код
Запускаем команду для генерации Dart-кода:

bash
protoc --dart_out=grpc:lib/generated -Ilib/protos lib/protos/chat.proto


4️⃣ Создаем клиента

class GrpcClient {
  late ChatServiceClient client;

  /// Инициализация канала соединения
  GrpcClient() {
    final channel = ClientChannel(
      'https://localhost',
      port: 50051,
      options: const ChannelOptions(
        credentials: ChannelCredentials.insecure(),
      ),
    );
    client = ChatServiceClient(channel);
  }

  /// Отправка сообщения
  Future<MessageResponse> sendMessage(String text, String sender) async {
    final message = Message()
      ..text = text
      ..sender = sender
      ..timestamp = DateTime.now().millisecondsSinceEpoch;
    
    try {
      return await client.sendMessage(message);
    } catch (e) {
      print('Error sending message: $e');
      return MessageResponse()..success = false..error = e.toString();
    }
  }
  
  /// Получение сообщений
  Stream<Message> receiveMessages() {
    return client.receiveMessages(Empty());
  }
}


5️⃣ Используем клиента в приложении

На что следует обратить внимание:
🔸gRPC клиент:
➡️инициализируется один раз при создании состояния
➡️обеспечивает двустороннюю коммуникацию
➡️управляет подпиской на поток сообщений

🔸Потоковая передача:
➡️receiveMessages() возвращает Stream<Message>
➡️listen() подписывается на новые сообщения

🔸Управление ресурсами:
➡️gRPC соединение должно закрываться
➡️Отмена подписок происходит автоматически при dispose()

Продолжение — в комментариях📌
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥1075💯1
This media is not supported in your browser
VIEW IN TELEGRAM
🔥С пылу с жару — несем вам первый выпуск нашего Flutter-дайджеста. Что интересного произошло в мире любимого фреймворка, читать здесь.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥105👌2
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 👩‍💼