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

🔗 Наш канал для разработчиков: @friflex_dev
🔗 Канал о продуктовой разработке: @friflex_product
Download Telegram
💭Привет! Это Анна, Flutter Team Lead Friflex

Сегодня поговорим про механизмы ограничения доступа объектов в Dart. Простыми словами — какие есть варианты подсветить, что объект не должен использоваться извне.

1️⃣ Начнем с банального — использования «


final String _privateData;

void _doExample() {}


Символ «_» в начале названия говорит о том, что объект может использоваться только в рамках текущей библиотеки.

Кроме того, что он самый известный, он — фактически единственный действительно ограничивающий. Проект не сможет быть скомпилирован, пока приватный объект будет использоваться вне допустимого участка кода.

2️⃣ @protected
Аннотация @protected говорит о том, что объект доступен только внутри класса и в классах наследниках.

Здесь очень важно понимать, что аннотация никак не ограничивает компиляцию и работу программы. Единственное, что вы получите при неправильном использовании защищенного объекта — замечание анализатора invalid_use_of_protected_member


// файл parent.dart

class Parent {
@protected
void doProtected() {}
}
...
// файл child.dart

class Child extends Parent {
void doExample() {
doProtected(); // допустимо
}
}

void main() {
final parent = Parent();

parent.doProtected(); // вызывает предупреждение анализатора
}


3️⃣ @visibleForTesting
Еще одна ограничивающая аннотация. Она как бы говорит — «да, я публичный, но только для того, чтобы быть доступным в тестах». Будет точно полезно для тех, кто покрывает проект тестами


// файл lib/parent.dart

class Parent {
@visibleForTesting
void doExample() {}
}
...
// файл lib/main.dart

void main() {
final parent = Parent();
parent.doExample(); // вызывает предупреждение анализатора invalid_use_of_visible_for_testing_member
}
...
// файл test/parent_test.dart

void main() {
test('Тестируем @visibleForTesting', () {
final test = Parent();
test.doExample(); // допустимо
});
}


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

❤️ — если было полезно
Please open Telegram to view this post
VIEW IN TELEGRAM
14👍8🔥4🤡3🤩1
Привет, это Катя, Flutter Dev Friflex

11 февраля вышел Flutter 3.41 — важный релиз с 868 коммитами от 145 контрибьюторов! Давайте разберем ключевые моменты.

Публичные окна релизов
Теперь мы знаем точные даты релизов на 2026 год:
Flutter 3.41 — февраль
Flutter 3.44 — май
Flutter 3.47 — август
Flutter 3.50 — ноябрь

Модульные библиотеки Material и Cupertino
Material
и Cupertino переезжают в отдельные пакеты. Преимущества:
▫️Обновления дизайна без ожидания выхода SDK
▫️Можно обновить дизайн-систему независимо от версии Flutter
▫️Быстрая адаптация к изменениям iOS/Android

Поддержка современных стандартов

iOS:
▪️Swift Package Manager теперь стандарт (CocoaPods устаревает)
▪️Полная поддержка UIScene lifecycle

Android:
▪️Важно: пока НЕ обновляйтесь на AGP 9 — поддержка в разработке
▪️Новые плагины используют Kotlin DSL по умолчанию

Платформо-специфичные ассеты
Теперь можно указать, для каких платформ нужен ассет:

flutter:
  assets:
    - path: assets/web_worker.js
      platforms: [web]
    - path: assets/desktop_icon.png
      platforms: [windows, linux, macos]


Это уменьшает размер приложения.

Новый Getting Started
Полностью переработанный гайд для новичков:
▫️Быстрая установка с hot-reload в вебе
▫️8 новых видео от команды Flutter
▫️Создание 4 приложений с нуля

Улучшения Fragment Shader
▪️
Синхронная декодировка изображений — текстуры доступны в том же кадре
▪️Поддержка до 128-bit float текстур для фото-фильтров и высококачественных эффектов

Widget Previews (экспериментально)
▫️
Встроенный Flutter Inspector
▫️Поддержка зависимостей с dart:ffi

iOS улучшения
▪️
Bounded blur — устранение цветовых артефактов в BackdropFilter
▪️Нативная стилизация перетаскивания в CupertinoSheet

Add-to-App
Flutter view теперь может автоматически подстраиваться под размер контента — идеально для встраивания в нативные скроллы!

Навигация
▫️
Navigator.popUntilWithResult — закрытие нескольких экранов с передачей результата
▫️Улучшенный StretchingOverscrollIndicator (портирован из Android 12)

Desktop
▪️
Экспериментальные API для popup и tooltip окон
▪️Поддержка диалоговых окон на Linux, macOS, Windows
▪️Merged threads по умолчанию на Linux

DevTools
▫️
Скомпилированы с dart2wasm для лучшей производительности
▫️Автоматические переподключения к Dart Tooling Daemon
▫️Установка: просто выполните flutter upgrade (а потом несколько дней резолвите конфликты)

Кто уже перешел? Много правок пришлось вносить?

🔗Полные release notes можно посмотреть в официальной документации
Please open Telegram to view this post
VIEW IN TELEGRAM
❤‍🔥8🔥54
This media is not supported in your browser
VIEW IN TELEGRAM
Привет! С вами Анна, Flutter Team Lead Friflex

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

◾️connectivity_plus
У этого плагина всего две основные функци.

1️⃣ checkConnectivity() — позволяет вернуть список активных типов подключения на устройстве. И здесь очень важно: полученные типы не гарантируют реальный доступ в Интернет. Это скорее чисто аппаратная проверка, подключен ли девайс к Wi-Fi, мобильному Интернету и прочему.

Для части устройств этот метод может быть полезен для определения VPN-подключения, что очень актуально сейчас. Но учтите, что на iOS и macOS тип vpn никогда не вернется, вместо него вернется просто other.


Future<void> main() async {
final connectivity = Connectivity();

final connectionsList = await connectivity.checkConnectivity();
print(connectionsList);
}


2️⃣ А вот вторая —onConnectivityChanged — представляет собой поток обновлений списка активных подключений. Через него будет удобно в реальном времени следить за изменениями через подписку.

◾️internet_connection_checker
Этот плагин уже проверит реальный доступ в Интернет.

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


Future<void> main() async {
final hasConnection = await InternetConnectionChecker.instance.hasConnection;
print(hasConnection);
}


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

И тут на помощь может прийти кастомный «чекер». В нем вы можете задать необходимые адреса для проверки.


Future<void> main() async {
final customChecker = InternetConnectionChecker.createInstance(
addresses: [AddressCheckOption(uri: Uri.parse('custom_resource'))],
);
final hasConnection = await customChecker.hasConnection;

print(hasConnection);
}


Кроме однократной проверки, как и в предыдущей библиотеке, можно подписаться на смену статуса через поток onStatusChange.

◾️network_info_plus
Если вам мало простого индикатора: есть Интернет или нет, этот инструмент будет точно полезен.

Он дает возможность получить более развернутые данные о Wi-Fi подключении на устройстве. Тут можно получить и название сети, и ip-адрес, и прочее-прочее.


Future<void> main() async {
final networkInfo = NetworkInfo();

print(networkInfo.getWifiName());
print(networkInfo.getWifiIP());
print(networkInfo.getWifiIPv6());
print(networkInfo.getWifiBroadcast());
print(networkInfo.getWifiBSSID());
}


Делитесь своими лучшими практиками в комментариях⬇️
Please open Telegram to view this post
VIEW IN TELEGRAM
11🔥7🥰4👍3🤩2🤡2
В общем, выбирайте 🙌

А какой для вас идеальный подарок на 23 февраля?
❤‍🔥73🔥3
☀️Привет, это Катя, Flutter Dev Friflex!

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

Почему это важно?
Дефолтные шрифты — это, конечно, хорошо, но если вы хотите, чтобы ваше приложение выделялось, кастомные шрифты — это must-have. Плюс — это занимает буквально 5 минут!

Как это сделать?
Шаг 1: Скачайте шрифт. Я обычно использую Google Fonts. Там много бесплатных и красивых вариантов

Шаг 2: Создайте папку fonts в корне проекта и положите туда файлы шрифта (обычно это .ttf или .otf)

Шаг 3: Откройте pubspec.yaml и добавьте:

flutter:
  fonts:
    - family: Montserrat
      fonts:
        - asset: fonts/Montserrat-Regular.ttf
        - asset: fonts/Montserrat-Bold.ttf
          weight: 700


Шаг 4: Используйте в коде:

Text(
  'Привет, Flutter!',
  style: TextStyle(
    fontFamily: 'Montserrat',
    fontSize: 24,
    fontWeight: FontWeight.bold,
  ),
)


Или можете установить шрифт для всего приложения в ThemeData:

MaterialApp(
  theme: ThemeData(
    fontFamily: 'Montserrat',
  ),
  home: MyHomePage(),
)


Автоподгрузка
Если не хотите возиться с загрузкой файлов, используйте пакет google_fonts:

Text(
  'Привет, Flutter!',
  style: GoogleFonts.montserrat(
    fontSize: 24,
    fontWeight: FontWeight.bold,
  ),
)


Шрифты загружаются автоматически из Интернета (или кешируются). Удобно для прототипирования!

Если есть вопросы по Flutter или идеи для следующих постов — пишите в комментариях💭
Please open Telegram to view this post
VIEW IN TELEGRAM
8
💭Привет! С вами Анна, Flutter-тимлид Friflex

Сегодня поговорим про параметр dirty у элемента во Flutter.

Что такое Element?
Согласно официальной документации Flutter, виджет представляет собой конфигурацию, которая описывает, как именно должен выглядеть интерфейс. А вот элемент представляет конкретный виджет в определенном положении в дереве и отвечает за его жизненный цикл и состояние.

А dirty представляет собой индикатор состояния элемента. Он явно говорит — тут требуется перестроение.

Как это работает?
По умолчанию dirty всегда false. Изменение значения флага наглядно можно увидеть на примере StatefulWidget'a. Допустим, нам надо обновить его состояние, и мы вызываем метод setState(). Посмотрим, что он делает под капотом.

@protected
void setState(VoidCallback fn) {
...
_element!.markNeedsBuild();
}


После ряда проверок выполняется вызов метода markNeedsBuild(). Внутри него выполняется проверка: помечался ли ранее элемент для ребилда. Если не был, то он помечается и помещается в список dirty элементов.

void markNeedsBuild() {
...
if (dirty) {
return;
}
_dirty = true;
owner!.scheduleBuildFor(this);
}


И здесь очень хорошо видно — на самом деле setState() не запускает моментальное перестроение виджета. Он лишь ставит элемент в очередь. А уже в следующем кадре Flutter обработает список dirty-элементов и перестроит их точечно.

Со Stateful-виджетами в целом понятно, а что с Stateless? Тут тоже ничего сложного. Хотя у виджета и нет своего состояния, но он все же может быть перестроен по ряду причин.

Например, если обновился непосредственный родитель: его метод build() выполнится заново и создаст новые конфигурации дочерних виджетов. Если конфигурация Stateless-виджета изменилась, его элемент будет обновлен и отмечен, как требующий перестроения.
Другой вариант — если виджет зависит от InheritedWidget, который обновился. В этом случае соответствующий элемент также будет помечен как dirty и перестроен.

❤️ — если было полезно
Please open Telegram to view this post
VIEW IN TELEGRAM
17👍4🔥3🤩1
Привет, это Катя, Flutter-разработчик Friflex. Сегодня разберем концепцию BuildContext.

Что такое BuildContext?
BuildContext
— это специальный объект, который представляет собой ссылку на конкретное место виджета в дереве элементов. По сути, это идентификатор позиции виджета в иерархии приложения.

Каждый виджет при построении получает свой уникальный BuildContext. Именно через него виджет может взаимодействовать с другими частями дерева: получать доступ к родительским виджетам, темам, навигации и многому другому.

Как это работает на практике?
Когда мы пишем метод build(), мы всегда получаем параметр context:

@override
Widget build(BuildContext context) {
  return Container(
    child: Text('Привет'),
  );
}


Этот context и есть BuildContext. Он связывает наш виджет с конкретным элементом в дереве. Через него Flutter понимает, где именно находится виджет и как с ним работать.

Почему BuildContext так важен?
Возьмем простой пример — доступ к теме приложения:

final theme = Theme.of(context);

Метод Theme.of(context) использует BuildContext, чтобы подняться вверх по дереву виджетов и найти ближайший ThemeData. Без context это было бы невозможно — Flutter просто не знал бы, откуда начинать поиск.

То же самое происходит с навигацией:

Navigator.of(context).push(
  MaterialPageRoute(builder: (context) => NextScreen()),
);

Navigator ищет ближайший Navigator в дереве, используя переданный BuildContext как отправную точку.

Распространенная ошибка
Часто встречается ситуация, когда разработчик пытается использовать BuildContext до того, как виджет добавили в дерево, или после того, как его удалили. Например:

@override
Widget build(BuildContext context) {
  Future.delayed(Duration(seconds: 2), () {
    // Опасно! Context может быть уже невалидным
    showDialog(context: context, builder: (_) => AlertDialog());
  });
  return Container();
}


Если виджет удалят из дерева за эти две секунды, использование context приведет к ошибке. Для таких случаев стоит проверять mounted в StatefulWidget или использовать более безопасные подходы.

BuildContext и InheritedWidget
Особенно важную роль BuildContext играет при работе с InheritedWidget. Именно через context виджеты подписываются на изменения:

final data = MyInheritedWidget.of(context);


При таком вызове Flutter регистрирует зависимость текущего виджета от MyInheritedWidget через BuildContext. Когда InheritedWidget обновится, все зависимые от него виджеты будут автоматически перестроены.

❤️ — если было полезно
Please open Telegram to view this post
VIEW IN TELEGRAM
18
Привет, это Катя, Flutter Dev Friflex! Сегодня разберем, зачем нужны Keys во Flutter, как они работают и когда их стоит (и не стоит) использовать.

Что такое Key?
Key — идентификатор виджета, который помогает Flutter сопоставлять новые виджеты с уже существующими элементами при обновлении дерева.

Как Flutter сопоставляет виджеты без ключей
Когда Flutter получает новый список виджетов при ребилде, он пытается сопоставить их с существующими элементами по позиции и типу (runtimeType). Если порядок элементов изменился или виджеты одного типа поменялись местами, состояние может перескочить на другой элемент — потому что Flutter повторно использовал элемент по индексу.

Пример проблемы без ключей

import 'package:flutter/material.dart';

class ItemWidget extends StatefulWidget {
final String title;
ItemWidget(this.title);

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

class _ItemWidgetState extends State<ItemWidget> {
int counter = 0;

@override
Widget build(BuildContext context) {
return ListTile(
title: Text('${widget.title} ($counter)'),
trailing: IconButton(
icon: Icon(Icons.add),
onPressed: () => setState(() => counter++),
),
);
}
}


При перестановке элементов состояния могут перепутаться

Типы
▫️ValueKey<T> — ключ по значению (идеален для id-модели).
▫️ObjectKey — сравнение по == объекта.
▫️UniqueKey — каждый раз новый ключ (заставляет создать новый Element; сбрасывает состояние).
▫️GlobalKey — глобальная уникальность + доступ к State/Context; использовать экономно (дорогой).

Когда использовать
▫️Динамические списки с добавлением/удалением/реордером — ValueKey(id).
▫️Формы, доступ к State извне — GlobalKey (только при необходимости).

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

Рекомендации
▫️Для сущностей с постоянным id — ValueKey(id).
▫️Если хотите сбросить состояние — UniqueKey.
▫️GlobalKey — только для специфических задач (формы, навигация, тесты).
▫️Если состояние перескакивает — сначала добавьте ключи, а не перестраивайте архитектуру.

И помните, правильный выбор ключа решает распространенные баги с состоянием 🦋
Please open Telegram to view this post
VIEW IN TELEGRAM
3🔥1
Привет! Это Анна, лидер Flutter-команды Friflex.

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

flutter_staggered_animations
Эта
библиотека позволит вам красиво анимировать отрисовку списков и сеток. Работает с виджетами ListView, GridView, Column и Row. Вид анимации и ее длительность можно задавать самостоятельно, есть возможность комбинировать эффекты между собой. В результате получаем интересную каскадную отрисовку каждого элемента последовательно.

liquid_glass_renderer
Эта
библиотека понравится тем, кому пришлась по душе новинка iOS 26 — эффект жидкого стекла. Проект довольно новый, но уже имеет несколько вариантов виджетов. Тут можно создать и единичные стеклянные формы, и формы из нескольких смешанных объектов.

Очень важно — в боевую версию внедрять его пока не стоит, об этом предупреждает и сам автор. Но потрогать на досуге и в личных проектах будет точно интересно.

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

percent_indicator
Очень
простая, но полезная библиотека. Если вам нужно создать какой-то составной индикатор прогресса, она точно придется кстати.

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

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

skeletonizer
Без
скелетонов в современных мобильных приложениях уже никуда, поэтому skeletonizer точно пригодится.

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

confetti
Хочется добавить праздника в проект? Пожалуйста, конфетти будут в самый раз!

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

❤️ — если присматриваетесь к одной из этих библиотек
21🔥1🤩1🤣1
😌ИИ-будущее: нас заменят?

Привет, это Катя, Flutter-разработчик Friflex! Сегодня коротко: про всплеск агентов и утилит вокруг них, модели, навыки, MCP и прочие инструменты.

Многие компании уже ищут ИИ-программистов или инженеров продукта, которые умеют не только писать код, но и проектировать и интегрировать агентов в продукт.

Ключевые игроки на рынке ИИ: Anthropic, OpenAI, Google (Vertex AI / PaLM), Microsoft, Meta, Hugging Face, Cohere, Mistral, Stability AI.

Понимаю, многие сейчас напряглись из‑за давления ИИ: страх потерять роль, ускоренные требования к навыкам, неопределенность задач. Именно поэтому важно не пугаться, а погружаться: изучать архитектуры агентов, безопасность, координацию (оркестрацию), CI/CD и продуктовый инжиниринг вокруг них.

Интересна ли вам эта тема? Если да, то дайте знать, какие моменты разобрать подробнее: выбор модели, архитектуру агентов, безопасность и приватность, управление навыками, CI/CD для агентов, кейсы инженера продукта или что-то еще? Пишите темы, а я соберу инфу ⬇️
Please open Telegram to view this post
VIEW IN TELEGRAM
5👍2
Привет! Это Анна, лидер Flutter-команды Friflex.

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

Библиотека помогает провести локальную аутентификацию с помощью настроек, которые есть на устройстве. Например, пин-кода, сканирования отпечатка пальца или идентификации по лицу (FaceID).

Все работает очень просто: плагин не проверяет код или лицо и отпечаток пальца самостоятельно. Он обращается в систему, запрашивает проверку, и возвращает в приложение простое булево значение. Разберем методы плагина подробнее ⬇️

Для начала нужно создать экземпляр класса LocalAuthentication. Через него будут выполняться все операции.

final localAuth = LocalAuthentication();


Метод isDeviceSupported() проверит наличие любого способа аутентификации на устройстве. А canCheckBiometrics() ответит, доступна ли аутентификация именно по биометрии.

final isDeviceSupported = await localAuth.isDeviceSupported();
final canAuthenticate = await localAuth.canCheckBiometrics;


Еще можно получить список всех доступных на устройстве способов аутентификации по биометрии. Если на устройстве не настроен вход по отпечатку пальца или по лицу, то список придет пустым.

final list = await localAuth.getAvailableBiometrics();


А здесь внимательно: getAvailableBiometrics вернет список только тех биометрических функций, которые настроены пользователем на устройстве. А флаг canAuthenticate просто покажет их наличие.

Дальше самое интересное — проверка. Она выполняется с помощью метода authenticate(). В него можно добавить строку с описанием причины запроса аутентификации. Этот текст пользователь увидит на экране. Также можно задавать ограничения. Например, установить true флаг biometricOnly. Тогда ввод системного пин-кода не будет запрашиваться.

final successAuth = await localAuth.authenticate(
localizedReason: 'ВОЙДИТЕ',
biometricOnly: true,
);


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

С вас ❤️, если было полезно
Please open Telegram to view this post
VIEW IN TELEGRAM
16🔥8🤩2
This media is not supported in your browser
VIEW IN TELEGRAM
🪙Привет! Это Анна, Friflex Flutter Team Lead

Знаете, как во Flutter-приложении получать информацию от аппаратных датчиков движения? Сейчас узнаете!

В этой задаче вам поможет плагин sensors_plus. Как описывают его разработчики — он дает возможность вашему Flutter-приложению обращаться к сенсорам устройства, таким как:
▪️ акселерометр
▪️ гироскоп
▪️ барометр
▪️ магнитометр

Как это работает?
Через плагин приложение обращается в платформу. Натив считывает данные с сенсоров и полученные данные передает во Flutter посредством Streams (потоков). В приложении же вам достаточно подписаться на необходимый поток с данными.

Библиотека дает возможность отслеживать данные по пяти основным событиям:

✔️AccelerometerEvent — ускорение устройства. Это событие не фильтрует гравитацию, поэтому в состоянии покоя покажет всегда 9.8 м/с² вверх

✔️UserAccelerometerEvent — в отличие от AccelerometerEvent отражает только фактическое ускорение устройства. Поток исключает гравитацию, что в состоянии покоя покажет 0

✔️GyroscopeEvent — вращение устройства

✔️MagnetometerEvent — данные окружающего магнитного поля.

✔️BarometerEvent — текущее атмосферное давление

Использовать очень просто. Достаточно подписаться на поток данных по необходимому событию.

late StreamSubscription<AccelerometerEvent> _accelerometerSubscription;
...
_accelerometerSubscription = accelerometerEventStream().listen((event) {
print('x: ${event.x}, y: ${event.y}, z: ${event.z}');
});


Не забывайте закрывать подписки
_accelerometerSubscription.cancel();


Для чего может быть полезно?
Представим, вам нужно сделать реализацию как в банках — при перевороте экрана требуется скрывать или открывать данные на экране. Или при тряске устройства небходимо выполнять перезапрос данных. Для всех этих задач sensors_plus точно будет полезен.
Please open Telegram to view this post
VIEW IN TELEGRAM
5🔥3🤩1
Всегда с нетерпением ждем этого дня, чтобы сделать подборку ИТ-мемов

Пусть поводов для улыбки будет больше 💛
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
7🔥6😁4👍2