Flutter Pulse
613 subscribers
379 photos
831 links
На канале будут новости про flutter с сайтов, информация об обновлении пакетов, а также авторский контент.
Download Telegram
Изменение яркости системной панели
Отобразить системную панель с правильной яркостью на iOS и Android 🚀

При разработке мобильных приложений важно обеспечить корректное отображение системной панели на разных платформах. В этой статье мы рассмотрим, как изменить яркость системной панели в приложениях Flutter для iOS и Android.

Код для изменения яркости системной панели:

SystemChrome.setSystemUIOverlayStyle(
SystemUIOverlayStyle(
statusBarColor: Colors.transparent, // Прозрачный цвет статус-бара
statusBarBrightness: // Установка яркости статус-бара в зависимости от темы
mode == ThemeMode.light ? Brightness.light : Brightness.dark,
statusBarIconBrightness: // Установка яркости иконок статус-бара в зависимости от темы
mode == ThemeMode.light ? Brightness.dark : Brightness.light,
),
);


Обратите внимание:
- statusBarBrightness применяется только на Android.
- statusBarIconBrightness также применяется только на Android, но с инвертированными значениями для iOS.

Особенности для iOS: 🤔
На iOS строка состояния может быть изначально скрыта. Чтобы отобразить ее, откройте ваш проект в Xcode и убедитесь, что свойство status bar initially hidden не отмечено.

👍 Оцените нашу новую рубрику советов по Flutter! Ваши отзывы помогут нам сделать контент еще лучше.

Все подобные новости можно найти по хэштегу #FlutterPulseTips

#flutter #dart #flutterpulse #FlutterPulseTips #MobileDev #iOS #Android #FlutterTips #DevTips
👍2
Рисуем и анимируем круглый прогресс-бар с помощью Custom Painter

В этом совете мы рассмотрим, как создать анимированный круглый прогресс-бар, используя виджет CustomPaint во Flutter. Такой прогресс-бар можно использовать, например, для индикации загрузки.

Основные моменты:

1️⃣ Передача анимации в конструктор CustomPainter автоматически вызывает перерисовку.

2️⃣ При обновлении прогресса мы изменяем начало и конец анимации, а затем запускаем её. Поскольку время загрузки предсказать невозможно, анимация прогресс-бара продолжается после обновления прогресса.

Пример кода:



class RoundProgressPainter extends CustomPainter {
final double radius;
final double progress;
final Color color;
final Animation<double> animation;
final double strokeWidth;

RoundProgressPainter({
required this.radius,
required this.progress,
required this.color,
required this.animation,
required this.strokeWidth,
}) : super(repaint: animation);

@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final progressAngle = math.pi * 2 * progress;
final progressPaint = Paint()
..color = color
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;

canvas.drawCircle(
center,
radius - strokeWidth / 2,
Paint()
..color = color.withOpacity(0.1)
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke,
);

canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
-math.pi / 2,
progressAngle,
false,
progressPaint,
);
}

@override
bool shouldRepaint(RoundProgressPainter oldDelegate) =>
progress != oldDelegate.progress || color != oldDelegate.color;
}





class RoundProgress extends StatefulWidget {
final double radius;
final double progress;
final Color color;
final Widget? child;
final double strokeWidth;

const RoundProgress({
super.key,
required this.radius,
required this.progress,
required this.color,
this.child,
this.strokeWidth = 4.0,
});

@override
_RoundProgressState createState() => _RoundProgressState();
}

class _RoundProgressState extends State<RoundProgress> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;

@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
_animation = Tween(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.decelerate),
);
_controller.forward(from: 0);
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

@override
void didUpdateWidget(covariant RoundProgress oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.progress != oldWidget.progress && widget.progress != null) {
_animation = Tween(begin: oldWidget.progress, end: widget.progress).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeIn),
);
_controller.forward(from: 0);
}
}

@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) => CustomPaint(
size: Size(widget.radius * 2, widget.radius * 2),
painter: RoundProgressPainter(
radius: widget.radius,
progress: widget.progress,
color: widget.color,
animation: _animation,
strokeWidth: widget.strokeWidth,
),
child: widget.child,
),
);
}
}



Оцените новую рубрику и напишите в комментариях, что вы хотели бы увидеть в следующих постах! 👍💬

Все подобные новости можно найти по хэштегу #FlutterPulseTips

#flutter #dart #flutterpulse #FlutterPulseTips #MobileDevelopment #UI #Animation #CustomPainter #ProgressBar #LoadingAnimation #FlutterTips
👍2
Тестирование с навигацией GoRouter
Как запускать тесты с навигацией GoRouter

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

Пример теста:



testWidgets('условие защиты возвращает false => переход на ошибочный url',
(WidgetTester tester) async {
final app = MaterialApp.router(
routerConfig: GoRouter(
initialLocation: '/page1',
routes: [
GoRoute(
path: '/page1',
builder: (context, state) => Guard(
canActivate: future.value(false),
fallbackRoute: '/page2',
),
child: const FakePage(msg: 'page1'),
),
GoRoute(
path: '/page2',
builder: (context, state) => const FakePage(msg: 'page2'),
),
],
),
);

await tester.pumpWidget(app);
await tester.pumpAndSettle(const Duration(milliseconds: 100));

expect(find.text('page2'), findsOneWidget);
});



Дополнительный совет:
Вы можете использовать ваш app router, чтобы получить точно такое же поведение маршрутизации, как и в вашем приложении.
Таким образом, вы действительно можете проверить, работает ли навигация корректно.
Встройте создание GoRouter в функцию с параметром initialLocation, чтобы ваш тест мог начинаться прямо с того места, которое вам нужно.



GoRouter createRouter(final String initialLocation) {
return GoRouter(
initialLocation: initialLocation,
routes: [
GoRoute(
path: '/page1',
builder: (context, state) => const FakePage(msg: 'page1'),
),
GoRoute(
path: '/page2',
builder: (context, state) => const FakePage(msg: 'page2'),
),
],
);
}



Оцените новую рубрику по тестированию Flutter-приложений! 👍
Все подобные новости можно найти по хэштегу #FlutterPulseTips
#flutter #dart #flutterpulse #FlutterPulseTips #MobileAppDevelopment #FlutterTips #Testing #GoRouter
2👍2
Как протестировать Изолят
Запуск функции изолята в модульных тестах

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

Рассмотрим пример неправильного тестирования изолята:


testWidgets('upload file and save avatar', (tester) async {
final file = await rootBundle.load('assets/images/splashscreen.png');
final bytes = file.buffer.asUint8List();
final xfile = XFile.fromData(bytes);

final jpgData = await compute(_avatarThumbnail, file);
}

// Функция, выполняющаяся в изоляте
Future<Uint8List> _avatarThumbnail(XFile file) {
return file.toJpeg(file, 300, 80);
}


Такой тест зависнет и никогда не завершится, поскольку функция compute запускает _avatarThumbnail в изоляте, но тест не ожидает завершения изолята.

Решение: Используйте tester.runAsync() для запуска асинхронного кода в тесте:


testWidgets('upload file and save avatar', (tester) async {
await tester.runAsync(() async {
final file = await rootBundle.load('assets/images/splashscreen.png');
final bytes = file.buffer.asUint8List();
final xfile = XFile.fromData(bytes);

final jpgData = await compute(_avatarThumbnail, file);
});
});

// Функция, выполняющаяся в изоляте
Future<Uint8List> _avatarThumbnail(XFile file) {
return file.toJpeg(file, 300, 80);
}


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

Оцените новую рубрику и напишите своё мнение! 👍💬

Все подобные новости можно найти по хэштегу #FlutterPulseTips
#flutter #dart #flutterpulse #FlutterPulseTips #MobileDevelopment #Testing #Isolate #FlutterTips #DartTips
👍1
Добавьте кастомные переходы страниц с GoRouter

Переходы между страницами важны для пользовательского опыта

Добавьте этот небольшой помощник, чтобы определить кастомный переход страницы:


Page<dynamic> Function(BuildContext, GoRouterState) buildPageTransition(
Widget child,
) => (BuildContext context, GoRouterState state) {
return CustomTransitionPage(
key: state.pageKey,
child: child,
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
FadeThroughTransition( // можно создать свой собственный переход или использовать из пакета animations на pub.dev
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
),
);
};


Теперь оберните свою страницу в переход, используя pageBuilder для вашего маршрута:


GoRoute(
path: '/signin',
builder: (context, state) => const SigninPage(),
pageBuilder: buildPageTransition(const SigninPage()),
),


Вы также можете настроить стандартный переход страницы прямо в вашей теме:


pageTransitionsTheme: PageTransitionsTheme(
builders: {
TargetPlatform.android: const ZoomPageTransitionsBuilder(),
TargetPlatform.iOS: const CupertinoPageTransitionsBuilder(),
},
),


Оцените нашу новую рубрику! 👍💡
Все подобные новости можно найти по хэштегу #FlutterPulseTips
#flutter #dart #flutterpulse #FlutterPulseTips #MobileDev #AppDev #UIUX #FlutterTips
👍5
Создание последовательных анимаций
Создайте пользовательскую цепочку эффектов, чтобы повторно использовать ее во всем приложении с помощью flutter_animate.

Создайте виджет с Animate и списком эффектов


import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';

class MoveFadeAnim extends StatelessWidget {
final int? delayInMs;
final Widget child;

const MoveFadeAnim({
super.key,
required this.child,
this.delayInMs,
});

@override
Widget build(BuildContext context) {
return Animate(
effects: [
// Эффект затухания с задержкой и продолжительностью 700 мс
FadeEffect(
delay: Duration(milliseconds: delayInMs ?? 0), // Задержка перед началом анимации
duration: const Duration(milliseconds: 700), // Продолжительность анимации
curve: Curves.easeIn, // Кривая анимации для эффекта затухания
),
// Эффект перемещения с задержкой и продолжительностью 450 мс
MoveEffect(
delay: Duration(milliseconds: delayInMs ?? 0), // Задержка перед началом анимации
duration: const Duration(milliseconds: 450), // Продолжительность анимации
curve: Curves.easeOut, // Кривая анимации для эффекта перемещения
begin: const Offset(0, 50), // Начальное смещение
end: Offset.zero, // Конечное смещение (нет смещения)
),
],
child: child, // Дочерний виджет, к которому применяются эффекты
);
}
}


Повторно используйте свою анимацию везде в приложении для последовательного поведения анимации


return MoveFadeAnim(
delayInMs: index * 150 + 50, // Вычисление задержки на основе индекса элемента
child: MenuCard(
height: 130, // Высота карточки меню
),
);


Оцените новую рубрику! 👍💡
Все подобные новости можно найти по хэштегу #FlutterPulseTips
#flutter #dart #flutterpulse #FlutterPulseTips #MobileAppDevelopment #Animation #UIUX #FlutterTips
👍2
Создаём кастомную панель приложений
Потому что вы можете 😉

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



class EditorAppBar extends StatelessWidget implements PreferredSizeWidget {
const EditorAppBar({
super.key,
});

@override
Widget build(BuildContext context) {
return Row(...); // Здесь вы можете настроить свой собственный дизайн
}

@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
// Используйте эту константу для рекомендуемой высоты
}



Чтобы использовать эту кастомную AppBar, просто передайте её в свойство appBar виджета Scaffold:


child: Scaffold(
appBar: EditorAppBar(),
...
),



Оцените нашу новую рубрику по Flutter советам 👍! Все подобные новости можно найти по хэштегу #FlutterPulseTips
#flutter #dart #flutterpulse #FlutterPulseTips #MobileDevelopment #CustomAppBar #AppBarDesign #FlutterTips
👍31
Загрузка байтов изображения и отображение на холсте
Не так сложно сделать с помощью Flutter 😉

Вы когда-нибудь задумывались, как можно загрузить изображение и отобразить его на холсте в Flutter? 🤔 Давайте разберемся в этом вместе! 💡

Шаг 1: Загрузка изображения
Сначала нам нужно загрузить изображение из наших ресурсов. Для этого мы используем метод load класса rootBundle:


import 'dart:ui' as ui;
import 'package:flutter/services.dart';

// Загружаем изображение из ресурсов
final ByteData data = await rootBundle.load(path);
// Преобразуем в Uint8List
final Uint8List bytes = data.buffer.asUint8List();
// Преобразуем в ui.Image
final ui.Image image = await decodeImageFromList(bytes);



Шаг 2: Отображение на холсте
Теперь, когда у нас есть изображение в формате ui.Image, мы можем отобразить его на холсте. Для этого создадим собственный класс CustomPainter:


// Создаем новый CustomPainter
class MyPainter extends CustomPainter {
final ui.Image image;

MyPainter(this.image);

@override
void paint(Canvas canvas, Size size) {
// Рисуем изображение на холсте
canvas.drawImageRect(image, srcRect, dstRect, Paint());
}
}



Вот и все! Теперь вы знаете, как загрузить байты изображения и отобразить их на холсте во Flutter. Просто и эффективно, не правда ли? 😊

Оцените нашу новую рубрику и оставьте свои комментарии! 👇

Все подобные новости можно найти по хэштегу #FlutterPulseTips
#flutter #dart #flutterpulse #FlutterPulseTips #mobiledev #appdev #codingtips #FlutterTips
👍3🔥1
Локальная база данных
Когда вы хотите, чтобы ваше приложение работало офлайн 🔄

Шаг 1: Использование пакета Drift
Для работы с локальной базой данных мы будем использовать пакет Drift. 📦


dart pub add drift
dart pub add drift_flutter
dart pub add drift_dev



Шаг 2: Создание базы данных


@DriftDatabase(tables: [TaskTable])
class Database extends $Database {
Database([QueryExecutor? e]) : super(e ?? driftDatabase(
name: 'todo-app',
native: const DriftNativeOptions(),
databaseDirectory: getApplicationSupportDirectory,
));

@override
int get schemaVersion => 2; // Версия базы данных

@override
MigrationStrategy get migration {
return MigrationStrategy(
onCreate: (m) async {
await m.createAll();
// Добавьте миграции здесь, если версия новая
},
);
}
}



Шаг 3: Создание таблицы


@DataClassName('TaskEntry')
class TaskTable extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get description => text()();

// Добавьте ваши запросы здесь
static Stream<List<TaskEntry>> getAllItems(Database database) =>
database.select(database.taskTable).watch();
}



Шаг 3: Создание или редактирование build.yaml
в корневой папке вашего Flutter-приложения 📁


targets:
$default:
builders:
drift_dev:
# Эти опции изменяют способ генерации кода
options:
databases:
default: lib/modules/drift/database.dart
sql:
dialect: sqlite
options:
version: "3.38"
modules: [fts5]


Запустите сборщик, чтобы регенерировать схему базы данных 🔄

Оцените новую рубрику! 👍💬
Все подобные новости можно найти по хэштегу #FlutterPulseTips
#flutter #dart #flutterpulse #FlutterPulseTips #MobileDevelopment #LocalDatabase #DriftPackage #FlutterTips #AppDevelopment
👍2🔥1
Сделайте текст выбираемым

По умолчанию текст не является выбираемым. 🤔

Почему? Виджет SelectionArea позволяет выбирать текст, указывая Flutter обрабатывать отрисовку и взаимодействие для выбора текста. 📝


return MaterialApp(
home: SelectionArea(
child: Scaffold(
appBar: AppBar(title: const Text('SelectionArea Sample')),
body: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Row 1'),
Text('Row 2'),
Text('Row 3'),
],
),
),
),
),
);


ИЛИ 🔄


const SelectableText(
'Hello! How are you?',
textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.bold),
)


Используйте SelectableText вместо Text 🔁

Как выбрать между SelectionArea и SelectableText? 🤔
Используйте selectionArea, если вы хотите включить выбор на нескольких виджетах, а не только на тексте. 📚
Для одного текста просто используйте SelectableText 👍

Оцените новую рубрику и напишите своё мнение! 💬

Все подобные новости можно найти по хэштегу #FlutterPulseTips 🔍

#flutter #dart #flutterpulse #FlutterPulseTips #MobileDevelopment #UIUX #CodingTips #AppDevelopment #FlutterTips
5👍1
Вложенная навигация с Go_Router
Давайте создадим боковую панель с Flutter Web и go_router 🤔

Вместо того, чтобы напрямую добавлять все маршруты (GoRoute), мы обернём их в поднавигацию 📦

Мы обернём все подмаршруты в StatefulShellRoute, чтобы управлять состоянием поднавигации 🔄

Для каждого подмаршрута у нас будет StatefulShellBranch, который может содержать несколько маршрутов 🌐

Каждый элемент будет иметь свой собственный стек навигации 📚



import 'package:go_router/go_router.dart'; // Импорт библиотеки go_router

GoRouter generateRouter() {
return GoRouter(
routes: [
// Страница без боковой панели
GoRoute(
name: 'signin',
path: '/signin',
builder: (context, state) => const SignInPage(),
),
// Страница с боковой панелью
StatefulShellRoute(
parentNavigatorKey: navigatorKey, // Ключ навигатора родительского маршрута
// Боковая панель будет отображаться слева
builder: (context, state, navigationShell) => Row(
children: [
SideBar(state: state), // Боковая панель
Expanded(child: navigationShell), // Содержимое навигации
],
),
// builder будет вызван, когда маршрут активируется
// navigationShell - виджет, который отображает содержимое маршрута
// Он будет показывать текущую последнюю страницу стека навигации для каждой ветки
navigatorContainerBuilder: (
BuildContext context,
StatefulNavigationShell navigationShell,
List<Widget> children,
) {
if (children.isEmpty) {
return SizedBox(); // Пустой контейнер, если нет дочерних элементов
}
return Scaffold(
body: children[navigationShell.currentIndex], // Отображение текущего дочернего элемента
);
},
branches: [
StatefulShellBranch(
routes: [
GoRoute(
name: 'users',
path: '/users',
builder: (context, state) => const UsersPage(),
),
GoRoute(
name: 'user profile',
path: '/users/:userId',
builder: (context, state) => const UserProfilePage(),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
name: 'notifications',
path: '/notifications',
builder: (context, state) => const NotificationsPage(),
),
],
),
],
),
],
);
}



Оцените новую рубрику 👍👏! Все подобные новости можно найти по хэштегу #FlutterPulseTips. Не забудьте подписаться и следить за новыми советами 🔔

#flutter #dart #flutterpulse #FlutterPulseTips #MobileDevelopment #FlutterTips #GoRouter #NestedNavigation #FlutterWeb
👍3👨‍💻1