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

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

Представь: ты создаешь Flutter-приложение и хочешь сделать переход между экранами эффектным. Что же выбрать?

💡Конечно, Hero

Как и положено настоящему герою, Hero анимирует элементы при смене экранов, плавно изменяя их размер и положение.

Что умеет Hero?
Улучшать восприятие интерфейса  
Сохранять контекст перехода  
Создавать красивые анимации

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

Как это работает?
Hero связывает элементы с помощью уникального тега и автоматически анимирует их переход.

Давайте разберемся на примере:

📌Первый экран

Оборачиваем изображение в Hero и задаем тег:

GestureDetector(
  onTap: () {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => SecondScreen(imageUrl: ‘some_image_url’),
      ),
    );
  },
  child: Hero(
    tag: 'image',
    child: Image.network(image, height: 200),
  ),
);


📌Второй экран

Используем тот же тег:

Hero(
  tag: 'image',
  Child: Image.network(imageUrl, width: 300),
);


Hero автоматически выполняет анимацию — никаких сложных настроек.

Параметры Hero:
tag
— уникальный идентификатор элемента
child
— сам анимируемый виджет (изображение, кнопка и другие элементы)
flightShuttleBuilder
— позволяет кастомизировать анимацию
placeholderBuilder
— задает виджет-заполнитель до завершения анимации
transitionOnUserGestures
— разрешает запуск анимации по жесту пользователя

Где еще можно использовать Hero?
🔴В анимации текста  
🔴Для перемещения кнопок  
🔴Чтобы создавать эффектные переходы между карточками

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

👍 Лайфхак: можно настроить анимацию вручную через heroFlightShuttleBuilder или управлять временем анимации с помощью HeroController.

Теперь Hero не только в твоем коде, но и в тебе — ты создал крутой переход!
Please open Telegram to view this post
VIEW IN TELEGRAM
👍128🔥6🍾3
Привет, с вами вновь Катя, 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