Flutter. Много
رفتن به کانال در Telegram
Заказать мобильную разработку: https://amiga.ru//?utm_source=tg Заказать рекламу в канале @amiga_agency_bot Новости Flutter-разработки, дайджесты мероприятий, личный опыт.
نمایش بیشتر3 023
مشترکین
-1324 ساعت
-247 روز
-6430 روز
آرشیو پست ها
3 019
+5
Hola, Amigos! Мы вводим еще одну новую рубрику и раз в месяц мы будем делиться с вами плагинами для Android Studio и VS Code, которыми пользуемся сами.
Сегодня посмотрим на Bloc - плагин от разработчиков одноименной библиотеки. Он позволяет:
• Создавать классы
Bloc, State и Event в пару кликов с уже добавленным под ваше название шаблонным кодом. Для Android Studio также доступно создание с использованием библиотек equatable и freezed;
• Оборачивать виджеты в BlocProvider, BlocListener, BlocBuilder, BlocConsumer и BlocSelector, как мы это делаем с оборачиванием в другие виджеты через плагин Flutter;
• Добавлять шаблонный код для bloc при помощи специализированных snippets.
Делитесь в комментариях, используете ли этот плагин?
P.S. Вы сможете находить посты этой рубрики через #плагины3 019
Hola, Amigos! На связи Павел Гершевич, Mobile Team Lead в Amiga. Сегодня поговорим о том, как защитить свой код от любопытных глаз и сделать реверс-инжиниринг практически невозможным, используя обфускацию.
Обфускация - процесс преобразования кода, чтобы он перестал быть понятным для человека, но его работоспособность не изменилась.
Как обфусцировать сборку?
Делать это, разумеется, необходимо при сборке для публикации. Например, мы собираем Android App Bundle:
flutter build appbundle --obfuscate --split-debug-info=/symbols
Параметр obfuscate запускает саму обфускацию, а уже split-debug-info выдает специальные файлы, которые помогут расшифровать информацию. Для этого мы указываем директорию, куда их нужно сложить.
Как расшифровать Stack Trace?
Для расшифровки также существует команда, в которую нужно будет передать файл Stack Trace и файл для расшифровки.
flutter symbolize \
-i <stack-trace-file> \
-d <obfuscated-symbols-file>
Сам файл расшифровки нужен для нужной нам архитектуры. Например, для Android на arm64 нужен будет файл app.android-arm64.symbols.
Добавляем файл в Crashlytics
Если мы используем Firebase Crashlytcs или любую подобную систему, то и там мы будем видеть нерасшифрованный Stack Trace. Чтобы этого не случалось, нужно выполнить команду для загрузки символов. Для Firebase она такая:
firebase crashlytics:symbols:upload --app=<firebase-app-id> <path-to-symbols>/symbols
Учтите, что Firebase App ID должен еще и соответствовать платформе - отдельно для Android, отдельно для iOS.
Если вы уже автоматически отправляете dSYM файлы для iOS в Crashlytics, то вам ничего не нужно делать, так как символы уже будут загружены.
Делитесь в комментариях, используете ли обфускацию на проектах?3 019
Hola, Amigos! Мы постоянно стремимся уменьшить написание boilerplate кода, поэтому вводим новую рубрику - полезные extensions, где будем рассказывать о небольших кусочках кода, которые спасают нас каждый день. А начинаем мы со
String.
Первое расширение позволит нам ускорить проверку на пустую строку.
extension NullableStringX on String? {
bool get isNullOrBlank => this == null || this.trim().isEmpty();
}
Пример:
final String? s = null;
s.isNullOrBlank; // true
' '.isNullOrBlank // true
'test'.isNullOrBlank // false
Второе позволяет сделать первую букву строки заглавной, а также сделать это для предложений.
extension StringX on String {
String capitalize() => this.trim().isEmpty ? this : '${this.trim()[0].toUpperCase()}${this.trim().substring(1).toLowerCase()}';
String capitalizeSentense() => this.split('.').map((e) => e.capitalize()).join('. ').trim();
}
Пример:
'мое ПрЕдЛоЖеНиЕ РАЗНОГО размера'.capitalize() // Мое предложение разного размера
'три. слова. точка.'.capitalizeSentense() // Три. Слова. Точка.
Еще одно позволяет скопировать в Clipboard для дальнейшей вставки вместо дополнительного создания объектов в коде.
extension StringX on String {
void copyToClipboard() => Clipboard.setData(ClipboardData(text: this));
}
В следующий раз мы разберемся с расширениями, которые помогут нам с валидацией данных и конвертацией.
Рассказывайте в комментариях, используете ли вы extensions на проектах?3 019
Hola, Amigos! На связи Павел Гершевич, Mobile Team Lead в Amiga. Не всё, что нам нужно, упаковано в библиотеки, поэтому иногда мы пишем решения сами. После этого возникает желание завернуть код в пакет и отправить на pub.dev, чтобы облегчить работу другим разработчикам. А может, вы сами создаете полезное SDK и внедряете поддержку Flutter.
И перед публикацией встает вопрос: какую лицензию выбрать? Давайте разбираться.
Сегодня мы рассмотрим лицензии для Open Source проектов, которые чаще всего используют библиотеки под Dart и Flutter - MIT (uuid, flutter_svg), BSD-3-Clause (библиотеки от команд Dart и Flutter) и Apache 2.0 (clock, rxdart).
Все они относятся к категории разрешительных: позволяют использовать, модифицировать и распространять код. Но отличия у них всё-таки есть. Давайте разбираться.
Лицензия MIT запрещает предъявлять иски к авторам. Для ее использования нужно оставить уведомления в коде и файл с текстом самой лицензии. Можно сказать, что она является базовой. Ее стоит выбирать для небольших проектов и примеров кода.
Лицензия BSD-3-Clause максимально похожа на MIT, но в нее добавлен пункт о запрете рекламы от имени автора. Ее можно выбрать, если вы не хотите, чтобы ваше имя использовали для рекламы чужого продукта.
Лицензия Apache 2.0 является самой защищенной, хотя у нее нет полноценного пункта о запрете рекламы от имени автора (только при согласовании), но она максимально защищает патенты. Еще одно отличие - нужно указывать изменения в коде, если мы модифицируем библиотеку. Такую лицензию выбирают, если проект серьезный и имеет патенты.
Еще одно важное отличие - совместимость с лицензиями GPLv3. MIT и BSD-3-Clause имеют совместимость только с GPLv2, поэтому их можно использовать в проектах с GPLv3, но не наоборот. А вот у Apache 2.0 есть полная совместимость.
P.S. В 2024 году мы анонсировали набор библиотек Amiga Dev Screen. Сейчас мы его активно заканчиваем, а также выбрали для него лицензию MIT.
3 019
+4
Hola, Amigos! В прошлой части мы показали как создать свой собственный Snippet в Android Studio. Теперь пришла очередь сделать это в VS Code.
Делитесь в комментариях, какие Snippets для вас были бы самыми полезными?
3 019
+5
Hola, Amigos! Мы все любим ускорять и упрощать написание шаблонного кода. Один из таких методов - Snippets.
Snippet - аббревиатура, которую IDE может преобразовать в код.
Показываем, как это сделать в Android Studio. А в следующей части расскажем про VS Code.
Рассказывайте в комментариях, используете snippets в работе?
3 019
+5
Hola, Amigos! На связи Павел Гершевич, Mobile TeamLead в Amiga. Для того, чтобы наши приложения работали лучше, можно отслеживать их жизненный цикл, чтобы не совершалось лишних действий. Для этого нам нужен
AppLifecycleState, который предоставляет добавление обратных вызовов для изменения состояния. Давайте посмотрим, какие состояния есть у Flutter-приложений:
resumed - стандартное состояние, когда приложение находится в foreground. При переходе в него можно запускать остановленные потоки данных (Stream) и анимации, а также обновлять устаревшие данные.
inactive - наше приложение видно, но пользователь не может с ним работать. Его можно увидеть, когда мы смотрим на все свернутые приложения. Также, например, открытие системного диалогового окна для выдачи разрешения переведет в это состояние. Тут можно остановить анимации. Но учитывайте, это состояние - временное.
hidden - еще одно временное состояние. Наше приложение не видно, но оно еще не полностью на паузе. Оно добавлено в Flutter 3.13 чтобы мы могли сохранить состояние, пока операционная система не порезала ресурсы.
paused - наше приложение полностью в фоне. Основной isolate еще работает, но Flutter Engine уже не занимается рендерингом. В этом состоянии можно отключиться от WebSockets, отменить работу таймеров, прослушку потоков данных. Но из этого состояния ОС может закрыть приложение.
detached - крайнее состояние нашего приложения. Оно показывает, что приложение закрывается либо открывается (холодный запуск).
Рассказывайте в комментариях, а что вы делаете при изменении жизненного цикла приложения?3 019
Hola, Amigos! На связи Павел Гершевич, Mobile TeamLead в Amiga. В каждом приложении мы авторизуем пользователей, но не все встраивают механизмы обновления токенов.
Поэтому мы написали статью о том, как это делается, и поделились нашим опытом. Из нее вы узнаете:
Как работает авторизация запросов на сервере
🔵 Что хранит в себе JWT и как узнать, когда он закончит действовать
🔵 Как работает QueryInterceptor в Dio и чем он отличается от обычного
🔵 3 способа обработки токенов с завершившимся сроком действия
Ссылка на статью на Хабре
Делитесь в комментариях, а как вы поступаете при получении ошибки 401 Unauthorized?
3 019
Hola, Amigos! Продолжаем разбираться с пагинацией. Сегодня рассмотрим, как подключить логику работы к пользовательскому интерфейсу.
Для того, чтобы мы могли вызывать подгрузку новой страницы, нам нужен триггер. В зависимости от функционала можно рассмотреть как навигацию по страницам, в таком случае необходимо будет сбрасывать предыдущие данные, так и реализацию бесконечного списка, который мы и разберем подробнее.
В бесконечном списке страницы грузятся в момент, когда пользователь практически доходит до конца. Для этого нам понадобится
ScrollController, который мы подцепляем к виджету списка, это может быть ListView.builder или ListView.separated. Далее нужно добавить прослушку:
_scrollController.addListener(() {
final currentScroll = _scrollController.position.pixels;
final maxScroll = _scrollController.position.maxScrollExtent;
if (currentScroll >= (maxScroll - 200) && !isLoading) {
… // Вызов функции для подгрузки
}
});
Тут мы получаем текущее и максимальное положение скролла и сравниваем их. Совет - не нужно дожидаться, пока пользователь дойдет до самого конца списка, лучше начинать грузить пораньше, например, когда до максимума осталось 200 пикселей. Тут все зависит от высоты элементов списка и их количества на экране.
Следующий момент - виджет загрузки первой страницы. Каким он будет, зависит от вас. Часто используют CircularProgressIndicator или какой-либо скелет, например, с Shimmer эффектом. Показываем мы его, если находимся в состоянии загрузки данных и у нас пока нет элементов списка:
if (isLoading && items.isEmpty) {
return …
}
И в самом конце, нужно сделать отображение загрузки новой страницы. Для этого мы манипулируем количеством элементов в списке и его последним элементом:
ListView.builder(
conroller: _scrollController,
…
itemCount: items.length + (isLoading && !isLastPage ? 1 : 0),
itemBuilder: (context, index) {
if (index < items.length) {
return … // Ваш элемент списка
}
return … // Ваш индикатор загрузки
},
);
Как и в случае с первоначальной загрузкой, вы можете выбрать то, что вам удобно и подходит.
Делитесь в комментариях, а каким виджетом вы показываете состояние загрузки на ваших проектах?3 019
🎬🎬🎬🎬🎬🎬🎬🎬🎬🎬🎬🎬🎬
📱 Мобильная разработка в ритме города: Day&Night* 2026
Приглашаем мобильных разработчиков на главную конференцию Городских сервисов Яндекса. Саша Аникин расскажет про будущее городов и роботакси, а Кирилл Нейман разберёт техническую архитектуру машины с голосовым управлением.
Всё остальное время займут тематические клубы. Мобильное направление курируют Саша Борисков — руководитель разработки клиентской платформы в Еде и Илья Царев — руководитель разработки в Яндекс Go.
Обсудим:
🔶 Архитектуру супераппов, песочницы и внедрение ИИ-агентов в мобильную разработку.
🔶 Как делегировать нейросетям рутинную часть разработки.
А для души будут клубы музыки и винила и активного образа жизни, где можно выдохнуть и пообщаться с единомышленниками.
🍸 Завершим вечер нашей традиционной вечеринкой до 2 ночи.
🚀 Регистрация открыта — успейте подать заявку!
Все заявки проходят модерацию, обязательно дождитесь обратной связи.
*День и Ночь
3 019
Hola, Amigos! Сегодня мы посмотрим, каким образом мы можем грузить и показывать данные пользователям при помощи пагинации.
Пагинация - способ, который используется для разделения больших данных на небольшие группы (страницы) вместо загрузки всего сразу.
Например, для API в 10000 объектов, вместо огромного массива данных, мы будем запрашивать потихоньку: страница 1 (объекты 1-10), страница 2 (объекты 11-20) и т. д.
Это помогает нам:
• Уменьшить нагрузку на сеть. Меньше данных в запросе - выше скорость его выполнения на сервере и быстрее передача ответа;
• Оптимизировать производительность. Меньше объем данных - меньше занимаемой оперативной памяти, быстрее обработка;
• Улучшить UX. Пользователи будут видеть новые данные практически, а не когда мы получим и распарсим все объекты.
Для того, чтобы правильно сделать пагинацию в мобильном приложении, нужно знать, как она делается на сервере. Тут существует 3 способа:
Page-based (стандартный способ)
Тут мы запрашиваем данные, передавая
page, страницу, и limit - количество элементов. Запрос будет выглядеть примерно так:
GET /posts?page=1&limit=10Offset-based (гибкий способ) Тут запрос осуществляется с указанием сколько элементов нужно пропустить -
offset.
GET /posts?offset=20&limit=10Cursor-based (профессиональный способ) Самый расширяемый метод. Мы запрашиваем через курсор (обычно ID или timestamp последнего полученного объекта) и получаем данные, которые были добавлены в БД после тех данных, которые мы уже знаем.
GET /posts?cursor=50&limit=10Последний способ применяется редко, так как он может не подойти под некоторые случаи. Но у первых двух тоже есть недостаток - может возникнуть дублирование или пропажа данных. С тем, как запрашивать данные, мы определились, теперь нужно понять, какая страница последняя? Тут есть 2 варианта - либо API вернет нам номер последней страницы при запросе, либо на последней странице придет число меньше указанного нами лимита. Теперь поработаем с нашим State Manager. Нам нужны состояния - загрузки, успеха и ошибки. И обязательно хранить в них - список полученных данных, страницу (если применяем page-based или offset-based) и при возможности номер последней страницы. Кто-то делает отдельное состояние для загрузки новой страницы, но можно обойтись и без него. И нам нужно 2 функции - для инициализации экрана, чтобы подгрузить первые данные, и для подгрузки во время пагинации. Для BLoC советуем отбрасывать событие подгрузки, если мы находимся в состоянии загрузки. В следующей части расскажем, как привязать это все к UI. А вы делитесь в комментариях, какой способ пагинации чаще всего используете на своих проектах?
3 019
Hola, Amigos! На связи Павел Гершевич, Mobile Team Lead в Amiga. Сегодня мы поговорим про то, как лучше всего кешировать картинки и в каком качестве их выводить на экран.
Часто случается ситуация, что сервер отдает слишком большую картинку. А уж если их несколько на одном экране - могут возникнуть проблемы с производительностью, да и размер кеша может значительно вырасти.
Как и все, для кеширования и отображения картинок с интернета, мы используем хорошо знакомую всем библиотеку
cached_network_image. Но не все знают, что при помощи нее можно настроить максимальный размер изображения, который будет закеширован и показан на экране. Для этого есть 4 свойства:
maxHeightDiskCache: 300,
maxWidthDiskCache: 300,
memCacheHeight: 300,
memCacheWidth: 300,
Давайте разберемся с ними:
maxHeightDiskCache и maxWidthDiskCache отвечают за кеш, который хранится в постоянной памяти. Уменьшая его размер, мы уменьшаем количество данных на устройстве пользователя.
memCacheHeight и memCacheWidth отвечают за изображение, которое хранится в ОЗУ и выводится пользователю. Делается это при помощи встроенного в Flutter виджета - ResizeImage. Похожие свойства есть и в Image.network - cacheHeight и cacheWidth. Тут мы указываем именно размер виджета, а не изображения.
Теперь давайте рассмотрим, как их использовать. Для того, чтобы правильно подобрать размер, нам нужны: ширина и высота виджета и Device Pixel Ratio - характеристика устройства, которая показывает плотность пикселей. Если размер нам неизвестен либо высчитывается автоматически, то можно прибегнуть к получению constraints через LayoutBuilder. В итоге у нас получается такой код:
LayoutBuilder((context, constraints) {
final devicePixelRatio = MediaQuery.devicePixelRatioOf(context);
final cacheHeight = constraints.maxHeight * devicePixelRatio;
final cacheWidth = constraints.maxWidth * devicePixelRatio;
return CachedNetworkImage(
…
maxHeightDiskCache: cacheHeight,
maxWidthDiskCache: cacheWidth,
memCacheHeight: constraints.maxHeight,
memCacheWidth: constraints.maxWidth,
…
);
});
Делитесь в комментариях, пробовали ли вы такой способ оптимизации производительности приложения?3 019
Hola, Amigos! Сегодня обсудим pattern matching. Он сокращает boilerplate, улучшает код и повышает безопасность работы со state.
1. Деструктуризация с помощью паттернов
Пример с Records:
var user = ('Naman', 29);
var (name, age) = user;
print(name);
print(age);
2. Деструктуризация объектов
Есть класс:
class User {
final String name;
final int age;
User(this.name, this.age);
}
Можно извлечь свойства так:
var User(:name, :age) = user;Это эквивалентно:
var name = user.name;
var age = user.age;
3. Pattern Matching в switch
Традиционный подход:
if (state is Success) {
final data = state.data;
}
Подход с pattern matching
switch (state) {
case Success(data: var data):
render(data);
case Loading():
showLoader();
case Error(message: var msg):
showError(msg);
}
4. Сопоставление коллекций
Паттерны умеют анализировать списки и map:
switch(list) {
case []:
print("Empty");
case [first, second]:
print(first);
}
switch(json) {
case {'status': 'ok', 'data': var d}:
process(d);
case {'status': 'error', 'message': var m}:
showError(m);
}
5. Guard-условия
switch(user) {
case User(age: var age) when age > 18:
print("Adult");
case _:
print("Minor");
}
А вы используете pattern matching?3 019
Hola, Amigos! Наш Flutter Team Lead Павел Гершевич примет участие в круглом столе на FlutterConf 🙂
Запускаешь новый проект — и снова тот же вопрос: какой state manager выбрать? Проверенный временем? Популярный в комьюнити? Или тот, который сейчас чаще всего советуют?
На FlutterConf вынесли тему на открытое обсуждение с аргументами и практическим опытом. Обсудим реальные кейсы, плюсы и минусы подходов, гибкость Flutter и попробуют понять, возможен ли тот самый «золотой стандарт».
В обсуждении участвуют:
— Павел Гершевич, Team Lead Flutter Amiga
— Анна Жаркова, руководитель группы разработки Usetech
— Олег Скирюк, Frontend Team Lead билайн
— Станислав Чернышев, доцент СПбГУАП, автор книги «Основы Dart»
— Николай Омётов, руководитель Flutter-отдела Mad Brains
— Андрей Смирнов, Dart Dependant Overthinking Specialist Яндекс
— Федор Благодырь, разработчик Яндекс
⚙️ Билеты уже в продаже. Круглый стол «Я твой State Manager труба шатал»
⚙️ Москва, 3-я ул. Ямского Поля, 26А
⚙️ 27 февраля, 18:05–19:20
⚙️ Зал 1
А с промокодом FLUTTER_MNOGO вас ждет скидка 20%
3 019
Запланируйте 2026 год вместе с Merge
В 2025 году под эгидой Merge прошло сразу четыре события для профессионального IT-сообщества, Мероприятия объединили более 6000 участников и свыше 500 спикеров.
Сейчас Merge строит планы на 2026 год. И приглашает всех присоединиться.
Merge Innopolis | 17–18 апреля
В 2026 году инновационный центр Поволжья, город Иннополис, готовится принять Merge в пятый раз. Все ключевые направления IT на одной площадке. 200+ спикеров, 2000+ участников, 10 потоков докладов по 7 ключевым трекам: «Разработка», «Управление», «Маркетинг», «Аналитика», «HR», «1С», «Карьера в IT».
Summer Merge | 3–5 июля
Самая летняя IT-тусовка на берегу Волги, неформальный нетворкинг и лучший палаточный уикенд для IT-специалистов.
Merge Baltic | 25–27 сентября
Профессиональная конференция Merge соберет представителей всех направлений сферы IT на берегу Балтийского моря. Здесь привычную деловую программу и нетворкинг расширит тревел-трек и экскурсии по удивительным местам Калининградской области.
По промокоду AMIGA действует скидка 10% на билеты. Успейте присоединиться до повышения цены.
Присоединяйтесь!
3 019
Hola, Amigos! Async/await и Isolate оба помогают держать UI плавным, но решают разные задачи. Сегодня обсудим, что и в каких ситуациях лучше использовать.
Неправильно: async/await для CPU-тяжелой задачи
Future<String> processBigData() async {
final raw = await fetchHugeJson(); // ← I/O — ок
final data = jsonDecode(raw); // ← блокирует UI ~1с
final result = heavyMath(data); // ← ещё ~1с
return result;
}
Правильно и просто: compute()
import 'package:flutter/foundation.dart';
Future<String> processBigData() async {
final raw = await fetchHugeJson(); // сеть — ок
return compute(_heavyWork, raw); // ← выполняется в Isolate
}
String _heavyWork(String json) {
final data = jsonDecode(json);
return heavyMath(data);
}
Когда нужен полный контроль, подойдет Raw Isolate
Future<void> startWorker() async {
final receivePort = ReceivePort();
isolate = await Isolate.spawn(_worker, receivePort.sendPort);
sendPort = await receivePort.first as SendPort;
}
Future<String> doHeavyTask(String input) async {
final response = ReceivePort();
sendPort.send([input, response.sendPort]);
return await response.first as String;
}
static void _worker(SendPort mainPort) async {
final port = ReceivePort();
mainPort.send(port.sendPort);
await for (final msg in port) {
final input = msg[0] as String;
final replyTo = msg[1] as SendPort;
replyTo.send("processed: $input"); // heavy work here
}
}
Разберем по ситуации:
- Блокирует main thread > ~300 мс → compute()
- Просто ждет (network/db) → async/await
- Нужен worker с диалогом → raw Isolate
А ты пользуешься Isolate/compute или решаешь через async/await? Делись опытом ⚙️3 019
Hola, Amigos! Небольшое обновление 🙂
Мы запустили отдельный канал нашей компании. Теперь все новости агентства, анонсы мероприятий и выступлений, кейсы, а также инсайты про рынок разработки будут выходить там.
Этот канал, как и раньше, остается про Flutter ❤️
3 019
Repost from Amiga
Hola, Amigos!
Совсем скоро нам исполнится 5 лет, и мы наконец-то создали этот канал. Сначала хотим к юбилею отрефлексировать все взлеты и падения, а позже будем делиться интересными инсайтами, анонсами мероприятий и выступлений, мыслями о продуктах и рынке.
Уже пять лет мы разрабатываем приложения и сайты — от идеи до выхода продукта на рынок и первых пользователей. Работаем со всем циклом: от аналитики и дизайна до разработки, поддержки и усиления команд. Были и удачные запуски, и сложные решения, и, конечно, ошибки, из которых потом вырастали сильные продукты.
Весь февраль будем рассказывать о себе и о том, как мы работаем ⚙️
Велкам!
3 019
Hola, amigos! Сегодня разберем подборку Flutter-инструментов, которые прокачивают взаимодействие с пользователем.
1. Dismissible позволяет легко реализовать логику swipe-to-delete.
Dismissible(
key: Key(item.id),
background: Container(color: Colors.red),
onDismissed: (direction) => deleteItem(item.id),
child: ListTile(
title: Text("Swipe me to delete"),
),
)
2. Tooltip показывает краткую подсказку при долгом нажатии (mobile) или наведении (web/desktop).
Tooltip(
message: 'Download PDF',
child: IconButton(
icon: Icon(Icons.download),
onPressed: () {},
),
)
3. Draggable подходит, чтобы реализовать drag-and-drop, как в Trello или корзине интернет-магазина.
Draggable<Color>(
data: Colors.blue,
feedback: Container(
height: 100,
width: 100,
color: Colors.blue.withOpacity(0.5),
),
childWhenDragging: Container(
height: 100,
width: 100,
color: Colors.grey,
),
child: Container(
height: 100,
width: 100,
color: Colors.blue,
),
);
4. ReorderableListView идеально подходит для настроек, плейлистов и любых кастомных списков.
ReorderableListView(
onReorder: (oldIndex, newIndex) {
setState(() {
if (newIndex > oldIndex) newIndex -= 1;
final item = list.removeAt(oldIndex);
list.insert(newIndex, item);
});
},
children: list
.map(
(item) => ListTile(
key: ValueKey(item),
title: Text(item.toString()),
),
)
.toList(),
);
А какими виджетами для UX чаще всего пользуетесь вы? Делитесь в комментариях.
اکنون در دسترس! پژوهش تلگرام ۲۰۲۵ — مهمترین بینشهای سال 
