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

🔗 Наш канал для разработчиков: @friflex_dev
🔗 Канал о продуктовой разработке: @friflex_product
Download Telegram
Рисуем как Пикассо, только на 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 👩‍💼
Привет, это Катя, Flutter Dev Friflex. Сейчас расскажу про три решения: Bloc, Riverpod и yx_scope, и еще немного про альтернативные подходы.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

flutter create new_app

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

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

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

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

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

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

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

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

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

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

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

flutter pub run flutter_flavorizr


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

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

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

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

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

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

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

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

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

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

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

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

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

some_package/   
  extension/   
  lib/   
  ...


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

extension/
  devtools/
    build/
    config.yaml


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

some_app/   

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


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

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

🔖Теперь вы знаете еще больше о создании расширений DevTools! В следующий раз я расскажу, как можно взаимодействовать со сторонним кодом с помощью Eval — до встречи.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍85🔥1