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

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

Сегодня немного о том, как улучшить 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
12🔥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🔥2
Привет, это Катя, 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
69%
Плагин geolocator
16%
Плагин location
15%
Кастомную реализацию
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
➡️Всем привет! Это Анна, Friflex Flutter Team Lead

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

Сегодня рассмотрим новую экспериментальную фичу, выпущенную с версией 3.35 — Flutter Widget Previewer.

Flutter Widget Previewer позволяет отобразить текущий вид конкретного виджета без запуска всего приложения на эмуляторе или реальном устройстве. На сегодня превью доступно только для отображения в Chrome, но команда Flutter планирует в скором будущем поддерживать эту функцию и в IDE, и на веб-сервере.

Как запустить?
В первую очередь вам необходимо установить Flutter версии 3.35.0 или выше.

Для запуска требуется в терминале выполнить команду

flutter widget-preview start


Она запустит локальный сервер и откроет превью виджета в Chrome.

Как использовать?
Flutter новой версии дает доступ к аннотации @Preview. Эту аннотацию можно подключить к:
▪️конструкторам и фабрикам публичных виджетов без обязательных аргументов
▪️ верхнеуровневым методам, возвращающим Widget или WidgetBuilder
▪️ статическим методам внутри класса, которые тоже возвращают Widget или WidgetBuilder

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

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

Рассмотрим на примере
Создадим простой виджет-карточку с картинкой, заголовком и описанием. Добавим к конструктору этого виджета аннотацию и запустим превью.

class CustomCard extends StatelessWidget {
@Preview()
const CustomCard({super.key});

@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Container(
height: 80,
width: 80,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.0),
image: const DecorationImage(
image: NetworkImage(
'https://images.unsplash.com/photo-1755133314246-2103970d4726?q=80&w=988&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
),
fit: BoxFit.cover,
),
),
),
const SizedBox(width: 16),
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Фото #2345',
style: TextStyle(fontSize: 20),
),
SizedBox(width: 16),
Text(
'Описание к фотографии #2345',
style: TextStyle(fontSize: 16),
),
],
),
],
),
),
);
}
}


Вид, который отобразится на выходе в превью, показала на картинке👆

Какие плюсы/минусы?
Лично для себя я нашла и преимущества, и недостатки.
запуск превью очень быстрый, в разы быстрее сборки
так как превью в вебе — легко проверять адаптивность верстки, достаточно поменять размер окна Chrome
можно имитировать разные системные условия, например, тему, локализацию
снижает нагрузку на компьютер разработчика, так как не надо запускать эмуляторы (актуально для слабых устройств)
позволяет детальнее проверять отдельные части интерфейса

фича экспериментальная, а значит еще «сырая»
пока нет поддержки IDE (а было бы очень удобно)
в текущей реализации не может обеспечить полноценный анализ интерфейса, так как хорошо работает только с простыми и статичными виджетами, без анимаций и зависимости от внешних состояний

📎Больше информации можно найти в официальной документации.

А какие у вас впечатления от этой фичи?
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥116👍5🤩1
Привет, это Роза, Flutter Dev Friflex🎙

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

🦖А поговорим мы про Docusaurus! Это опенсорс-фреймворк от Meta, который позволяет быстро развернуть сайт документации.

Идея Docusaurus заключается в том, чтобы писать документацию с использованием Markdown и MDX на основе React. Такое сочетание позволяет легко создавать как обычные страницы, так и интерактивный контент — например, вставлять компоненты прямо в документацию.

Основные возможности:
✔️ Основан на React, поэтому все легко кастомизировать
✔️ Есть готовые темы и поддержка Markdown, чтобы писать проще и быстрее
✔️ Простая интеграция с GitHub Pages или любым хостингом
✔️ Много фич «из коробки»: поиск, версионирование, темная тема
✔️ Поддерживает миграцию

Как работать с Docusaurus?

Давайте быстренько пройдемся по шагам. Для начала вам нужно создать Docusaurus-проект в вашем репозитории. Для этого (после установки Node.js) запустите:

npx create-docusaurus@latest my-website classic


В результате у вас появится примерно такая структура:

my-website  
├── blog
├── docs
├── src
├── static
├── docusaurus.config.js
├── package.json
└── sidebars.js


Ключевые каталоги:

▫️blog/ — записи блога, которые можно писать на Markdown или MDX
▫️docs/ — ваши файлы документации
▫️src/ — кастомные компоненты, страницы и стили на React
▫️static/ — статические ресурсы, например картинки и шрифты
▫️docusaurus.config.js — главный файл конфигурации сайта
▫️sidebars.js — настройка боковой панели и структуры документации

Запуск и публикация

Чтобы посмотерть, что у вас получилось:

1. Установите зависимости:

npm install


2. Запустите локальный сервер разработки:

npx docusaurus start


Сайт будет доступен по адресу: https://localhost:3000.

Все изменения в *.md-файлах или компонентах будут применяться мгновенно благодаря горячей перезагрузке.

На этом этапе ваш сайт уже работает — осталось лишь наполнить его контентом и опубликовать.

Далее все зависит от ваших умений.

✔️ Хотите полноценный сайт с главной страницей, кастомными компонентами и выпадающими окнами? Используйте React/JS/TS и расширяйте проект

✔️ Нужна просто документация? Достаточно оформить всё в .md-файлах в docs — роутинг создаётся автоматически (можно и вручную)

Для публикации ващего сайта вам нужно будет запустить команду:

npm run build


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

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

npm run serve


Это запустит локальный сервер, и вы сможете просматривать свой сайт, перейдя по указанному URL.

Развернуть сайт можно где угодно: GitHub Pages, Vercel, Netlify, Docker + собственный домен.

В итоге с Docusaurus можно буквально за пару часов поднять удобный и красивый сайт документации, который легко поддерживать и расширять🙌
Please open Telegram to view this post
VIEW IN TELEGRAM
👍144🔥3
👀Привет! Хочется больше узнать о том, что волнует именно вас.

〰️Столкнулись с какой-то сложной задачей во Flutter?
〰️ Есть грабли, на которые наступаете снова и снова?
〰️ Или тема, о которой хотелось бы прочитать в канале?

📍Делитесь через форму — разберемся вместе, самые интересные кейсы разложим по полочкам в следующих постах
Please open Telegram to view this post
VIEW IN TELEGRAM
7🔥6👍4