Эффективная передача стиля текста виджетам
Привет, разработчики Flutter! 👋 Сегодня мы поделимся с вами полезным советом о том, как эффективно передавать стиль текста вашим виджетам. 📝
Вы когда-нибудь сталкивались с ситуацией, когда приходилось копировать стиль текста для дочерних элементов? 🤔 Теперь вы можете этого избежать! 😉
Использование DefaultTextStyle.merge
Преимущества использования DefaultTextStyle.merge:
• Позволяет задать только те свойства текста, которые вы хотите переопределить при использовании вашего виджета.
• Упрощает код и делает его более читаемым.
👍 Оцените новую рубрику FlutterPulseTips и поделитесь своими мыслями! 🤔
Все подобные новости можно найти по хэштегу #FlutterPulseTips
#flutter #dart #flutterpulse #FlutterPulseTips #MobileDevelopment #UIUX #CodingTips #AppDevelopment
Привет, разработчики Flutter! 👋 Сегодня мы поделимся с вами полезным советом о том, как эффективно передавать стиль текста вашим виджетам. 📝
Вы когда-нибудь сталкивались с ситуацией, когда приходилось копировать стиль текста для дочерних элементов? 🤔 Теперь вы можете этого избежать! 😉
Использование DefaultTextStyle.merge
class MyWidget extends StatelessWidget {
final Widget title;
const TheBestCustomWidget({
super.key,
required this.title,
});
@override
Widget build(BuildContext context) {
return WidgetContainer(
children: [
DefaultTextStyle.merge(
style: Theme.of(context).textTheme.headlineMedium,
child: title,
),
],
);
}
}
MyWidget(
title: Text(
'Signup now',
style: Theme.of(context).textTheme.headlineLarge.copyWith(
color: Theme.of(context).colorScheme.onBackground,
),
),
)
MyWidget(
title: Text(
'Signup now',
style: TextStyle(
color: Theme.of(context).colorScheme.onBackground,
),
),
)
Преимущества использования DefaultTextStyle.merge:
• Позволяет задать только те свойства текста, которые вы хотите переопределить при использовании вашего виджета.
• Упрощает код и делает его более читаемым.
👍 Оцените новую рубрику FlutterPulseTips и поделитесь своими мыслями! 🤔
Все подобные новости можно найти по хэштегу #FlutterPulseTips
#flutter #dart #flutterpulse #FlutterPulseTips #MobileDevelopment #UIUX #CodingTips #AppDevelopment
👍1
Как протестировать Изолят
Запуск функции изолята в модульных тестах
При написании модульных тестов для Flutter-приложений часто возникает необходимость протестировать функции, выполняющиеся в изоляте. Изолят - это отдельный поток выполнения в Dart, который может выполняться параллельно с основным потоком. Однако тестирование таких функций может быть проблематичным.
Рассмотрим пример неправильного тестирования изолята:
Такой тест зависнет и никогда не завершится, поскольку функция
Решение: Используйте tester.runAsync() для запуска асинхронного кода в тесте:
Таким образом, вы сможете корректно протестировать функции, выполняющиеся в изоляте.
Оцените новую рубрику и напишите своё мнение! 👍💬
Все подобные новости можно найти по хэштегу #FlutterPulseTips
#flutter #dart #flutterpulse #FlutterPulseTips #MobileDevelopment #Testing #Isolate #FlutterTips #DartTips
Запуск функции изолята в модульных тестах
При написании модульных тестов для 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
Глупый и умный: создаём "глупый" конструктор и умные фабрики
Привет, друзья! 👋 Сегодня мы поговорим о том, как улучшить ваш код на Flutter/Dart, используя "глупые" конструкторы и умные фабрики. 📈
Что такое "глупый" конструктор?
"Глупый" конструктор - это конструктор, который только присваивает данные. Он не должен делать ничего другого! 🚫
Пример плохого конструктора:
Пример хорошего "глупого" конструктора:
Зачем использовать фабрики?
Фабрики идеально подходят для более сложных присвоений значений. Они позволяют создавать объекты более гибко и читаемо. 🌟
Пример фабрики:
Вывод:
- Конструкторы должны быть "глупыми" и только присваивать данные.
- Для более сложных операций используйте фабрики.
Оцените нашу новую рубрику! 👍 Нам важно ваше мнение. Оставляйте свои комментарии и предложения. 💬
Все подобные новости вы можете найти по хэштегу #FlutterPulseTips
#flutter #dart #flutterpulse #FlutterPulseTips #MobileDevelopment #CodingTips #Programming #SoftwareDevelopment
Привет, друзья! 👋 Сегодня мы поговорим о том, как улучшить ваш код на Flutter/Dart, используя "глупые" конструкторы и умные фабрики. 📈
Что такое "глупый" конструктор?
"Глупый" конструктор - это конструктор, который только присваивает данные. Он не должен делать ничего другого! 🚫
Пример плохого конструктора:
class Device {
String? id;
String? name;
OperatingSystem? platform;
Device({
this.id,
}) :
// Плохая практика - присвоение значений в теле конструктора
name = null,
platform = null {
final deviceInfo = ...; // Получение информации об устройстве
name = deviceInfo.name;
platform = deviceInfo.platform;
}
}
Пример хорошего "глупого" конструктора:
class Device {
String? id;
String name; // Теперь обязательное поле
OperatingSystem platform; // Теперь обязательное поле
Device({
this.id,
required this.name, // Требуем имя устройства
required this.platform, // Требуем платформу устройства
});
}
Зачем использовать фабрики?
Фабрики идеально подходят для более сложных присвоений значений. Они позволяют создавать объекты более гибко и читаемо. 🌟
Пример фабрики:
factory Device.current() {
final deviceInfo = ...; // Получение информации об устройстве
return Device(
name: deviceInfo.name, // Присваиваем имя устройства
platform: deviceInfo.platform, // Присваиваем платформу устройства
);
}
Вывод:
- Конструкторы должны быть "глупыми" и только присваивать данные.
- Для более сложных операций используйте фабрики.
Оцените нашу новую рубрику! 👍 Нам важно ваше мнение. Оставляйте свои комментарии и предложения. 💬
Все подобные новости вы можете найти по хэштегу #FlutterPulseTips
#flutter #dart #flutterpulse #FlutterPulseTips #MobileDevelopment #CodingTips #Programming #SoftwareDevelopment
👍3❤1
Избегайте тестирования с помощью моков
Моки отражают вашу реализацию
Лучшие тесты не отражают вашу реализацию.
Они позволяют рефакторить код, не задумываясь о том, как они работают.
Вы тестируете то, что они возвращают, а не как они это делают.
Таким образом, вы можете рефакторить код, пока они продолжают работать.
Наш тест не знает ничего о том, как мы используем
👋 Прощай, mockito!
Оцените новую рубрику и напишите своё мнение! 👍
Все подобные новости можно найти по хэштегу #FlutterPulseTips
#flutter #dart #flutterpulse #FlutterPulseTips #MobileDevelopment #TestingTips #CodingBestPractices
Моки отражают вашу реализацию
Лучшие тесты не отражают вашу реализацию.
Они позволяют рефакторить код, не задумываясь о том, как они работают.
Вы тестируете то, что они возвращают, а не как они это делают.
Таким образом, вы можете рефакторить код, пока они продолжают работать.
test('on receive message, should dispatch as a notification', () async {
final repository = AppNotificationsRepository(
notificationsApi: fakeNotificationsApi,
notificationPublisher: dispatcher,
);
Notification? receivedNotification;
dispatcher.subscribe((notification) => receivedNotification = notification);
fakeNotificationsApi.sendForegroundMessage(
const RemoteMessage(
data: {
'title': 'title',
'body': 'example body',
},
),
);
await Future.delayed(const Duration(milliseconds: 100));
expect(receivedNotification, isNotNull);
expect(receivedNotification!.title, 'title');
expect(receivedNotification!.body, 'example body');
});
class FakeNotificationsApi implements NotificationsApi {
OnRemoteMessage? _foregroundHandler;
@override
void setForegroundHandler(OnRemoteMessage handler) {
_foregroundHandler = handler;
}
void sendForegroundMessage(RemoteMessage message) {
_foregroundHandler?.call(message);
}
}
Наш тест не знает ничего о том, как мы используем
NotificationsApi
.Mockito
заставил бы нас имитировать каждый метод и проверять, как мы их используем.👋 Прощай, mockito!
Оцените новую рубрику и напишите своё мнение! 👍
Все подобные новости можно найти по хэштегу #FlutterPulseTips
#flutter #dart #flutterpulse #FlutterPulseTips #MobileDevelopment #TestingTips #CodingBestPractices
💯2
Создаём кастомную панель приложений
Потому что вы можете 😉
Вы можете создать кастомную AppBar, реализовав интерфейс PreferredSizeWidget. Это даст вам гибкость в настройке панели приложений под нужды вашего приложения.
Чтобы использовать эту кастомную AppBar, просто передайте её в свойство appBar виджета Scaffold:
Оцените нашу новую рубрику по Flutter советам 👍! Все подобные новости можно найти по хэштегу #FlutterPulseTips
#flutter #dart #flutterpulse #FlutterPulseTips #MobileDevelopment #CustomAppBar #AppBarDesign #FlutterTips
Потому что вы можете 😉
Вы можете создать кастомную 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
👍3❤1
Создай расширение темы
Упростите доступ к свойствам темы вашего приложения 💻
Расширение темы позволяет упростить доступ к свойствам темы вашего приложения. Для этого нужно создать расширение класса
Пример использования
До: сложный доступ к свойствам темы
После: упрощенный доступ с расширением
Оцените новую рубрику по достоинству! 👍💬 Оставляйте ваши отзывы и предложения в комментариях! 💬👇
Все подобные новости можно найти по хэштегу #FlutterPulseTips
#flutter #dart #flutterpulse #FlutterPulseTips #MobileDevelopment #UIUX #CodingTips #AppDevelopment #SoftwareDevelopment
Упростите доступ к свойствам темы вашего приложения 💻
Расширение темы позволяет упростить доступ к свойствам темы вашего приложения. Для этого нужно создать расширение класса
BuildContext
.
extension ApparenceKitThemeExt on BuildContext {
ApparenceKitColors get colors => Theme.of(this).extension<ApparenceKitColors>()!;
// Получение цветов темы
TextTheme get textTheme => Theme.of(this).textTheme;
// Получение текстовой темы
ApparenceKitTextTheme get fonts => Theme.of(this).extension<ApparenceKitTextTheme>()!;
// Получение шрифтов темы
ThemeData get theme => Theme.of(this);
// Получение данных темы
Brightness get brightness => Theme.of(this).brightness;
// Получение яркости темы
ApparenceKitThemeData get kitTheme => ThemeProvider.of(this).current.data;
// Получение данных темы ApparenceKit
}
Пример использования
До: сложный доступ к свойствам темы
class MyWidget extends StatelessWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context) {
return Container(
color: Theme.of(context).colorScheme.primary,
);
}
}
После: упрощенный доступ с расширением
class MyWidget extends StatelessWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context) {
return Container(
color: context.colors.primary,
);
}
}
Оцените новую рубрику по достоинству! 👍💬 Оставляйте ваши отзывы и предложения в комментариях! 💬👇
Все подобные новости можно найти по хэштегу #FlutterPulseTips
#flutter #dart #flutterpulse #FlutterPulseTips #MobileDevelopment #UIUX #CodingTips #AppDevelopment #SoftwareDevelopment
👍5
Локальная база данных
Когда вы хотите, чтобы ваше приложение работало офлайн 🔄
Шаг 1: Использование пакета Drift
Для работы с локальной базой данных мы будем использовать пакет Drift. 📦
Шаг 2: Создание базы данных
Шаг 3: Создание таблицы
Шаг 3: Создание или редактирование build.yaml
в корневой папке вашего Flutter-приложения 📁
Запустите сборщик, чтобы регенерировать схему базы данных 🔄
Оцените новую рубрику! 👍💬
Все подобные новости можно найти по хэштегу #FlutterPulseTips
#flutter #dart #flutterpulse #FlutterPulseTips #MobileDevelopment #LocalDatabase #DriftPackage #FlutterTips #AppDevelopment
Когда вы хотите, чтобы ваше приложение работало офлайн 🔄
Шаг 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 и SelectableText? 🤔
Используйте
Для одного текста просто используйте
Оцените новую рубрику и напишите своё мнение! 💬
Все подобные новости можно найти по хэштегу #FlutterPulseTips 🔍
#flutter #dart #flutterpulse #FlutterPulseTips #MobileDevelopment #UIUX #CodingTips #AppDevelopment #FlutterTips
По умолчанию текст не является выбираемым. 🤔
Почему? Виджет
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
❤6👍1
🚀 Расширяем возможности Flutter с помощью FFI: бинарники и компиляция под капотом!
🔥 Готовы погрузиться в мир нативной магии? В новом видео от эксперта Руслана Цицера — ключевые секреты интеграции C/C++ библиотек во Flutter через FFI! Узнайте, как избежать подводных камней и заставить "железо" работать на вас.
👉 СМОТРЕТЬ ВИДЕО 👈
💡 В этом выпуске:
🔧 Две главные проблемы FFI и их элегантные решения:
1️⃣ Выбор компилятора — почему для iOS, Android и macOS нужны разные инструменты и как их настроить
2️⃣ Интеграция бинарников — куда поместить скомпилированные библиотеки, чтобы Flutter их "увидел"
📦 Практические примеры:
- Работа с Makefile и скриптами сборки
- Особенности подключения под Android (
- Автоматизация переноса бинарников
🚨Внимание, лайфхак! Для iOS показан работающий метод подключения через "кустарные" CocoaPods — минимум конфигурации, максимум результата!
🔗 Ресурсы:
- GitHub с примерами кода
💬 "Если вы работаете с нативным кодом — это видео сэкономит вам часы поисков!"
👍 Не пропустите! Узнайте, как:
- Собрать универсальные бинарники под любую платформу
- Избежать ошибок ABI-совместимости
- Оптимизировать процесс сборки
👉 Подписывайтесь на канал, жмите 🔔 колокольчик, чтобы не пропустить новые выпуски! Ваши лайки и комментарии — лучшая мотивация для автора 💙
И про реакции на нашем канале тоже не забывайте!) 👍
#Flutter #Dart #FlutterPulse #FlutterPulseTips #FlutterPulseYoutube #FFI #Rust #NativeCode #MobileDevelopment
🔥 Готовы погрузиться в мир нативной магии? В новом видео от эксперта Руслана Цицера — ключевые секреты интеграции C/C++ библиотек во Flutter через FFI! Узнайте, как избежать подводных камней и заставить "железо" работать на вас.
👉 СМОТРЕТЬ ВИДЕО 👈
💡 В этом выпуске:
🔧 Две главные проблемы FFI и их элегантные решения:
1️⃣ Выбор компилятора — почему для iOS, Android и macOS нужны разные инструменты и как их настроить
2️⃣ Интеграция бинарников — куда поместить скомпилированные библиотеки, чтобы Flutter их "увидел"
📦 Практические примеры:
- Работа с Makefile и скриптами сборки
- Особенности подключения под Android (
jniLibs
) и iOS (Framework + Info.plist)- Автоматизация переноса бинарников
🚨
🔗 Ресурсы:
- GitHub с примерами кода
💬 "Если вы работаете с нативным кодом — это видео сэкономит вам часы поисков!"
👍 Не пропустите! Узнайте, как:
- Собрать универсальные бинарники под любую платформу
- Избежать ошибок ABI-совместимости
- Оптимизировать процесс сборки
👉 Подписывайтесь на канал, жмите 🔔 колокольчик, чтобы не пропустить новые выпуски! Ваши лайки и комментарии — лучшая мотивация для автора 💙
И про реакции на нашем канале тоже не забывайте!) 👍
#Flutter #Dart #FlutterPulse #FlutterPulseTips #FlutterPulseYoutube #FFI #Rust #NativeCode #MobileDevelopment
❤2
Вложенная навигация с Go_Router
Давайте создадим боковую панель с Flutter Web и go_router 🤔
Вместо того, чтобы напрямую добавлять все маршруты (GoRoute), мы обернём их в поднавигацию 📦
Мы обернём все подмаршруты в StatefulShellRoute, чтобы управлять состоянием поднавигации 🔄
Для каждого подмаршрута у нас будет StatefulShellBranch, который может содержать несколько маршрутов 🌐
Каждый элемент будет иметь свой собственный стек навигации 📚
Оцените новую рубрику 👍👏! Все подобные новости можно найти по хэштегу #FlutterPulseTips. Не забудьте подписаться и следить за новыми советами 🔔
#flutter #dart #flutterpulse #FlutterPulseTips #MobileDevelopment #FlutterTips #GoRouter #NestedNavigation #FlutterWeb
Давайте создадим боковую панель с 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
👍4👨💻1