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

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

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

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

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

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

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

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


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


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

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

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


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

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


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

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


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

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


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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

Советы

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

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

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

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

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


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

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

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

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

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


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

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

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

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


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

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

Каждый из вариантов имеет место быть. Например, для простого короткого списка статичных контейнеров нет смысла создавать CustomScrollView, так как с этой целью прекрасно справится SingleChildScrollView. А при необходимости добавить на экран несколько сеток и вложенных списков со сложной анимацией оптимальным будет именно CustomScrollView. Здесь важно понимать различия и с умом подходить к выбору инструмента.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥86💯3👍2
This media is not supported in your browser
VIEW IN TELEGRAM
Привет, это Роза, Flutter Dev Friflex!🚀

Сегодня немного о том, как улучшить UI-опыт в ваших Flutter web-приложениях.

Порой даже в самом продуманном интерфейсе нужны подсказки.  
Допустим, у вас есть кнопка: иконка вроде есть, но… что она делает? Чтобы закрыть такие «дыры» в UX во Flutter есть такой виджет как Tooltip. Вроде мелочь, но при помощи одной лишь строчки вы заметно улучшите опыт пользователя.

Tooltip — это всплывающая подсказка, которая появляется:
✔️ при долгом тапе на мобильных устройствах
✔️при наведении курсора в вебе и на десктопе

Например, это может быть  полезно:
▫️ для иконок без текста
▫️ для кнопок с непонятным назначением

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

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

Базовое использование:

Tooltip(
  message: 'Добавить',
  child: Icon(Icons.add),
)


Но на этом не все, вы также можете его кастомизировать: задать фон, стили, задержки и даже использовать rich-контент.

    Tooltip(
  decoration: BoxDecoration(
    color: Theme.of(context).colorScheme.surface,
  ),
  richMessage: WidgetSpan(
    alignment: PlaceholderAlignment.baseline,
    baseline: TextBaseline.alphabetic,
    child: Padding(
      padding: const EdgeInsets.all(4.0),
      child: ConstrainedBox(
        constraints: BoxConstraints(maxWidth: maxWidth),
        child: Text(
          message,
          style: Theme.of(context).textTheme.bodySmall,
        ),
      ),
    ),
  ),
  child: const InfoIcon(isOutline: true, size: 20),
);



Некоторые виджеты уже встроенно поддерживают тултипы. Например, IconButton.

IconButton(
  icon: Icon(Icons.search),
  tooltip: 'Поиск',
  onPressed: () {},
)


При использовании tooltip не стоит перебарщивать. Добавляйте подсказки только в те места, где это действительно необходимо! Не надо дублировать то, что и так ясно.

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

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

Сегодня я расскажу о том, как Git помогает в повседневной работе с Flutter-проектами, какие встроенные инструменты есть в Android Studio и VS Code для работы с ним и как они делают жизнь разработчика чуть проще.

Зачем разработчикам нужен Git?
Все проекты — это, как правило, разные приложения, где часто работают несколько разработчиков, дизайнеров и тестировщиков. Git помогает:

✔️Хранить историю изменений проекта
✔️Безопасно тестировать фичи в отдельных ветках
✔️Устранять баги, не трогая основную логику
✔️Организовать code review- и pull request-процессы

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

Прописываете типовой флоу Git в Flutter-проекте:

git checkout -b feature/new-cool-widget
# Работаешь, коммитишь
git add .
git commit -m "Добавил новый виджет для экрана авторизации"
git push origin feature/new-cool-widget


Затем открываете pull request в GitHub или GitLab, проходите ревью, мержите — и вуаля!

Git + Android Studio
Android Studio (или IntelliJ) предоставляет шикарный визуальный интерфейс для работы с Git:

Встроенные фичи:
▫️Commit panel: показывает изменения, можно выбрать, что включить в коммит
▫️Version control tab: история изменений, лог коммитов, сравнение файлов
▫️Interactive rebase, cherry-pick и squash — всё доступно из GUI
▫️Branch manager: быстрое переключение, создание и удаление веток

Лайфхак: ⌘ + K (macOS) или Ctrl + K (Windows/Linux) — быстрое окно коммита


Git + VS Code
VS Code тоже хорош для Git, особенно если вы фанат клавиатуры и расширений.

Основные фичи:
▫️Панель Source Control (обычно слева)
▫️Быстрое создание веток, коммитов, пушей и пулов
▫️Интеграция с GitHub: можно прямо в редакторе открывать pull requests

Полезные расширения:
▫️GitLens — суперподробная история изменений
▫️Git Graph — визуализация веток
▫️GitHub Pull Requests — ревью прямо из редактора

Лайфхак: Cmd + Shift + P → Git: Commit — и вуаля, можно коммитить без мышки


Git ignore в Flutter

gitignore
# Flutter
.dart_tool/
.packages
.pub/
build/
ios/Pods/
android/.gradle/


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

Советы из практики
▪️
Названия веток лучше делать по шаблону: feature/, bugfix/, hotfix/, refactor/
▪️Используйте rebase осторожно, а лучше не используйте, если знаете, о чем я
▪️Часто пушьте — особенно в командной работе
▪️Никогда не коммитьте pubspec.lock в библиотеку, но обязательно — в приложение

Используйте встроенные возможности среды разработки, а не пишите все вручную👍
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥105👍4🤔1
This media is not supported in your browser
VIEW IN TELEGRAM
Привет! Это Анна, Friflex Flutter Team Lead👋

Наверняка, самым частоиспользуемым Flutter-разработчиками методом можно назвать метод build() в Stateful- и Stateless-виджетах. Сегодня поделюсь с вами своим топ-5 лучших практик для его оптимизации.

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

class Example extends StatelessWidget {
const Example({super.key});

@override
Widget build(BuildContext context) {
return Column(
children: [
const Text('Example text'), // оптимально
Text('Example text not const'), // можно оптимизировать, добавив const
],
);
}
}


2. Кэшировать обращения к MediaQuery.of(context) и Theme.of(context)
Каждое обращение у этих классов к методам of(context) под капотом подразумевает вызов dependOnInheritedWidgetOfExactType(). Этот метод производит поиск необходимых данных по всему дереву виджетов. Такая операция занимает некоторое время. Если таких операций было запущено несколько, то время увеличивается.

Кроме этого, если от MediaQuery вам необходим, например, только размер экрана, лучше вместо стандартного of(context) использовать sizeOf(context).

/// Пример неоптимизированного использования
class NotOptimizedExample extends StatelessWidget {
const NotOptimizedExample({super.key});

@override
Widget build(BuildContext context) {
return Container(
width: MediaQuery.of(context).size.width * 0.8,
height: MediaQuery.of(context).size.height * 0.3,
margin: EdgeInsets.all(MediaQuery.of(context).size.width * 0.05),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
border: Border.all(color: Theme.of(context).dividerColor),
),
child: const Text('Hello'),
);
}
}

/// Пример оптимизированного использования
class OptimizedExample extends StatelessWidget {
const OptimizedExample({super.key});

@override
Widget build(BuildContext context) {
final sizeData = MediaQuery.sizeOf(context);
final themeData = Theme.of(context);

return Container(
width: sizeData.width * 0.8,
height: sizeData.height * 0.3,
margin: EdgeInsets.all(sizeData.width * 0.05),
decoration: BoxDecoration(
color: themeData.primaryColor,
border: Border.all(color: themeData.dividerColor),
),
child: const Text('Hello'),
);
}
}


3. Вызывать перестроение только тех участков, которые действительно должны измениться.
Здесь для примера рассмотрим Stateful-виджет NotOptimizedExample, State которого выглядит таким образом:

class _NotOptimizedExampleState extends State<NotOptimizedExample> {
int counter = 0;

@override
Widget build(BuildContext context) {
return Column(
children: [
PageHeader(), // Пересоздается
CounterView(),
CounterButton(
onTap: () => setState(() {
counter++;
}),
),
PageFooter(), // Пересоздается
],
);
}
}

В этом случае при тапе на кнопку нам нужно обновить только CounterView. Но подобной реализацией мы заставляем перерисовать все содержимое экрана.
Оптимальное решение здесь — ребилдить только CounterView.

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

Пятый способ — в комментариях👇
Please open Telegram to view this post
VIEW IN TELEGRAM
13🔥6❤‍🔥3
Привет, это Роза, Flutter Dev Friflex⭐️

Если вы пишете тесты во Flutter, то наверняка сталкивались с тем, как сложно тестировать асинхронные операции. Часто такие операции приходится мокать или обходить с помощью Future.delayed. Но что делать, если в коде используются не только отложенные Future, но и настоящие таймеры, задержки и другие механизмы, зависящие от времени?

Сегодня поговорим о пакете fake_async, который может помочь с этим делом.

Пакет fake_async создан для тестирования кода, зависящего от времени. Flutter-тесты могут запускаться в специальной зоне FakeAsync, где вы управляете временем вручную. То есть  fake_async перехватывает все стандартные Dart-таймеры и заменяет их на свои «поддельные». Благодаря этому вы можете контролировать время в тестах, не дожидаясь реальных секунд или минут.

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

Future<String> fetchData() {
  return Future.delayed(Duration(seconds: 2), () => 'Data loaded');
}


Без fake_async тестирование этой функции потребовало бы реальной 2-секундной задержки.

Попробуем заменить на fake_async.

import 'package:fake_async/fake_async.dart';
import 'package:test/test.dart';

void main() {
  test('fetchData должен вернуть "Data loaded" через 2 секунды', () {
    fakeAsync((async) { // 1. Оборачиваем тест в fakeAsync()
      final future = fetchData();

      async.elapse(Duration(seconds: 2)); // 2. Перемещаем виртуальное время на 2 секунды

      expect(future, completion('Data loaded')); // 3. Немедленно проверяем результат
    });
  });
}


fakeAsync() создает виртуальное время, которым можно управлять вручную через elapse(). Это позволяет имитировать прохождение времени и тут же проверять результат асинхронных операций — без реальных пауз.

Также  у fake_async есть другие полезные методы:
▪️ flushTimers() — для мгновенного выполнения всех запланированных таймеров
▪️ advanceTime(Duration duration) — похож на elapse, но иногда дает чуть более тонкий контроль

Хотя fake_async очень полезен, у него есть свои особенности, которые важно учитывать:
каждый вызов fakeAsync(...) создает свою виртуальную временную зону, полностью изолированную от других
он не управляет async/await напрямую, только таймерами и микрозадачами
может конфликтовать с tester.runAsync в flutter_test
elapse() не влияет на scheduleMicrotask — только на макрозадачи
потоки типа Stream.periodic могут вести себя нестабильно
в flutter_test возможны конфликты с фреймами и анимациями

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

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

📌 Подробнее о семантике во Flutter можно прочитать в официальной документации

🧠 Что такое Semantic Role?
Это описание назначения виджета, помогающее вспомогательным технологиям понять, как этот элемент должен «звучать» для пользователя. Flutter сам добавляет нужные роли в стандартные виджеты (например, TabBar, MenuAnchor, Table). Но если вы используете кастомный компонент — семантика может быть утеряна.

Например, если вы делаете список с кастомной реализацией, не забудьте явно указать SemanticsRole.list и SemanticsRole.listItem:

Semantics(
role: SemanticsRole.list,
explicitChildNodes: true,
child: Column(
children: [
Semantics(
role: SemanticsRole.listItem,
child: Text('Первый элемент списка'),
),
Semantics(
role: SemanticsRole.listItem,
child: Text('Второй элемент списка'),
),
],
),
)



🧪 Как тестировать доступность?
Flutter предлагает встроенные Guideline API.
Они проверяют:
✔️Размеры кликабельных областей (48x48 на Android, 44x44 на iOS)
✔️Контраст текста
✔️Наличие меток на интерактивных элементах

Такие тесты можно запускать параллельно с другими widget-тестами, например, в test/a11y_test.dart.

🌐 Доступность на Web
Для отладки доступности веб-приложений можно включить визуализацию семантических узлов:

flutter run -d chrome --profile --dart-define=FLUTTER_WEB_DEBUG_SHOW_SEMANTICS=true

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

Чеклист перед релизом
Перед выпуском убедитесь, что:
▫️Все интерактивные элементы работают и дают понятный фидбэк
▫️Экранный диктор читает все элементы (проверьте с TalkBack и VoiceOver)
▫️Контраст текста достаточный (не менее 4.5:1)
▫️Ничто не меняет контекст пользователя внезапно (например, не переходит на новый экран во время ввода)
▫️Размер кликабельных элементов — минимум 48x48
▫️Ошибки отображаются понятно и даются способы их исправить
▫️Интерфейс остается читаемым при масштабировании и в режимах для дальтоников

💚Доступность — это не только забота о людях с особыми потребностями, но и шаг к созданию действительно качественного продукта. Делайте интерфейсы удобными для всех, и вы точно не прогадаете.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍147🤩3
🏓Привет! С вами Анна, Flutter Team Lead Friflex

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

✔️ узнать реальное поведение пользователей, какие действия они предпринимают и что именно им интересно
✔️ определить «узкие места» — где пользователи теряются, какие функции вообще не используются
✔️ принять определенные бизнес-решения
✔️ оценить эффективность обновлений и внедрения нового функционала
✔️ обнаружить ошибки и падения приложения

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

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

Наиболее распространенные сервисы — AppMetrica и Firebase Analytics. Оба сервиса имеют адаптированные для Flutter плагины — appmetrica_plugin и firebase_analytics. Они обладают широким функционалом, который позволяет удобно структурировать собранные данные и формировать отчеты.

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


class AnalyticsService {
Future<void> init(String apiKey) async {
await AppMetrica.activate(AppMetricaConfig(apiKey));
}

Future<void> reportEvent(String event) async {
await AppMetrica.reportEvent(event);
}
}


Представьте, сегодня вы интегрировали один сервис аналитики, а через полгода бизнес принимает решение переехать на другой сервис. В таком случае, если у вас нет основного класса сбора аналитики, вам придется заново во всем проекте вносить изменения по всем точкам отправки данных. А в случае, если у вас есть класс-обертка, как в примере, изменения затронут лишь сам AnalyticsService.

3. Добавлять информативные события
Технически вы можете отправить события с любыми названиями, например, event_1, event_2. Но важно помнить, что основная цель сбора аналитики — это ее дальнейшая расшифровка и исследование. Поэтому гораздо информативнее и полезнее окажутся события с говорящим названием, например, exit_profile_button_tap.

4. Отправлять вложенные параметры
Представим, в вашем приложении несколько экранов, на которых есть кнопка «Добавить в корзину». При тапе на эту кнопку вы отправляете событие add_product_to_cart. Но в таком случае тут непонятно, что именно добавляется, с какого экрана.

Для бизнеса значительно полезнее будет то же событие, но с дополнительными параметрами:

'add_product_to_cart', {
'screen': 'main',
'product_id': '123'
}


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

6. Обязательно согласовывать события с аналитиками и заказчиком
Наверное, самый важный пункт из перечисленных. Задача разработчика — грамотно расставить метрики в коде. А их интерпретацией будут заниматься другие люди, на опыт и требования которых нам безусловно стоит опираться.

Делитесь в комментариях своими лучшими практиками сбора аналитики во Flutter-приложениях👇
Please open Telegram to view this post
VIEW IN TELEGRAM
9🔥6👍5👌1
This media is not supported in your browser
VIEW IN TELEGRAM
Всем привет, с вами Роза, Flutter Dev Friflex👋

Мы уже обсуждали, как устроена локализация во Flutter через .arb-файлы и flutter gen-l10n. Это отлично работает, пока ваш проект небольшой.

Но что делать, если команда растет, количество языков увеличивается, а переводы начинают теряться в чатах и таблицах? Невольно задумываешься: как хранить переводы и правильно организовать работу.

Есть разные способы, но сегодня поговорим про Translation Management Systems (TMS) — системах управления переводами.

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

Что же умеет TMS?
◾️ Централизует все переводы в одном месте
Можете забыть про разрозненные ARB-файлы или таблицы. Все ваши строки для всех языков хранятся в единой базе данных, доступной для всех участников команды.
    
◾️ Автоматизирует рутину
Вместо ручного экспорта/импорта, TMS автоматически подтягивает новые строки из кода, отправляет их на перевод, а затем генерирует готовые к использованию файлы для вашего приложения.
    
◾️ Разграничивает роли у пользователей
Каждый участник процесса видит только те задачи и языки, к которым у него есть доступ. Это помогает поддерживать порядок и безопасность.

◾️ Over-the-Air (OTA)
Пожалуй, одна из самых полезных фич! TMS позволяет обновлять тексты в приложении без выпуска новой версии.

◾️ Использует умные инструменты для переводчиков
Глоссарии, Translation Memory, подсказки по контексту — все это ускоряет работу и повышает качество переводов.

Когда стоит задуматься о TMS?
▫️ Команда растет, и вам нужен порядок в работе с переводами
▫️Проект масштабируется на 2-3 и более языков
▫️ Вы хотите видеть прогресс переводов по каждому языку и модулю
▫️ Устали от ручной работы по синхронизации файлов
▫️ Вам нужны функции вроде машинного перевода или памяти переводов

Если вы узнаете свой проект в одном или нескольких из этих пунктов, скорее всего, пришло время для централизованного решения.

Но TMS не всегда лучшее решение для проектов. Хотя они предлагают мощные возможности, есть и обратная сторона:
Стоимость
Большинство TMS работают по подписке, и это может быть накладно для маленьких команд.
Сложность внедрения
Интеграция TMS в существующий CI/CD-процесс может потребовать значительных усилий и времени. 
Избыточность для маленьких проектов
Если у вас всего один или два языка и ограниченное количество переводов, ручное управление через .arb-файлы и GitHub может быть более простым и экономичным решением

✔️Наиболее популярные TMS: Lokalise, Phrase, Crowdin и Localizely.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥73👍2💅1
➡️Привет, это Катя, Flutter Dev Friflex

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

Что мы хотим сделать?
Кнопка будет вести себя вот так:
Сначала — кнопка с текстом GET ➡️ нажимаем — она превращается в спиннер (идет подготовка загрузки) ➡️ потом появляется круговая загрузка с иконкой отмены ➡️ когда все скачалось — кнопка показывает OPEN

1. Создаем виджет DownloadButton
Это обычный StatelessWidget, который получает извне текущее состояние загрузки и отображает нужный UI.

enum DownloadStatus {
  notDownloaded,
  fetchingDownload,
  downloading,
  downloaded,
}


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

AnimatedContainer(
  duration: transitionDuration,
  decoration: ShapeDecoration(
    shape: isDownloading || isFetching
        ? CircleBorder()
        : StadiumBorder(),
    color: isDownloading || isFetching
        ? Colors.transparent
        : CupertinoColors.lightBackgroundGray,
  ),
  child: ...
);


3. Добавляем текст
GET → когда еще не загружено
OPEN → когда уже загружено
В промежуточных состояниях текст скрыт

AnimatedOpacity(
  opacity: isDownloading || isFetching ? 0.0 : 1.0,
  duration: transitionDuration,
  child: Text(
    isDownloaded ? 'OPEN' : 'GET',
    style: TextStyle(
      fontWeight: FontWeight.bold,
      color: CupertinoColors.activeBlue,
    ),
  ),
);


4. Добавляем индикатор загрузки
Если статус fetchingDownload — отображаем крутящийся спиннер.
Если downloading — прогресс-бар и иконка «стоп» по центру, чтобы отменить загрузку.

Stack(
  alignment: Alignment.center,
  children: [
    CircularProgressIndicator(value: progress),
    if (isDownloading)
      Icon(Icons.stop, size: 14, color: CupertinoColors.activeBlue),
  ],
);


5. Обработка нажатий
Когда пользователь нажимает кнопку — вызывается соответствующий колбэк:

void _onPressed() {
  switch (status) {
    case DownloadStatus.notDownloaded:
      onDownload();
      break;
    case DownloadStatus.downloading:
      onCancel();
      break;
    case DownloadStatus.downloaded:
      onOpen();
      break;
    case DownloadStatus.fetchingDownload:
      break;
  }
}


🟢В итоге — получаем кнопку с красивой анимацией и демонстрацией происходящего.

Подробнее можно прочитать в официальной документации.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥106👍5
This media is not supported in your browser
VIEW IN TELEGRAM
Всем привет! С вами Анна, Flutter Team Lead Friflex⭐️

Сегодня я поделюсь своим личным топ-5 практик для работы с изображениями и иконками в крупном Flutter-проекте.

1. Структурируйте файлы изображений
Когда проект масштабный, картинок и иконок может быть очень много. Чтобы было легко и просто ими управлять, хорошая практика — папку assets делить на вложенные папки по назначению изображений и группировать изображения по функционалу, где они применяются. Например, отделять фотографии от SVG-иконок, а иконки BottomNavigationBar от иконок фичи Корзина.

assets
|-- images
|-- feature_1
|-- feature_2
|-- icons
|-- bottom_navigation_bar
|-- features
|-- feature_1
|-- feature_2


2. Храните изображения в оптимальном формате
Это тот формат, который позволяет минимизировать размер файла, при этом сохраняя максимальное качество. Как показывает практика, для больших картинок и фотографий оптимальным можно считать WEBP формат, а для небольших иконок — SVG.

P.S: а еще лучше большие изображения не вшивать в приложение, а хранить на сервере

3. Кэшируйте сетевые изображения
Здесь могу рекомендовать плагин cached_network_image. Он позволяет загружать изображение из сети всего один раз. При повторном появлении картинки на экране загрузка будет локальная, очень быстрая.

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

5. Актуализируйте assets и pubspec
Чем старше и больше становится проект, тем реже разработчики вспоминают об этом пункте. Какие-то картинки заменились на новые, какие-то случайно продублировались при создании новой фичи — за этим важно следить. Все файлы, добавленные в assets и включенные в pubspec.yaml, попадают в сборку, непосредственно увеличивая ее размер.

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

Делитесь своими советами по работе с картинками в проекте👇
Please open Telegram to view this post
VIEW IN TELEGRAM
10👍6🔥4
💬Всем привет, это Роза, Flutter Dev Friflex!

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

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

Допустим, вы это сделали. Что дальше? Конечно, приступаем к внедрению!

Тут все зависит от того, какой генератор локализации вы используете:
▪️Если intl_utils — подключаете библиотеку, настраиваете нужные конфиги (ID проекта, API-ключ) и запускаете команду синхронизации
▪️Если другие генераторы — скачиваете файлы генерации (через интерфейс Localizely или API) и подключаете их в проект по стандартному шаблону
  
🚀Это все круто, но мы хотим больше. По первому плану вам нужно после каждого изменения ключей вручную синхронизировать переводы с сервисом. Но это можно автоматизировать — и еще добавить OTA (Over-the-Air), чтобы получать свежие переводы без релиза новой версии приложения.

Давайте по порядку!
✔️Автоматизация при помощи gitHub
Здесь все строится вокруг конфигурационного файла localizely.yml в репозитории. В нем указываете, какие файлы переводов импортировать (pull) из GitHub в Localizely и какие экспортировать (push) обратно. После этого в разделе Integrations настраиваете автоматическую синхронизацию и привязываете GitHub  для автоматического push/pull. А чтобы было еще удобнее — добавляете веб-хук. Тогда переводы обновляются в Localizely сразу после пуша изменений в репозиторий.

✔️Так, а теперь давайте к самому интересному — OTA
С ним вы можете доставлять новые переводы прямо в приложение без релиза в сторы. Пользователь просто открывает приложение, а там уже актуальные тексты.

Устроено это примерно таким образом: вы подключаете библиотеку для работы с OTA, добавляете SDK-токен и указываете ID дистрибутива — ветку с версиями релизов приложения. Далее при запуске приложение запрашивает сервис и сохраняет в кэш самую свежую версию переводов. Можно настроить обновление не только при старте, но и в фоне — например, раз в несколько часов или дней.

Это полезно, если у вас много динамичного контента или часто правятся тексты. 

⛓️‍💥В итоге у нас получается такая цепочка:  
GitHub следит за синхронизацией файлов → Localizely получает свежие ключи и переводы → OTA мгновенно доставляет их в приложение

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

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

Сегодня расскажу про распознавание лиц на Flutter. Это важный инструмент в современных мобильных приложениях: от разблокировки смартфона по лицу до автоматического заполнения данных по фото.

Используя Flutter, мы можем достаточно просто реализовать распознавание лиц, текста или звука, при этом кроссплатформенно. Сегодня я расскажу, как добавить распознавание лиц в свое приложение на Flutter с помощью Google ML Kit.

Зачем нужно распознавание лиц?
✔️
Безопасность — контроль доступа по лицу
✔️Идентификация — определение пользователя среди базы данных
✔️Интерактивные приложения — фильтры, маски и другие AR-фичи
✔️Автоматизация — например, распознавание посетителей в системе учета

Что нам понадобится
Для примера будем использовать пакет:
▪️google_mlkit_face_detection — для обнаружения лиц на изображениях
▪️image_picker — для выбора фото из галереи или камеры

Добавим в pubspec.yaml:

Yaml
dependencies:
  google_mlkit_face_detection: ^0.13.1
  image_picker: ^1.1.2


Настройка и инициализация

1️⃣ Импортируем пакеты:


import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
import 'package:image_picker/image_picker.dart';


2️⃣ Создаем переменные:

late ImagePicker _picker;
late FaceDetector _faceDetector;
dynamic _image;
Size? _imageSize;
List<Face> _faces = [];


3️⃣ Инициализируем в initState:

@override
void initState() {
  super.initState();
  _picker = ImagePicker();
  _faceDetector = FaceDetector(
    options: FaceDetectorOptions(),
  );
}


4️⃣Закрываем детектор в dispose:

@override
void dispose() {
  _faceDetector.close();
  super.dispose();
}


Логика распознавания
▫️Метод для получения и обработки изображения:


Future<void> _getAndScanImage({final bool? isFromCamera}) async {
  // Очищаем данные
  setState(() {
    _image = null;
    _faces = [];
    _imageSize = null;
  });

  // Получаем изображение
  final imageXFile = await _picker.pickImage(
    source: isFromCamera != null && isFromCamera
        ? ImageSource.camera
        : ImageSource.gallery,
  );

  // Обрабатываем
  if (imageXFile != null) {
    final inputImage = InputImage.fromFilePath(imageXFile.path);
    final facesList = await _faceDetector.processImage(inputImage);
    final imageAsBytes = await imageXFile.readAsBytes();
    final imageDecoded = await decodeImageFromList(imageAsBytes);

    setState(() {
      _faces = facesList;
      _image = imageDecoded;
      _imageSize = Size(
        imageDecoded.width.toDouble(),
        imageDecoded.height.toDouble(),
      );
    });
  }
}


▫️Отображение результата

Expanded(
  child: FittedBox(
    child: SizedBox(
      width: _imageSize!.width,
      height: _imageSize!.height,
      child: CustomPaint(
        painter: FacePainter(
          faceList: _faces,
          imageFile: _image,
        ),
      ),
    ),
  ),
)


Здесь FacePainter — это кастомный класс, который рисует рамки вокруг найденных лиц. Про CustomPainter рассказывала в этом посте.

Как видите, добавить распознавание лиц на Flutter можно буквально в несколько шагов. ML Kit обрабатывает изображение прямо на устройстве, без отправки данных на сервер, что повышает безопасность и скорость работы.
🔥64👍1
Привет! С вами Анна, Friflex Flutter Team Lead🚀

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

1 шаг — выбор инструмента определения геопозиции
Для получения данных о местоположении пользователя можно использовать следующие инструменты:

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

▫️ location — не менее популярный плагин, имеет функционал схожий с geolocator

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

2 шаг — настройка разрешений на Android
На Android необходимо в AndroidManifest.xml выдать специальные разрешения.

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>


Для Android API 29 и выше при необходимости использования геопозиции в фоне нужно добавить разрешение:

<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>


Важно! Необходимо следить за тем, какие разрешения вы запрашиваете, не запрашивать лишнего. При модерации Google Play может отклонить вашу сборку, если посчитает, что вы выполняете лишние запросы.

При запросе разрешений вам стоит предусмотреть сценарии, когда пользователь отклонил запрос 1 раз и больше. Если пользователь выбрал пункт «Больше не спрашивать» или на последних версиях Android отклонил разрешение дважды, выдать разрешение он сможет только вручную из настроек. Здесь важно обеспечить ему наиболее простой путь.

3 шаг — настройка разрешений на iOS

На iOS необходимо в Info.plist добавить ключи с необходимым разрешением.

<key>NSLocationWhenInUseUsageDescription</key>
<string>Для определения вашего местоположения</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>Для фонового доступа к местоположению</string>


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

4 шаг — интеграция функционала в приложение

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

Готово!

❤️ — если было полезно
11🔥3💯2
🌎А какой инструмент для определения геопозиции чаще всего используете вы?
Anonymous Poll
70%
Плагин geolocator
17%
Плагин location
14%
Кастомную реализацию
This media is not supported in your browser
VIEW IN TELEGRAM
Всем привет, это Роза, Flutter Dev Friflex💫

Представьте: у вас есть данные, которые нужно защитить и передать через API. Как это сделать безопасно?

Здесь можно использовать JWT (JSON Web Token).

Веб-токены JSON (JWT) — это компактный и безопасный способ передачи информации между сторонами в виде JSON-объекта. JWT подписывается секретным ключом или парой ключей (RSA/ECDSA), что позволяет убедиться в целостности данных.

Почему JWT лучше логина/пароля?

✔️ Ограниченный срок жизни: пароль может «утечь» и жить вечно, а токен живет, например, всего 15 минут. Даже если его украдут, через короткое время он станет бесполезным.
✔️ Нет нагрузки на БД: проверка пароля требует похода в базу. JWT-сервер проверяет «на месте», без лишних запросов.

Если вы используете «черный список» для отзыва токенов, то придётся обращаться к базе или Redis для проверки, не был ли токен отозван.

Виды JWT

▪️Access token — токен для доступа к API, живет недолго (~15 мин). При истечении срока сервер возвращает 401. По умолчанию — многоразовый
▪️ Refresh token — токен для обновления пары токенов, живет дольше (несколько дней). Используется на эндпоинте ~/auth/refresh, когда истекает срок access токена

Структура JWT

Токен состоит из 3 частей, разделенных точкой: xxxxx.yyyyy.zzzzz.

1. Header — заголовок, который хранит тип токена (JWT) и алгоритм подписи (например, HMAC SHA256)
    
2. Payload — данные о пользователе и токене. Стандартные поля:
▫️ iss — кто выдал токен (issuer)
▫️ sub — идентификатор пользователя (subject)
▫️ aud — для кого предназначен токен (audience)  
▫️ exp — время, в течение которого токен считается валидным (expiration)
▫️ iat — время создания токена (issued at)
▫️ jti — уникальный идентификатор токена (JWT ID)

3. Signature — подпись, гарантия целостности
 
⚠️ Важно: данные в payload видны всем. JWT подписывается, а не шифруется


Как JWT защищает наши данные?

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

Black-list токенов

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

При проверке сервер сначала смотрит, не попал ли токен туда, и только потом валидирует его. Если токен найден в «черном списке» → возвращается 401.

Аутентификация с JWT

Когда говорят об аутентификации с помощью JWT, то имеют в виду, как приложения подтверждают личность пользователя. Устроено это примерно таким образом:
✔️ Пользователь вводит логин и пароль
✔️ Сервер проверяет данные и, если они верны, генерирует access и refresh токены
✔️ Клиент сохраняет токены (например, в Secure Storage)
✔️ При каждом запросе к защищенному API клиент отправляет access token в заголовке  Authorization: Bearer <token>
✔️ Сервер проверяет подпись и срок действия токена
✔️ Если access token истек, клиент использует refresh token для получения новой пары токенов

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

❤️ — и в следующем посте разберем, как реализовать авторизацию во Flutter с помощью JWT
12👍4🔥3😁1
Привет, это Катя, Friflex Flutter Dev👋

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

Распознавание текста (OCR) — это одна из самых популярных функций в мобильных приложениях:
▪️сканирование документов
▪️автоматическое заполнение форм по фото
▪️извлечение текста из визиток, чеков, квитанций
▪️перевод текста прямо с картинки
С помощью Flutter и Google ML Kit добавить OCR в приложение можно всего за несколько шагов.

Зачем нужно распознавание текста?
✔️Автоматизация — больше не нужно вручную переписывать текст
✔️Удобство — пользователь делает фото, а приложение достаёт оттуда данные
✔️Инклюзивность — для слабовидящих можно превращать картинку с текстом в озвученный контент
✔️Применимость в бизнесе — распознавание документов, счетов, ценников

Что нам понадобится
Для примера будем использовать пакеты:
▪️google_mlkit_text_recognition — для распознавания текста
▪️image_picker — для выбора фото из галереи или камеры

Добавим в pubspec.yaml:

Yaml
dependencies:
  google_mlkit_text_recognition: ^0.13.1
  image_picker: ^1.1.2


Настройка и инициализация

1️⃣ Импортируем пакеты:

import 'dart:io';
import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart';
import 'package:image_picker/image_picker.dart';


2️⃣ Создаем переменные:

late TextRecognizer _recognizer;
File? _imageFile;
String _scanResults = '';


3️⃣ Инициализируем в initState:

@override
void initState() {
  super.initState();
  _recognizer = TextRecognizer();
}


4️⃣ Закрываем в dispose:

@override
void dispose() {
  _recognizer.close();
  super.dispose();
}


Логика распознавания

Метод для получения и обработки изображения:

Future<void> _getAndScanImage({final bool? isFromCamera}) async {
  // Очищаем данные
  setState(() {
    _imageFile = null;
    _scanResults = '';
  });

  // Получаем изображение
  final pickedImage = await ImagePicker().pickImage(
    source: isFromCamera != null && isFromCamera
        ? ImageSource.camera
        : ImageSource.gallery,
  );

  // Обрабатываем
  if (pickedImage != null) {
    final file = File(pickedImage.path);
    setState(() {
      _imageFile = file;
    });

    final results = await _recognizer.processImage(
      InputImage.fromFile(file),
    );

    setState(() {
      _scanResults = results.text;
    });
  }
}


Отображение результата

Полученный текст можно вывести обычным Text:

Text(_scanResults)


А изображение показать через Image.file(_imageFile!), обернув все в Column:

Column(
  children: [
    if (_imageFile != null) Image.file(_imageFile!),
    const SizedBox(height: 16),
    Text(_scanResults),
  ],
)


Как видите, добавить распознавание текста на Flutter тоже очень просто. Эта функция подойдет для любых приложений, где пользователю важно быстро извлечь текст с фото или документа.
👍9🔥83