Совсем недавно команда 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
🔥12❤6👍5🤩1
Привет, это Роза, Flutter Dev Friflex🎙
Недавно у меня была задача — нужно было сделать сайт документации для одного проекта. Хочу поделиться удобным инструментом для этого, пока готовлю продолжение по jwt.
🦖А поговорим мы про Docusaurus! Это опенсорс-фреймворк от Meta, который позволяет быстро развернуть сайт документации.
Идея Docusaurus заключается в том, чтобы писать документацию с использованием Markdown и MDX на основе React. Такое сочетание позволяет легко создавать как обычные страницы, так и интерактивный контент — например, вставлять компоненты прямо в документацию.
Основные возможности:
✔️ Основан на React, поэтому все легко кастомизировать
✔️ Есть готовые темы и поддержка Markdown, чтобы писать проще и быстрее
✔️ Простая интеграция с GitHub Pages или любым хостингом
✔️ Много фич «из коробки»: поиск, версионирование, темная тема
✔️ Поддерживает миграцию
Как работать с Docusaurus?
Давайте быстренько пройдемся по шагам. Для начала вам нужно создать Docusaurus-проект в вашем репозитории. Для этого (после установки Node.js) запустите:
В результате у вас появится примерно такая структура:
Ключевые каталоги:
▫️blog/ — записи блога, которые можно писать на Markdown или MDX
▫️docs/ — ваши файлы документации
▫️src/ — кастомные компоненты, страницы и стили на React
▫️static/ — статические ресурсы, например картинки и шрифты
▫️docusaurus.config.js — главный файл конфигурации сайта
▫️sidebars.js — настройка боковой панели и структуры документации
Запуск и публикация
Чтобы посмотерть, что у вас получилось:
1. Установите зависимости:
2. Запустите локальный сервер разработки:
Сайт будет доступен по адресу:
Все изменения в *
На этом этапе ваш сайт уже работает — осталось лишь наполнить его контентом и опубликовать.
Далее все зависит от ваших умений.
✔️ Хотите полноценный сайт с главной страницей, кастомными компонентами и выпадающими окнами? Используйте React/JS/TS и расширяйте проект
✔️ Нужна просто документация? Достаточно оформить всё в
Для публикации ващего сайта вам нужно будет запустить команду:
В папке
Чтобы убедиться, что все работает так, как задумано, протестируйте сборку локально, выполнив следующую команду:
Это запустит локальный сервер, и вы сможете просматривать свой сайт, перейдя по указанному URL.
Развернуть сайт можно где угодно: GitHub Pages, Vercel, Netlify, Docker + собственный домен.
В итоге с Docusaurus можно буквально за пару часов поднять удобный и красивый сайт документации, который легко поддерживать и расширять🙌
Недавно у меня была задача — нужно было сделать сайт документации для одного проекта. Хочу поделиться удобным инструментом для этого, пока готовлю продолжение по 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
👍14❤4🔥3
Please open Telegram to view this post
VIEW IN TELEGRAM
❤7🔥6👍4
На днях Google объявил о грядущих глобальных изменениях в политике безопасности Android. С 2027 года компания планирует ужесточить правила установки приложений из неизвестных источников по всему миру. Это затронет и создателей софта.
✔️Google Play становится единственным местом установки приложений. Тут непонятно, конечно, с RuStore
✔️Установка APK извне Google Play усложняется
✔️Google вводит механизм верификации разработчиков. Те, кто уже публикуется в Google Play, будут верифицированы автоматически
✔️Сайлоудинг останется доступным, дебажные сборки не будут затронуты
✔️Верификация будет бесплатной для студентов и независимых разработчиков, но регистрация аккаунта обойдется в 25$
✔️Разработчикам придется платить за публикацию приложений в Google Play
✔️Новая консоль предназначена для распространения приложений за пределами Google Play
✔️Пользователи, привязывающие свои сборки к аккаунтам разработчиков, могут нести юридическую ответственность
Еще есть время оценить свои каналы распространения и начать процесс верификации заранее.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤8🔥4👍2
Сегодня начнем разбирать вопрос про Provider, который нам прислал один прекрасный подписчик. Разработчики provider описывают этот класс как некоторую обертку для InheritedWidget. Чтобы лучше его понять, сегодня вспомним, что такое InheritedWidget и как он работает.
InheritedWidget — виджет, который позволяет передавать некоторые данные дальше по дереву виджетов. При его использовании пропадает необходимость прокидывать данные через конструкторы. Для примера создадим виджет CustomInherited.
class CustomInherited extends InheritedWidget {
const CustomInherited({required this.data, required Widget child})
: super(child: child);
final String data;
static CustomInherited? of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<CustomInherited>();
@override
bool updateShouldNotify(CustomInherited oldWidget) => data != oldWidget.data;
}
CustomInherited реализует статический метод
of(context)
. Под капотом этот метод вызывает context.dependOnInheritedWidgetOfExactType<T>()
, который в свою очередь по переданному контексту выполняет поиск самого ближайшего экземпляра виджета типа CustomInherited и возвращает его.Теперь CustomInherited готов к внедрению в дерево виджетов.
CustomInherited(
data: 'inherited data',
child: Container(
padding: const EdgeInsets.all(16),
child: Text(CustomInherited.of(context)?.data ?? ''),
),
),
Так как InheritedWidget предоставляет данные по контексту дальше, то его необходимо располагать выше виджетов, которые должны иметь доступ к его данным. Здесь, например, виджету Text необходимо передать строку из CustomInherited, поэтому инхерит располагаем выше.
По необходимости официальная документация Flutter предлагает создавать два метода —
maybeOf(context)
и of(context)
, где первый возвращает nullable экземпляр, а второй — non-nullable.
static CustomInherited? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<CustomInherited>();
}
static CustomInherited of(BuildContext context) {
final result = maybeOf(context);
assert(result != null, 'No CustomInherited found in context');
return result!;
}
Также InheritedWidget обязует нас переопределять метод
updateShouldNotify
. Этот метод возвращает bool значение, которое отвечает за то, должны ли зависимые виджеты пересобираться при изменении данных внутри InheritedWidget.Делитесь своим опытом работы с InheritedWidget в комментариях.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤9❤🔥4🔥3
В предыдущих постах мы уже разбирали распознавание лиц и распознавание текста. Сегодня расскажу про распознавание звука.
Распознавание речи — это один из самых востребованных инструментов в мобильных приложениях:
✔️голосовой ввод текста
✔️создание заметок голосом
✔️управление приложением и голосовыми командами
✔️перевод речи с одного языка на другой
✔️доступность для пользователей с ограниченными возможностями
Что нам понадобится
Для примера будем использовать пакет для преобразования речи в текст в реальном времени — speech_to_text
Добавим в pubspec.yaml:
dependencies:
speech_to_text: ^6.6.0
Настройка и инициализация
1️⃣ Импортируем пакеты:
import 'package:speech_to_text/speech_recognition_result.dart';
import 'package:speech_to_text/speech_to_text.dart';
2️⃣ Создаем переменные:
late SpeechToText _speechToText;
String _recognizedText = '';
3️⃣ Инициализируем в initState:
@override
void initState() {
super.initState();
_speechToText = SpeechToText();
}
4️⃣ Закрываем в dispose:
@override
void dispose() {
_speechToText.cancel();
super.dispose();
}
Логика распознавания речи
Метод для начала прослушивания:
Future<void> _startListening() async {
// Очищаем данные
setState(() {
_recognizedText = '';
});
await _speechToText.listen(
pauseFor: const Duration(seconds: 2),
listenOptions: SpeechListenOptions(
listenMode: ListenMode.dictation,
),
onResult: _onSpeechResult,
);
}
Метод для остановки прослушивания:
Future<void> _stopListening() async {
await _speechToText.stop();
setState(() {});
}
Метод обратного вызова для результата:
void _onSpeechResult(SpeechRecognitionResult result) {
setState(() {
_recognizedText = result.recognizedWords;
});
}
Отображение результата
Полученный текст можно вывести обычным Text:
Text(_recognizedText)
Теперь у нас есть три мощных примера работы с ML Kit и дополнительными библиотеками в Flutter:
▪️распознавание лиц — аутентификация и AR-фичи
▪️распознавание текста — сканирование документов и перевод
▪️распознавание звука — голосовой ввод, заметки и управление приложением
Please open Telegram to view this post
VIEW IN TELEGRAM
❤3🔥3👍2
Anonymous Poll
27%
Распознавание лиц
44%
Распознавание текста
29%
Распознавание звука
❤2🔥1
Привет, это Роза, Flutter Dev Friflex! 👋
В прошлом посте мы с вами разобрались, что такое JWT и зачем он нужен. А сегодня поговорим о том, как же нам реализовать авторизацию при помощи JWT во Flutter.
Для этого нам понадобится библиотека dart_jsonwebtoken, которая позволит нам создавать и верифицировать токены, а также библиотека для безопасного хранения данных на устройстве flutter_secure_storage.
✔️Серверная часть
Создание и подпись токена
Эта логика находится на сервере, который генерирует токен после успешной аутентификации пользователя.
Напомню, JWT состоит из трех частей: header, payload, signature. В коде это можно описать так (создание и подпись):
Библиотека dart_jsonwebtoken автоматически добавляет header и signature, а также поле exp (срок действия) на основе expiresIn.
Также возможно создание ключей по другим алгоритмам (RSA SHA-256, ECDSA P-256, ECDSA secp256k и другим). Для этого необходимо прокинуть key с нужным алгоритмом в sign.
Например:
Проверка токена (верификация)
Чтобы убедиться, что токен подлинный и не был изменен, его нужно верифицировать с тем же секретным ключом.
dart_jsonwebtoken автоматически проверяет срок действия токена и выбрасывает исключение, если он истек.
Отзыв токена: Refresh Token и Blacklist
Основная проблема JWT: даже если пользователь вышел из системы, токен остается валидным до истечения срока действия. Чтобы решить эту проблему, используются короткоживущие Access Token и долгоживущие Refresh Token, а также черный список (Blacklist). Подробнее об этом было в прошлом посте.
Реализовать blacklist вы можете примерно таким образом:
Добавляете таблицу blacklisted_tokens для хранения jti (JWT ID). Далее при генерации токена задаете уникальный ID, например, с помощью пакета uuid
А при проверке учитываете blacklist:
Клиентская часть в комментариях👇
В прошлом посте мы с вами разобрались, что такое JWT и зачем он нужен. А сегодня поговорим о том, как же нам реализовать авторизацию при помощи JWT во Flutter.
Для этого нам понадобится библиотека dart_jsonwebtoken, которая позволит нам создавать и верифицировать токены, а также библиотека для безопасного хранения данных на устройстве flutter_secure_storage.
✔️Серверная часть
Создание и подпись токена
Эта логика находится на сервере, который генерирует токен после успешной аутентификации пользователя.
Напомню, JWT состоит из трех частей: header, payload, signature. В коде это можно описать так (создание и подпись):
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:uuid/uuid.dart';
// Создаем полезную нагрузку (payload) для токена
final payload = {
// jti — уникальный идентификатор токена, необходимый для Blacklist
'jti': const Uuid().v4(),
'userId': 123,
'role': 'user',
};
// Создаем экземпляр JWT
final jwt = JWT(payload);
// Подписываем токен с помощью секретного ключа
// Секретный ключ должен храниться только на сервере и никогда не передаваться на клиент!
// expiresIn задает срок действия токена в секундах
final token = jwt.sign(
SecretKey('ВАШ_СЕКРЕТНЫЙ_КЛЮЧ'),
expiresIn: const Duration(minutes: 15),
);
Библиотека dart_jsonwebtoken автоматически добавляет header и signature, а также поле exp (срок действия) на основе expiresIn.
Также возможно создание ключей по другим алгоритмам (RSA SHA-256, ECDSA P-256, ECDSA secp256k и другим). Для этого необходимо прокинуть key с нужным алгоритмом в sign.
Например:
// Читаем приватный ключ из файла
final pem = File('./rsa_private.pem').readAsStringSync();
final key = RSAPrivateKey(pem);
// Подписываем токен с указанием алгоритма
final token = jwt.sign(key, algorithm: JWTAlgorithm.RS256);
Проверка токена (верификация)
Чтобы убедиться, что токен подлинный и не был изменен, его нужно верифицировать с тем же секретным ключом.
try {
// Верифицируем токен с помощью секретного ключа
final jwt = JWT.verify(token, SecretKey('SECRET_KEY'));
// Если верификация успешна, можно получить полезную нагрузку
print('Payload: ${jwt.payload}');
} on JWTExpiredException {
// Срок действия токена истёк
print('Token expired');
} on JWTInvalidException {
// Токен недействителен (изменён, некорректная подпись)
print('Token is invalid');
}
dart_jsonwebtoken автоматически проверяет срок действия токена и выбрасывает исключение, если он истек.
Отзыв токена: Refresh Token и Blacklist
Основная проблема JWT: даже если пользователь вышел из системы, токен остается валидным до истечения срока действия. Чтобы решить эту проблему, используются короткоживущие Access Token и долгоживущие Refresh Token, а также черный список (Blacklist). Подробнее об этом было в прошлом посте.
Реализовать blacklist вы можете примерно таким образом:
Добавляете таблицу blacklisted_tokens для хранения jti (JWT ID). Далее при генерации токена задаете уникальный ID, например, с помощью пакета uuid
import 'package:uuid/uuid.dart';
final uuid = Uuid();
final payload = {
…,
'jti': uuid.v4(), // Уникальный ID токена
};
А при проверке учитываете blacklist:
bool valid(String secretKey, {Set<String> blackList = const {}}) {
final jwt = JWT.verify(token, SecretKey('SECRET_KEY'));
final jti = jwt.payload['jti'] as String?;
final notBlacklisted = !(jti != null && blackList.contains(jti));
return notBlacklisted;
}
Клиентская часть в комментариях
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7❤6🔥4
Сегодня поговорим про архитектуру — разберемся, чем отличается ephemeral state от app state во Flutter и как с ними работать.
Что такое state?
В самом широком смысле, state — это все, что хранится в памяти приложения во время его работы: шрифты, текстуры, анимации, UI, переменные.
Но управляем мы далеко не всем. Например, за текстуры отвечает сам Flutter. Нам важнее другое определение:
👉 State — это данные, необходимые для перестроения UI в любой момент времени.
Ephemeral state (локальное состояние)
Ephemeral state (еще называют UI state или локальным состоянием) — это данные, которые можно «замкнуть» в рамках одного виджета.
Примеры:
▫️текущая страница в PageView
▫️выбранный таб в BottomNavigationBar
▫️прогресс анимации
Такое состояние:
✔️не нужно сохранять между сессиями
✔️не нужно шарить по всему приложению
✔️меняется локально
Обычно для него достаточно StatefulWidget + setState().
Пример с BottomNavigationBar:
class MyHomepage extends StatefulWidget {
const MyHomepage({super.key});
@override
State<MyHomepage> createState() => _MyHomepageState();
}
class _MyHomepageState extends State<MyHomepage> {
int _index = 0;
@override
Widget build(BuildContext context) {
return BottomNavigationBar(
currentIndex: _index,
onTap: (newIndex) {
setState(() {
_index = newIndex;
});
},
// ... items ...
);
}
}
Здесь _index — это ephemeral state. Он живет только в этом экране и спокойно сбрасывается при перезапуске приложения.
App state (глобальное состояние)
App state (или shared state) — это данные, которые:
▫️нужны разным экранам
▫️должны сохраняться между сессиями
▫️определяют ключевую бизнес-логику
Примеры:
▪️данные пользователя (логин, токен)
▪️настройки и предпочтения
▪️корзина в e-commerce
▪️уведомления или список непрочитанных статей
Для управления app state часто используют:
✔️Provider / Riverpod — декларативный state management
✔️Redux / BLoC — для сложных приложений
✔️Hive / SharedPreferences — для сохранения на диск
Нет универсального правила. Важно понимать: граница между ephemeral и app state условная.
Например, выбранный таб в BottomNavigationBar может быть ephemeral state (если он нужен только внутри экрана), но стать app state, если:
▪️нужно восстанавливать его при повторном входе
▪️он влияет на другие части приложения
Автор Redux, Дэн Абрамов, говорил:
«The rule of thumb is: Do whatever is less awkward»
То есть используйте то решение, которое проще и естественнее именно в вашем случае.
Если кратко:
Во Flutter есть два типа состояния:
✔️Ephemeral state — локальный, простой, живет в одном виджете, управляется через setState()
✔️App state — общий, сложный, нужен разным экранам и сохраняется между сессиями
Идеальный подход — гибко сочетать оба вида в зависимости от задач.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤6👍4🔥3
В предыдущем посте мы вспомнили, что такое InheritedWidget. Сегодня продолжаю отвечать на вопрос нашего подписчика о том, что такое Provider и как он работает.
Библиотека provider — одна из наиболее известных во Flutter-сообществе. Она дает доступ к специальному виджету Provider и его вариациям, которые сами разработчики пакета позиционируют как более удобную версию InheritedWidget:
A wrapper around InheritedWidget to make them easier to use and more reusable.
Здесь важно понять, что Provider работает по тому же принципу, что и InheritedWidget — данные прокидываются по дереву виджетов, а доступ к ним в любой момент можно получить по контексту.
Передача данных
Provider позволяет удобно передавать данные дальше по дереву виджетов. Как и InheritedWidget, его необходимо встраивать в дерево выше тех мест, которые будут использовать передаваемые данные.
Реализовать это можно двумя способами.
1. С помощью стандартного конструктора
Provider()
Здесь создание источника данных выполняется через функцию create.
Provider(
create: (context) => CustomData(),
child: Container(),
),
2. С помощью конструктора
Provider.value()
Этот конструктор не будет создавать новый экземпляр источника данных — он требует передачу уже ранее созданного.
final customData = CustomData();
...
Provider.value(
value: customData,
child: Container(),
),
При необходимости провайдить несколько источников данных можно использовать MultiProvider.
Кроме этого библиотека дает доступ к множеству разных видов провайдера, например, StreamProvider и FutureProvider.
Чтение данных
Далее по дереву от экземпляра провайдера мы можем получить переданные данные через контекст. Для этого существует три метода:
1.
context.read<T>(
) — однократно считывает данные 2.
context.watch<T>()
— прослушивает данные, при их изменении обновляет текущий виджет3.
context.select<T, R>(R cb(T value))
— прослушивает только указанную часть данных, например, одно полеБиблиотека также дает доступ к виджетам Consumer и Selector. Они так же, как методы
context.watch
и context.select,
прослушивают изменения данных провайдера, но позволяют ребилдить не весь текущий виджет, а только дочерние.
class Example extends StatelessWidget {
const Example({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
const Text('Строка 1'),
Text(context.watch<CustomData>().data), // заставит перестроиться весь виджет Example
const Text('Строка 3'),
],
);
}
}
class Example2 extends StatelessWidget {
const Example2({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
const Text('Строка 1'),
Consumer<CustomData>( // перестроит только дочерний Text
builder: (context, customData, _) {
return Text(customData.data);
},
),
const Text('Строка 3'),
],
);
}
}
Еще больше полезной информации можно найти в документации классов библиотеки, а также в README.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤8🔥5❤🔥1
Недавно от вас был запрос разобрать SliverPersistentHeader. Но прежде чем углубиться в него, давайте вспомним, что такое slivers и каких видов они бывают.
Этот пост я решила разделить на две части:
✔️в этой — обзор и базовая теория
✔️в следующей — разберем на практике, как менять прозрачность и расположение элементов при скролле
Начнем!
Что такое Sliver?
Sliver — это часть прокручиваемой области с гибко настраиваемым поведением.
В отличие от стандартных виджетов вроде ListView, GridView или Column, которые создают обычный прокручиваемый список, Sliver-виджеты работают в связке с CustomScrollView.
Благодаря этому можно собрать кастомную прокрутку: закрепленные заголовки, анимированные списки или коллапсирующий AppBar.
Об этом как раз писали в посте.
Чтобы наполнить CustomScrollView элементами, можно использовать разные виды Sliver. Например:
▪️SliverAppBar: AppBar с поддержкой скролла и анимаций. Он может фиксироваться, а также изменять свою высоту и внешний вид при прокрутке
▪️SliverToBoxAdapter: позволяет разместить обычный виджет внутри CustomScrollView
▪️SliverFillRemaining: заполняет все оставшееся пространство в области просмотра
▪️SliverList: создает список элементов
▪️SliverFixedExtentList: список с фиксированной высотой элементов. Он более производителен, чем SliverList, когда все элементы имеют одинаковый размер
SliverFixedExtentList(
itemExtent: 50,
delegate: SliverChildListDelegate(items),
)
▪️SliverPrototypeExtentList: высота задается прототипом (удобно, когда элементы одинаковой высоты, но зависят от контента)
▪️SliverVariedExtentList: позволяет элементам иметь разные размеры
▪️SliverReorderableList: список, в котором можно менять порядок элементов
▪️SliverAnimatedList: список с анимацией вставки/удаления
▪️SliverFillViewport: каждый элемент заполняет всю область просмотра. Размер элементов можно регулировать с помощью viewportFraction
▪️SliverGrid: отображает элементы в виде сетки
▪️SliverAnimatedGrid: SliverGrid с анимацией вставки и удаления
SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
delegate: SliverChildListDelegate(items),
)
Управление расположением и отступами
Для управления расположением Sliver-виджетов используются:
▫️SliverPadding: добавляет отступы для другого Sliver-виджета
SliverPadding(
padding: const EdgeInsets.all(50),
sliver: SomeSliver(),
)
▫️SliverSafeArea: добавляет отступы от системных областей UI
▫️SliverConstrainedCrossAxis: ограничивает размер дочернего Sliver-виджета по поперечной оси
▫️SliverMainAxisGroup: позволяет объединить несколько Sliver-виджетов подряд. В отличие от SliverList, этот виджет подходит для элементов разной высоты
SliverMainAxisGroup(
slivers: [Sliver1(), Sliver2()],
)
▫️SliverCrossAxisExpanded: распределяет пространство по flex-коэффициенту
Делегаты
Говоря о Sliver-списках, нельзя не упомянуть о делегатах, которые определяют, как и когда создавать элементы:
✔️SliverChildListDelegate: используется для фиксированного списка виджетов
✔️SliverChildBuilderDelegate: лениво создает элементы через builder (для длинных списков)
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(title: Text(items[index])),
childCount: items.length,
),
)
✔️SliverGridDelegateWithFixedCrossAxisCount: сетка с фиксированным количеством колонок
✔️SliverGridDelegateWithMaxCrossAxisExtent: адаптивная сетка с максимальной шириной элемента
Продолжение в комментариях👇
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5❤3🔥3
Что такое Feature Flags?
Feature Flags — это техника управления функционалом приложения, при которой фичи можно включать или выключать без изменения кода и деплоя.
Как работает?
В коде фича окружена проверкой флага:
if (featureFlags.isEnabled('newPaymentFlow')) {
showNewPaymentScreen();
} else {
showOldPaymentScreen();
}
Флаги хранятся локально, в конфигурации или на сервере.
Плюсы
✅Позволяет постепенно внедрять новые функции
✅Упрощает A/B тесты и эксперименты
✅Можно быстро отключить проблемную функцию без деплоя
Минусы
➖Сложность управления множеством флагов
➖Код может становиться запутанным из-за множества условий
➖Требует дисциплины — старые флаги нужно удалять
И небольшой вопрос на засыпку
Please open Telegram to view this post
VIEW IN TELEGRAM
❤9🔥3👍2
После релиза обнаружен баг в новой фиче «чаты между пользователями». Как лучше всего поступить с feature flags?
Anonymous Quiz
7%
Удалить весь код фичи и выпустить хотфикс
86%
Отключить фичу через Feature flag для всех пользователей
1%
Оставить баг, пока команда не напишет новый релиз
7%
Временно скрыть кнопку «чаты» через CSS
👍4❤2🔥2👎1
Сегодня поговорим про виджеты для обработки нажатий и жестов на экране во Flutter-приложениях. Самые распространенные — GestureDetector, InkWell и Listener. Разберем, чем они отличаются и в каком случае лучше использовать тот или иной виджет.
GestureDetector — самый универсальный виджет. Он имеет широкий спектр отслеживания жестов на экране. С его помощью можно обработать как простые одинарные или двойные нажатия (onTap, onDoubleTap), так и более сложные действия, например, свайпы и жесты масштабирования (onHorizontalDrag, onVerticalDrag, onPan, onScale).
Кроме того, что GestureDetector дает возможность отслеживать жесты, некоторые колбэки также возвращают объекты дополнительных сведений о манипуляции пользователя. Например, колбэк
onHorizontalDragUpdate
вызывается при горизонтальном свайпе и дает доступ к объекту DragUpdateDetails
. В нем содержатся данные о позиции указателя и координатах смещения. Это может быть полезно, когда необходимо отслеживать, где именно был выполнен свайп. Визуально виджет никак не реагирует на жесты и не требует обязательной обертки в Material-виджет.
GestureDetector(
onTap: () => print('Tapped!'),
onLongPress: () => print('Long Pressed!'),
onScaleUpdate: (details) => print(
'Scale Updated: ${details.scale}',
),
onHorizontalDragUpdate: (details) => print(
'Drag Updated: ${details.delta}',
),
);
InkWell — наиболее простой виджет из сегодняшнего списка. Он позволяет отслеживать только самые простые нажатия (onTap, onDoubleTap, onLongPress).
Как и GestureDetector, некоторые колбэки возвращают основную информацию о произведенном жесте.
Но в отличие от GestureDetector, отображает анимацию нажатия. Этот визуальный эффект легко кастомизировать. InkWell требует обязательного наличия любого Material-виджета в качестве родителя.
InkWell(
onTap: () => print('Tapped!'),
onTapUp: (details) => print('Tap Up: ${details.localPosition}'),
hoverDuration: const Duration(milliseconds: 100),
highlightColor: Colors.red,
child: Container(
padding: const EdgeInsets.all(16.0),
child: const Text('Tap me!'),
),
);
Listener — низкоуровневый виджет для обработки событий указателя.
Его основное отличие от GestureDetector и InkWell в том, что он не отслеживает сами жесты, не распознает их отличия друг от друга. Он работает непосредственно с указателем, дает полные данные о его поведении.
Как и GestureDetector, виджет не дает никакого визуального отклика.
Listener(
onPointerDown: (event) => print('Pointer Down: ${event.localPosition}'),
onPointerPanZoomStart: (event) => print('Pointer Pan Zoom Start: ${event.localPosition}'),
onPointerMove: (event) => print('Pointer Move: ${event.localPosition}'),
);
Подведем итог — когда же применять каждый из них?
✔️InkWell используйте для обработки самых простых нажатий, когда требуется минимум контроля над действием, а также когда в интерфейсе необходима визуальная анимация жеста.
✔️GestureDetector будет полезен при отслеживании нестандартных жестов, например, свайпов в каком-то конкретном направлении или масштабировании. Также его можно использовать вместо InkWell, когда не требуется показывать никакие визуальные эффекты.
✔️Listener применяйте для самых специфических задач. Он отлично подойдет, когда вам потребуется отслеживать события указателя в нестандартных движениях пользователя. Если GestureDetector не сможет помочь в вашей задаче, Listener точно справится.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤9🔥2👍1💯1
Anonymous Quiz
9%
InkWell
17%
Listener
74%
GestureDetector
❤1
This media is not supported in your browser
VIEW IN TELEGRAM
Пусть код компилируется с первого раза, баги боятся вас, а перерывы на кофе длятся дольше, чем митинги.
Вы — волшебники, которые превращают идеи в работающие продукты. А теперь отложите телефон, суббота — для отдыха
Please open Telegram to view this post
VIEW IN TELEGRAM
❤19🎉3🔥1
This media is not supported in your browser
VIEW IN TELEGRAM
В прошлом посте мы разобрали slivers и зачем они нужны. Сегодня поговорим про SliverPersistentHeader.
SliverPersistentHeader — это виджет, который меняет свою высоту при прокрутке. Его можно использовать, например, для создания сжимающихся шапок в профилях пользователей или в деталях товара.
Так как SliverPersistentHeader сам по себе не знает, как отображать содержимое, он использует делегат — SliverPersistentHeaderDelegate. Он управляет его поведением и внешним видом через:
✔️ build — определяет, как выглядит виджет на каждом этапе прокрутки
✔️ maxExtent — задает максимальную высоту заголовка в его развернутом состоянии
✔️ minExtent — определяет минимальную высоту, до которой заголовок сжимается
✔️ shouldRebuild — условие для перерисовки делегата
Внутри метода build доступны параметры:
▪️ double shrinkOffset — показывает, насколько сильно заголовок сжался (от 0 до maxExtent — minExtent)
▪️ bool overlapsContent — указывает, перекрывает ли содержимое последующие сливеры
Для управления поведением заголовка при прокрутке у SliverPersistentHeader есть параметры pinned и floating:
▫️ pinned: true — шапка остается закрепленной, когда прокрутка достигает минимальной высоты
▫️ floating: true — заголовок появляется сразу, как только пользователь начинает прокручивать страницу вверх, даже если прокрутка еще не дошла до самого верха
Пример использования
Давайте реализуем пример «сжимающейся» шапки профиля, где аватар и заголовок плавно меняют свой размер и положение при скролле.
Шаг 1: Подготовка виджетов
Сначала создадим три вспомогательных виджета, которые будут анимироваться:
1. ColoredContainer для затемнения фона
class ColoredContainer extends StatelessWidget {
const ColoredContainer({super.key, required this.progress});
final double progress;
@override
Widget build(BuildContext context) {
return Opacity(
opacity: progress,
child: const ColoredBox(color: Colors.black54),
);
}
}
2. HeaderTitle для заголовка, который будет смещаться к центру
class HeaderTitle extends StatelessWidget {
const HeaderTitle({super.key, required this.progress});
final double progress;
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: const Duration(milliseconds: 100),
padding: EdgeInsets.lerp(
const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
const EdgeInsets.only(bottom: 16),
progress,
),
alignment: Alignment.lerp(
Alignment.bottomLeft,
Alignment.bottomCenter,
progress,
),
child: Text(
'Profile',
style: TextStyle.lerp(
Theme.of(context).textTheme.titleLarge!.copyWith(color: Colors.white),
Theme.of(context).textTheme.titleMedium!.copyWith(color: Colors.white),
progress,
),
),
);
}
}
Продолжение — в комментариях 👇
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥7👍6❤3
Сегодня расскажу про dart:ffi — интерфейс для вызова нативного кода (C/C++) напрямую из Dart.
Зачем нужен dart:ffi
Обычно для связи Flutter с нативом мы используем MethodChannel. Но бывают задачи, где этот способ слишком громоздкий или недостаточно быстрый. В таких случаях выручает dart:ffi:
✔️доступ к системным API напрямую
✔️использование готовых библиотек на C (SQLite, OpenSSL, TensorFlow Lite и других)
✔️оптимизация производительности тяжелых вычислений
Как это работает
▫️Dart связывается с C через указатели и структуры
▫️Нужные функции подключаются через DynamicLibrary.open()
▫️Типы приходится преобразовывать: Dart ⇄ C
Пример: подключаем простую C-функцию
C-библиотека math.c:
C
// math.c
int sum(int a, int b) {
return a + b;
}
Компиляция в динамическую библиотеку:
▪️Linux/Mac: gcc -shared -o libmath.so math.c
▪️Windows: gcc -shared -o math.dll math.c
Dart-код (main.dart):
import 'dart:ffi';
import 'dart:io';
// Загружаем библиотеку
final dylib = Platform.isWindows
? DynamicLibrary.open("math.dll")
: DynamicLibrary.open("libmath.so");
// Сигнатура функции C
typedef c_sum_func = Int32 Function(Int32, Int32);
typedef dart_sum_func = int Function(int, int);
// Получаем функцию
final sum = dylib.lookupFunction<c_sum_func, dart_sum_func>('sum');
void main() {
print(sum(3, 4)); // 7
}
Основные типы FFI
▪️ Int8, Int16, Int32, Int64 ↔ int
▪️ Float, Double ↔ double
▪️ Pointer<T> — указатель на C-объект
▪️ Struct — структуры данных
Когда использовать
✅ Использовать:
▫️подключение нативных библиотек (sqlite3, opus, zlib)
▫️задачи производительности (машинное обучение, криптография)
▫️системные API iOS/Android
❌ Не использовать:
▫️в обычных Flutter-приложениях (чаще достаточно MethodChannel)
▫️если задачу можно решить чистым Dart или готовым пакетом
❤️ — если было полезно
Please open Telegram to view this post
VIEW IN TELEGRAM
❤17🔥5👍2
При создании мобильных приложений разработчики часто используют фразу «адаптивная верстка». Сегодня поговорим о том, что же это такое и как реализовать.
Адаптивная верстка — это верстка, которая автоматически подстраивается под разные размеры и типы экранов.
При создании мобильного приложения разработчику всегда стоит держать в голове мысль о том, что устройства пользователей могут быть абсолютно разными. Кто-то пользуется смартфоном со стандартным экраном, а кто-то предпочитает огромные планшеты или даже нестандарные складные устройства, по типу Samsung Fold. При всем этом разнообразии верстка никогда не должна ломаться и тем более падать в ошибку, так как это всегда вызывает много негатива со стороны юзеров.
Для поддержания адаптивности верстки могу порекомендовать 5 самых полезных и универсальных виджетов.
1. MediaQuery
Довольно распространенный виджет для получения данных об устройстве — размерах и ориентации экрана, соотношении сторон, плотности пикселей и многом другом. С его помощью вы можете определять и даже рассчитывать размеры других виджетов на странице.
final screenWidth = MediaQuery.sizeOf(context).width;
final aspectRatio = MediaQuery.sizeOf(context).aspectRatio;
final isPortrait = MediaQuery.of(context).orientation == Orientation.portrait;
2. OrientationBuilder
Этот виджет позволяет отслеживать ориентацию вашего экрана и ее изменения. В случае, если пользователь перевернет устройство, виджеты, вложенные в OrientationBuilder будут перестроены. Например, это может быть полезно, когда в вертикальной ориентации на экран нужно вывести карточки в одну колонку, а на горизонтальной — в две.
OrientationBuilder(
builder: (context, orientation) {
return orientation == Orientation.portrait
? OneColumnView()
: TwoColumnsView();
},
)
3. LayoutBuilder
Этот виджет отслеживает изменения размеров родительского виджета (или экрана, если родительский виджет не будет иметь ограничений). Так же, как и OrientationBuilder, при изменении отслеживаемых параметров он будет перестраивать вложенные виджеты.
LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 400) {
return WideLayout();
} else {
return NarrowLayout();
}
},
)
4. AspectRatio
Позволяет задавать дочернему виджету соотношение сторон. Например, у вас стоит задача добавить на экран рекламный баннер, в который будут приходить изображения с сервера. В таком случае, если зафиксировать, допустим, высоту, а ширину оставить по размеру экрана, то картинка будет обрезаться. Здесь оптимальным решением будет добавить AspectRatio с соотношением сторон, совпадающим с соотношением изображения баннера.
AspectRatio(
aspectRatio: 16 / 9,
child: Image.network(src),
)
5. ConstrainedBox
Довольно часто при адаптации верстки под планшеты требуется ограничивать ширину содержимого страницы так, чтобы она не была шире определенного значения. При этом на меньших устройствах содержимое должно быть по ширине экрана. Именно здесь на помощь придет ConstrainedBox. Он позволяет задать минимальные и максимальные размеры дочерних виджетов.
ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 400,
),
child: const Card(child: Text('Hello World!')),
)
P.S. и конечно же не забываем про базовые flex-виджеты — Flexible и Expanded.
Делитесь своими советами по адаптивной верстке в комментариях
Please open Telegram to view this post
VIEW IN TELEGRAM
❤14👍7🔥5🤩1
Если вы хоть раз делали приложение и для Web, и для мобильных платформ, то точно сталкивались с конфликтами библиотек dart:io и dart:html (а точнее, уже dart:web) или других платформенно-специфичных пакетов. Вы могли увидеть примерно такое сообщение в консоли:
Failed to build iOS app
Error (Xcode): lib/main.dart:7:8: Error: Dart library 'dart:html' is not available on this platform.
или, например:
Running Gradle task 'assembleDebug'...
../lib/web_widgets/url_functions_web.dart:19:31: Error: Type 'ui.HashUrlStrategy' not found.
class UrlPathStrategy extends ui.HashUrlStrategy {
^^^^^^^^^^^^^^^^^^
........
И это объяснимо, ведь мы используем пакет, который не поддерживается в мобильном устройстве, все просто и понятно.
Давайте по порядку!
◾️ dart:io — библиотека для работы с файлами, сокетами и прочим «системным» API. Он поддерживается только на мобильных и desktop устройствах.
◾️ dart:html(dart:web) — библиотека для работы с DOM и Web API. Поддерживается только в браузере.
Соответственно, если мы подключаем dart:io в вебе или dart:web на мобильных устройствах, компилятор выдаст ошибку.
Чтобы решить эту проблему, можно:
1. Использовать сторонние кроссплатформенные библиотеки:
Например, cross_file.
Этот пакет предоставляет абстракцию XFile, которая позволяет работать с файлами на всех платформах (Web, Desktop, Mobile).
При помощи cross_file, например, вы можете сделать файловый пикер:
Future<XFile?> showFilePicker({List<String>? extensions}) {
return FilePicker.platform.pickFiles(
allowedExtensions: extensions,
type: extensions?.isNotEmpty ?? false ? FileType.custom : FileType.any,
).then((result) {
return result?.xFiles.firstOrNull;
});
}
2. Делать условные импорты
Если подходящей библиотеки нет, можно подключать разные реализации под разные платформы. Например, для настройки ClientChannelBase для grpc. Подробнее о grpc можете прочитать здесь.
export 'grpc_stub.dart'
if (dart.library.io) 'grpc_io.dart'
if (dart.library.js_interop) 'grpc_web.dart';
При использовании этого файла (импорте его в ваш виджет) Dart сам подставит нужную реализацию в зависимости от платформы:
✔️ если это мобильные или desktop → grpc_io.dart
✔️ если это Web → grpc_web.dart
✔️ а если ни одна из библиотек не доступна → grpc_stub.dart
grpc_stub.dart (заглушка):
import 'package:grpc/grpc_connection_interface.dart';
ClientChannelBase setupChannel(String url) =>
throw UnimplementedError();
grpc_io.dart (Mobile/Desktop):
import 'package:grpc/grpc.dart';
import 'package:grpc/grpc_connection_interface.dart';
ClientChannelBase setupChannel(String url) {
return ClientChannel(
uri.host,
port: uri.port,
options: ChannelOptions(credentials: credentials),
);
}
grpc_web.dart (Web):
import 'package:grpc/grpc_connection_interface.dart';
import 'package:grpc/grpc_web.dart';
ClientChannelBase setupChannel(String url) {
return GrpcWebClientChannel.xhr(uri);
}
А в коде используем :
late final ClientChannelBase channel = setupChannel(baseUrl);
Вот и все! Делитесь в комментах вашими лайфхаками при работе с кроссплатформой!
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10🔥3❤1