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

🔗 Наш канал для разработчиков: @friflex_dev
🔗 Канал о продуктовой разработке: @friflex_product
Download Telegram
This media is not supported in your browser
VIEW IN TELEGRAM
Ценный артефакт: книга «Основы Flutter» c афтографами авторов. Разыграем в канале?
👍22💯6
Подвели итоги конкурса на CrossConf. Поздравляем:

@malou1337
@Suneru
@swogf

Ждем победителей у стенда Friflex
5👏4
@floral_whale (Роза) ❤️, рассказывала про их сервис локализации с OTA обновлениями, прикольно

Из известных мне вроде знаю только о phrase

Ждем когда доработают до конца 👍
Выглядит вроде как production-ready
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥97
Всем привет, это Роза, Flutter dev Friflex!

Сегодня мы разберем, как быстро поднять и использовать собственное облачное хранилище MinIO S3.

MinIO — это полностью совместимый с S3-протоколом объектный сторедж для хранения файлов, резервных копий и больших датасетов.

MinIO поддерживает S3 API (операции GET, PUT, DELETE ), а также:
✔️Управление бакетами: Bucket policy, versioning и lifecycle rules
✔️Безопасность: шифрование на стороне сервера и клиента
✔️Поддержку Multi-user / Multi-tenant окружений
✔️Простую интеграцию с любыми S3-SDK и библиотеками

Он совместим с разными S3-инструментами, легко масштабируется благодаря архитектуре Shared-Nothing, обрабатывает запросы параллельно, использует Erasure Coding для надежности и восстановления данных, а целостность хранения обеспечивается криптографическими хэшами.

Для чего можно использовать MinIO:
▪️Приватное облако — хранение изображений и файлов пользователей
▪️AI/ML и Big Data — хранение больших наборов данных
▪️Бэкапы — резервное копирование баз данных и виртуальных машин

Использование MinIO во Flutter
Благодаря S3-совместимости, MinIO можно использовать как локальное S3 для быстрой разработки приложений на Flutter.

Для работы подходит пакет minio, совместимый с MinIO, AWS S3 и Firebase Storage.

Чтобы начать работу, достаточно создать клиент, передав данные S3-сервера:

import 'package:minio/minio.dart';

MinioClient minio = MinioClient (
  endPoint : 'your-s3-endpoint-url.com', // Замените на адрес вашего MinIO
  accessKey : 'your-access-key',        // Лучше передавать через переменные окружения (env)
  secretKey : 'your-secret-key',        // Лучше передавать через переменные окружения (env)
  useSSL : false, // Установите true, если ваш сервер S3 использует HTTPS
);


Библиотека предоставляет удобные методы для работы с объектами. Рассмотрим некоторые из них:

◽️Загрузка объекта в S3
Метод putObject позволяет загружать объекты из потока байтов (Stream<Uint8List>).

Future<AppFileLink> uploadFile({
  required String bucket,
  required String object,
  required Stream<Uint8List> data,
}) async {
  // putObject выполняет загрузку
  return minio 
    .putObject(
      bucket,
      object,
      data,
    )
    // После успешной загрузки получаем публичную ссылку
    .then((_) => getPublicUrl(bucket: bucket, object: object)); 
}

   
◽️Загрузка с прогрессом
Для больших файлов можно отслеживать ход загрузки с помощью колбэка onProgress:

await minio.putObject(
  'mybucket', 
  'myobject', 
  Stream<Uint8List>.value(Uint8List(1024)),
  onProgress: (bytes) {
    print('$bytes uploaded');
  },
);


◽️Получение временно подписанной ссылки (Presigned URL)
Для безопасного предоставления доступа к файлу без публичного доступа к бакету используется временно подписанная ссылка.

Future<AppFileLink> getPublicUrl({
  required String bucket,
  required String object,
  Duration expires = const Duration(days: 7), // Срок действия ссылки
}) async {
  final link = await minio.presignedGetObject(
    bucket,
    object,
    expires: expires.inSeconds, // Срок действия в секундах
  );
  // Возвращаем ссылку
  // ...
}


📎С полным списком методов можно ознакомиться в документации библиотеки

Продолжение — в комментариях 👇
🔥8👍54
🟢Привет, это Катя, Flutter Dev Friflex.

Сегодня расскажу, как устроен Event Loop и почему понимание его работы критически важно для написания эффективного асинхронного кода.

Event Loop — это механизм управления асинхронностью, который обрабатывает задачи (events) в определенном порядке. Он позволяет выполнять операции ввода-вывода и другие длительные процессы без блокировки основного потока.

Основные компоненты
✔️Future
— объект, представляющий результат асинхронной операции, который станет доступен в будущем
✔️Stream — поток асинхронных данных, который может передавать несколько значений со временем
✔️Event Loop — центральный механизм, управляющий очередями задач и их последовательным выполнением
✔️Система очередей
Event Loop работает с двумя типами очередей:

▪️Microtask Queue — очередь микрозадач с наивысшим приоритетом. Выполняется сразу после завершения синхронного кода, до обработки основных событий. 
▪️Event Queue — основная очередь событий, содержащая таймеры, операции ввода-вывода, Future и другие задачи.

Порядок: Сначала синхронный код → все микрозадачи → задачи из Event Queue.

Пример

import 'dart:async';

void main() {
  print('Начало');

  Timer(Duration(seconds: 1), () => print('Timer'));
  scheduleMicrotask(() => print('Microtask 1'));
  Future(() => print('Future')).then((_) => print('Future then'));
  scheduleMicrotask(() => print('Microtask 2'));

  print('Конец');
}


Вывод:
Начало
Конец
Microtask 1
Microtask 2
Future
Future then
Timer


Сначала выполняется синхронный код (Начало, Конец), затем обе микрозадачи, и только после этого Future и Timer из основной очереди.

Работа с async/await
Ключевые слова async и await упрощают работу с асинхронным кодом, делая его похожим на синхронный:

Future<void> fetch() async {
  print('Загрузка...');
  await Future.delayed(Duration(seconds: 2));
  print('Готово!');
}

void main() async {
  print('Старт');
  await fetch();
  print('Финиш');
}


Вывод:
Старт
Загрузка...
(пауза 2 секунды)
Готово!
Финиш


Оператор await приостанавливает выполнение функции до завершения Future, но не блокирует Event Loop — он может обрабатывать другие задачи.

Итого
Синхронный код всегда выполняется первым, микрозадачи имеют приоритет перед обычными событиями, а Future и Timer обрабатываются в порядке добавления в Event Queue.

Закрепим знания задачкой

В какой последовательности будут выведены значения?

import 'dart:async';

void main() {
  print('1');
  
  Future(() => print('2')).then((_) {
    print('3');
    scheduleMicrotask(() => print('4'));
  });
  
  scheduleMicrotask(() => print('5'));
  
  Future(() => print('6'));
  
  print('7');
}
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥81👍1
😎 В какой последовательности будут выведены значения?
Anonymous Quiz
9%
1, 7, 5, 2, 3, 6, 4
69%
1, 7, 5, 2, 3, 4, 6
8%
1, 7, 2, 5, 3, 4, 6
13%
1, 7, 5, 2, 6, 3, 4
7👍4🆒1
This media is not supported in your browser
VIEW IN TELEGRAM
🌟Всем привет! Это Анна, Flutter Team Lead Friflex

Наверняка каждый Flutter-разработчик знает, что такое Isolate и как он работает. Немного освежим теорию в памяти.

Dart — однопоточный язык программирования. Когда мы создаем Flutter-приложения, все вычисления и операции выполняются по умолчанию в основном и единственном потоке — изоляте (Isolate).

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

В Dart изоляты можно запускать тремя основными способами:
1️⃣ Isolate.run() — метод запустит новый изолят, в котором будет выполнена переданная функция. По завершении операций из изолята вернутся ожидаемые данные, а поток самостоятельно завершится
2️⃣ Isolate.spawn() — также запускает новый изолят, в который необходимо передавать функцию для выполнения. При этом в отличие от изолята, запущенного через run(), дает возможность непрерывно обмениваться сообщениями с основным потоком
3️⃣ Isolate.spawnUri() — работает аналогично spawn() за одним исключением. В изолят можно отправить прямую ссылку на Dart-программу.

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

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

В решении этой задачи нам поможет Isolate.spawn() и порты получения данных ReceivePort().

Шаг 1. Создаем top-level метод парсинга данных в изоляте _parseInIsolate(). Метод должен принимать обязательно два порта отправки данных — порт отправки результата парсинга и порт отправки ошибок. Именно в этом методе мы закладываем логику обработки ошибки. В случае перехвата ошибки, отправляем ее в порт ошибок errorSendPort

Future<void> _parseInIsolate(
({
SendPort dataSendPort,
SendPort errorSendPort,
List<Map<String, dynamic>> rawDataList,
}) params,
) async {
final result = <ResultEntity>[];

for (final data in params.rawDataList) {
try {
// здесь выполняем парсинг каждого элемента
result.add(...);
} on Object catch (error, stackTrace) {
// в случае ошибки парсинга конкретного объекта
// отправляем ошибку обратно в главный изолят
params.errorSendPort.send((error: error, stackTrace: stackTrace));
continue;
}
}
// в порт данных отправляем готовые объекты
params.dataSendPort.send(result);
}


Шаг 2. Создаем метод получения данных fetchData(). В нем выполняем запрос на бэкенд. Дальнейшие шаги будут дополнять этот метод

Future<List<ResultEntity>> fetchData() async {
final rawDataList = await _httpClient.get('v1/examples/data');
}


3 шаг. После получения данных создаем два порта ReceivePort(). Один порт будет отвечать за передачу обработанных объектов, второй — за передачу данных о локальных ошибках парсинга

final dataPort = ReceivePort();
final errorPort = ReceivePort();


Шаг 4. Открываем новый изолят и запускаем в нем метод _parseInIsolate(). Обязательно передаем в него два порта отправки и данные, которые надо обработать


final isolate = await Isolate.spawn(
_parseInIsolate,
(
dataSendPort: dataPort.sendPort,
errorSendPort: errorPort.sendPort,
rawDataList: rawDataList,
),
);


Продолжение в комментариях👇
Please open Telegram to view this post
VIEW IN TELEGRAM
9🔥6👌1
Знаете, что объединяет все эти фото?

Концентрация Flutter-разработчиков на них зашкаливает. Делимся яркими моментами с конференции CrossConf.

Героини и авторы постов @flutterfriendly тоже там были: Катя и Анна модерировали потоки по Flutter. Роза выступала с докладом.

📎 Еще больше кадров — здесь
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
12🔥2
💭Всем привет, это Роза, Flutter Dev Friflex

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

Шейдер — это программа, которая выполняется на графическом процессоре и отвечает за вычисление цвета каждого пикселя. Именно благодаря шейдерам мы можем получать освещение, тени, блики, искажения, волны, шумы, стеклянные поверхности и множество других эффектов. Для их написания используется язык GLSL. Он напоминает Си и специально создан для задач реального времени, например, в играх и анимации.

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

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

1️⃣ Чтобы шейдер стал частью приложения, достаточно подключить его в pubspec.yaml:

flutter: 
  shaders: 
    - shaders/my_shader.frag


Здесь важно именно расширение .frag. Flutter пока поддерживает только фрагментные шейдеры.

2️⃣ После этого загружаем шейдер:

late FragmentProgram program;

Future<void> loadMyShader() async {
  program = await FragmentProgram.fromAsset('shaders/my_shader.frag');
}


Объект FragmentProgram можно использовать для создания одного или нескольких экземпляров FragmentShader. 

3️⃣ Далее создаем FragmentShader и передаем нужные параметры. Например, размер canvas или время для анимации. Сам рендеринг удобно выполнять через CustomPainter:

class ShaderPainter extends CustomPainter {
  final FragmentShader shader;
  final double time;

  ShaderPainter(FragmentShader fragmentShader, this.time)
      : shader = fragmentShader;

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint();
    shader.setFloat(0, size.width);
    shader.setFloat(1, size.height);
    shader.setFloat(2, time);
    paint.shader = shader;
    canvas.drawRect(Offset.zero & size, paint);
  }

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



Важно помнить про несколько нюансов:
▪️Шейдеры во Flutter пока ограничены только фрагментными программами. Их размер и сложность влияют на вес сборки и производительность, поэтому лучше не переусердствовать
▪️Желательно кэшировать FragmentProgram, чтобы не загружать его повторно
▪️И, конечно, следить за количеством uniform-переменных

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

shader.setFloat(0, controller.value);


Контроллер меняет число каждый кадр, шейдер пересчитывает волну, и ваш UI плавно анимируется, нагружая GPU, а не CPU.

Ключевой момент: индекс в shader.setFloat(index, value) обязательно должен совпадать с порядком объявления uniform-переменных в GLSL-коде.

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

❤️ — если хотите посты, в которых мы разберем создание своего собственного шейдера и подключение его к анимации
Please open Telegram to view this post
VIEW IN TELEGRAM
14👍2🔥1
This media is not supported in your browser
VIEW IN TELEGRAM
👍Привет, это Катя, Friflex Flutter Dev

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

Базовое сравнение
Встроенный оператор == у коллекций (List, Set, Map) проверяет ссылки на объект, а не содержимое. То есть два списка с одинаковыми элементами будут не равны, если это разные объекты в памяти:

void main() {
  var a = [1, 2, 3];
  var b = [1, 2, 3];

  print(a == b); // false, разные объекты
  print(identical(a, b)); // false
}


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

ListEquality
Сравнение списков поэлементно в строгом порядке:

import 'package:collection/collection.dart';

void main() {
  var a = [1, 2, 3];
  var b = [1, 2, 3];

  print(const ListEquality().equals(a, b)); // true
}


SetEquality
Сравнение множеств без учета порядка элементов:

import 'package:collection/collection.dart';

void main() {
  var a = {1, 2, 3};
  var b = {3, 2, 1};

  print(const SetEquality().equals(a, b)); // true
}


MapEquality
Сравнение словарей по ключам и значениям:

import 'package:collection/collection.dart';

void main() {
  var a = {"x": 1, "y": 2};
  var b = {"y": 2, "x": 1};

  print(const MapEquality().equals(a, b)); // true
}


Глубокое сравнение
Если коллекции вложенные (List<Map<String, Set<int>>>), то используют DeepCollectionEquality. Он рекурсивно сравнивает элементы на любом уровне вложенности:

import 'package:collection/collection.dart';

void main() {
  var a = [
    {"nums": {1, 2}}
  ];
  var b = [
    {"nums": {2, 1}}
  ];

  print(const DeepCollectionEquality().equals(a, b)); // true
}


Сравнение с кастомной логикой
Можно задать свою функцию сравнения для элементов коллекций:


import 'package:collection/collection.dart';

void main() {
  var a = ["Hello", "world"];
  var b = ["hello", "WORLD"];

  var eq = ListEquality(StringEquality(ignoreCase: true));
  print(eq.equals(a, b)); // true
}


❤️ — если было полезно
Please open Telegram to view this post
VIEW IN TELEGRAM
24👍7🔥3
This media is not supported in your browser
VIEW IN TELEGRAM
У вас собеседование через 5 минут!

🔤🔤🔤! Испугались? Привет! Это Анна, Flutter Team Lead Friflex. Сегодня мы опустим все вопросы технических реализаций и поговорим о насущном, о том, что вселяет страх и ужас начинающим специалистам — техническом собеседовании на позицию Flutter-разработчика.

Для начала отмечу, что в зависимости от компании и от уровня кандидата собеседования могут проходить немного по-разному. Здесь будет некоторая усредненная информация, которая поможет подготовиться именно начинающим разработчикам.

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

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

2️⃣ Технические вопросы
Техническое собеседование всегда подразумевает их наличие. Здесь рекомендую заранее изучить списки вопросов под свой уровень в интернете. А также саму вакансию — почти всегда в вакансии указывается стек технологий, по которому с большей долей вероятности вы получите вопрос на собеседовании. От себя для новичков могу посоветовать изучить следующие темы:

Dart
▫️ Null-safety
▫️ Event Loop
▫️ параллельность
▫️ dart:async, Future, Stream, Isolate
▫️ модификаторы классов и переменных, конструкторы классов
▫️ коллекции и операции с ними
▫️ обработка ошибок

Flutter
▫️ Stateless- и Stateful-виджеты, жизненный цикл Stateful-виджета
▫️ Layout-виджеты, отличия Flexible/Expanded
▫️ InheritedWidget
▫️ BuildContext, Widget, Element
▫️ навигация без библиотек
▫️ Navigator, отличия императивной и декларативной (верхнеуровневой)
▫️ анимации
▫️ виджеты прокрутки (SingleChildScrollView, ListView, CustomScrollView)
▫️ тесты

Конечно, это только малая часть. Если у вас есть, чем дополнить этот список — wellcome в комментарии!

3️⃣ Моральная подготовка
Собеседование — это всегда стресс для кандидата. Честно скажу, я пока не встречала ни одного человека, который не переживал бы перед ним. И здесь важно понять, что волнение — это абсолютно нормальная реакция на этот этап. От волнения можно сбиться, запутаться, что-то забыть, и это норма!

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

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

Давайте в комментариях соберем лучшие советы для всех, кто проходит и кто проводит собеседования👇
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥97😁3🥰1