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

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

С каждым месяцем разработка и портирование Flutter-приложений на ОС Аврора становится все более популярным запросом у заказчиков. Этот повышенный спрос рождает у разработчиков все больше интереса к операционной системе и работе с ней.

На Youtube-канале Friflex
мой коллега Юрий Петров @mobile_developing выпустил серию видео, которые помогут вам подготовиться к разработке приложений для ОС Аврора, а также подскажут, как просто и быстро портировать Flutter-приложение. Особенно рекомендую видео:
▫️Полный гайд по установке и настройке Flutter Aurora
▫️Пример портирования Wonderous на Аврора.


Кто уже разрабатывал Flutter-приложения под ОС Аврора знают, что первым шагом необходимо добавить папку с нативным кодом, которая обеспечит поддержку системы. Для этого в терминале достаточно выполнить команду:

flutter-aurora create --platforms=aurora --org={название_организации} .


После успешного выполнения команды в корне проекта будет создана папка aurora. И сегодня мы подробно изучим ее содержимое.

▪️Папка desktop и файл .desktop
Файл .desktop является основной конфигурацией вашего приложения. Именно здесь вы можете менять название самого приложения, иконку, запрашиваемые разрешения. Выглядит содержимое файла следующим образом⬇️

Основные поля, которые могут вам потребоваться, пометила комментариями с описанием.

[Desktop Entry]
Type=Application
Name=Example // Название приложения (только на английском)
Name[ru]=Приложение-пример // Название приложения на русском
Comment=Мобильное приложение для сети магазинов Example. // Описание приложения
Icon=ru.example.mobile.aurora.example_mobile // Иконка приложения
Exec=/usr/bin/ru.example.mobile.aurora.example_mobile
X-Nemo-Application-Type=silica-qt5

[X-Application]
Permissions=DeviceInfo;Camera;Internet;UserDirs;SecureStorage // Запрашиваемые разрешения
OrganizationName=ru.example.mobile.aurora // Название организации, которое было указано при создании папки aurora
ApplicationName=example_mobile


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

▪️Папка flutter
Сюда автоматически генерируются файлы для подключения плагинов. Все сгенерированные здесь файлы не рекомендую менять вручную.

▪️Папка icons
Здесь хранятся иконки приложения в png-формате в 4-х разрешениях — 86x86, 108x108, 128x128, 172x172. Если вы хотите заменить иконку, вам достаточно заменить картинки-заглушки своими изображениями. Важно — разрешения и формат должны сохраниться.

▪️Папка rpm
Здесь хранится еще один важный файл конфигурации .spec. Этот файл нечто сродни всем нам знакомому pubspec.yaml. Здесь мы указываем основную информацию о приложении:

Name: ru.example.mobile.aurora.example_mobile // Название бандла
Summary: Мобильное приложение для сети магазинов Example // Описание продукта, которое будет отображено при запуске
Version: 1.0.1 // Версия приложения
Release: 20 // Версия сборки


Важно — версия приложения на Авроре не подтягивается из pubspec.yaml, как на Android или iOS. Ее требуется поднимать именно здесь.

Также в этом файле при подключении плагинов по необходимости можно указывать зависимости под ключом BuildRequires:

BuildRequires: pkgconfig(streamcamera)
BuildRequires: pkgconfig(sqlite3)
BuildRequires: pkgconfig(runtime-manager-qt5)


А вы уже сталкивались с разработкой приложений для ОС Аврора?
7🔥4👍2🥰1
Привет, это Роза, Flutter Dev Friflex! 👋

Представь ситуацию: тебе нужно выполнить определенный код только после того, как интерфейс полностью будет отрисован. Как быть? Конечно же, использовать addPostFrameCallback! Давайте разберем его.

Немного теории и фактов
▫️addPostFrameCallback – это метод класса WidgetsBinding, который наследуется от SchedulerBinding во Flutter
▫️ Он позволяет зарегистрировать функцию обратного вызова, которая появится после завершения отрисовки текущего кадра, но до начала следующего
▫️ Обратный вызов выполняется один раз и не может быть отменен
▫️ В качестве параметра обратный вызов получает Duration с указанием времени его вызова

Когда это может быть полезно?
▪️Показ диалогового окна после полной визуализации дерева виджетов
▪️Прокрутка к нужной позиции после построения UI
▪️Программная прокрутка к определенной позиции после построения интерфейса
▪️Запуск анимаций, зависящих от уже построенного UI

Ты можешь подумать: «Я же могу использовать StatefulWidget и реализовать все в initState без всяких addPostFrameCallback! В чем же польза?»

На самом деле, добавлять такую логику в initState — не самая лучшая идея. Вот почему:
▫️ initState вызывается до первой отрисовки виджета. Если ты попытаешься получить context или размеры виджетов в initState, ты можешь словить ошибку или просто получить нулевые значения, потому что виджеты еще не отрисованы и не имеют реальных размеров и позиций
▫️Если ты вызовешь setState() в initState, когда build() еще не закончился, ты получишь ошибку, похожую на > setState() or markNeedsBuild() called during build
▫️addPostFrameCallback, в отличие от этого, гарантирует, что все уже отрисовано и можно спокойно работать с контекстом

И осталось самое главное — разобраться, как использовать. Представим, нам нужно показать уведомление после того, как экран будет полностью отрисован.

class _SomeScreenState extends State<SomeScreen> {
  @override
  void initState() {
    super.initState();

    // Показываем snackbar после полной отрисовки интерфейса
    WidgetsBinding.instance.addPostFrameCallback((_) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Some text')),
      );
    });
  }

  // build method


На заметку:
✔️ Не используй addPostFrameCallback для тяжелых операций — они могут замедлить отрисовку следующего кадра
✔️ Если тебе нужно выполнять действия на каждом кадре, то addPostFrameCallback не подойдет
✔️ А если просто нужно отложить выполнение кода без привязки к UI — можно использовать Future.delayed или Future.microtask.

На этом все! А сейчас предлагаю выполнить addPostFrameCallback по версии Telegram и поставить ❤️
17👍6🔥3👌1
Привет, это Катя, Flutter Dev Friflex. Сегодня обсудим AutomaticKeepAliveClientMixin.

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

Проблема
Допустим, у нас есть 3 вкладки, и каждая из них — это StatefulWidget с вложенными виджетами. Когда мы перелистываем между ними, Flutter по умолчанию «выгружает» неактивную вкладку, чтобы экономить память.

Что происходит:
Проскроллили вниз на 1-й вкладке ➡️ переключились на 2-ю ➡️
вернулись обратно на 1-ю ➡️
все обнулилось — нас кинуло в самое начало.

Решение
AutomaticKeepAliveClientMixin — это миксин, который говорит Flutter: «Пожалуйста, не выгружай меня, даже если я временно не на экране». Он работает вместе с PageStorage — специальным механизмом Flutter для хранения состояний виджетов.

Как использовать
На коленке наклепали такой виджет, который каждый раз будет сбрасывать свое состояние:

class MyTab extends StatefulWidget {
  @override
  _MyTabState createState() => _MyTabState();
}

class _MyTabState extends State<MyTab> {
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: 100,
      itemBuilder: (_, index) => ListTile(title: Text('Item $index')),
    );
  }
}


Теперь добавляем AutomaticKeepAliveClientMixin, чтобы этого не происходило:

class _MyTabState extends State<MyTab> with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true; // СУПЕР ВАЖНО

  @override
  Widget build(BuildContext context) {
    super.build(context); // ОБЯЗАТЕЛЬНО ВЫЗЫВАТЬ
    return ListView.builder(
      itemCount: 100,
      itemBuilder: (_, index) => ListTile(title: Text('Item $index')),
    );
  }
}


Миксин работает только в тех местах, где Flutter может использовать KeepAlive, например, в TabBarView, PageView, ListView.builder, внутри PageStorage.

Где использовать
▫️
Вкладки (TabBarView), чтобы запоминать положение и состояние
▫️Страницы в PageView, например, в onboarding-экранах
▫️Динамические списки, где хотим сохранить scroll-положение и состояние каждого элемента

Как быстро проверить, что KeepAlive работает
Работает, если initState called будет вызван только один раз:

@override
void initState() {
  super.initState();
  print('initState called'); 
}


📎Документы по миксину — здесь

🔥 — если использовали AutomaticKeepAliveClientMixin на практике
👍176🔥5
Всем привет! Это Анна, Friflex Flutter Team Lead👋

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

Основной механизм общения нативного и Dart-кода во Flutter-приложениях — Platform Channels. Это общий термин для названия всех видов каналов.

Есть три вида платформенных каналов:
▫️ MethodChannel — канал для одноразовых асинхронных вызовов методов платформы. Работает по принципу запрос-ответ. Через MethodChannel методом invokeMethod Flutter делает запрос в платформу, которая затем возвращает некоторый результат
▫️ EventChannel — канал для непрерывного общения с платформой. Работает в формате потоков событий от платформы во Flutter
▫️ BasicMessageChannel — канал для простого асинхронного обмена сообщениями с платформой

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

На примере всеми известного плагина permission_handler рассмотрим, как настраивается общение с платформой.

В Dart-коде создается экземпляр MethodChannel.

const MethodChannel _methodChannel = MethodChannel('flutter.baseflow.com/permissions/methods');

Затем этот канал используется в методах для запроса платформенных данных. Например, метод checkPermissionStatus запрашивает статус текущего разрешения.

 @override
Future<PermissionStatus> checkPermissionStatus(Permission permission) async {
final status = await _methodChannel.invokeMethod(
'checkPermissionStatus', permission.value);

return decodePermissionStatus(status);
}


В нативном коде Android тоже создается экземпляр канала, к нему подключается обработчик вызовов.

private void startListening(Context applicationContext, BinaryMessenger messenger) {
methodChannel = new MethodChannel(
messenger,
"flutter.baseflow.com/permissions/methods");

methodCallHandler = new MethodCallHandlerImpl(
applicationContext,
new AppSettingsManager(),
this.permissionManager,
new ServiceManager()
);

methodChannel.setMethodCallHandler(methodCallHandler);
}


В самом нативном обработчике уже задается обработка вызываемых методов. Например, так обрабатывается запрос checkPermissionStatus

@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) {
switch (call.method) {
...
case "checkPermissionStatus": {
@PermissionConstants.PermissionGroup final int permission = Integer.parseInt(call.arguments.toString());
permissionManager.checkPermissionStatus(
permission,
result::success);
break;
}
...
}
}


❤️ — если пост был полезен
13🔥2🤯1
Какой тип Platform Channel вам приходится использовать чаще всего?
Anonymous Poll
91%
MethodChannel
7%
EventChannel
1%
BasicMessageChannel
Привет, это Роза, Flutter Dev Friflex! 👋

В любой разработке рано или поздно придется столкнуться с такими аббревиатурами, как KISS, DRY и YAGNI. Чтобы в следующий раз точно знать, что они означают и как их расшифровывать, давай разберемся, что к чему.

Зачем все это?
Ты нарушаешь базовые архитектурные принципы, если в твоем коде:
▫️Повторяются фрагменты — одна и та же логика дублируется в нескольких местах
▫️Сложная логика — код трудно читать и поддерживать
▫️Есть «код на будущее» — ты добавляешь функции и классы, которые сейчас не нужны, но «вдруг пригодятся»

Такой код быстро превращается в ад, и его сложно поддерживать. Именно для этого и существуют три основных правила:
☑️ KISS (Keep It Simple, Stupid) — делай проще
☑️ DRY (Don't Repeat Yourself) — не повторяйся
☑️ YAGNI (You Aren’t Gonna Need It) — тебе это не понадобится

Это не просто модные слова — это база, которая делает твой код чище, понятнее и легче в поддержке.

KISS: Keep It Simple, Stupid — Будь проще!
Принцип KISS говорит: не усложняй! Простые решения всегда выигрывают, потому что их легче поддерживать. Чем проще код, тем легче его читать, понимать и изменять в будущем. Сложные архитектуры, перегруженные конструкции и «гениальные» решения часто оборачиваются проблемами.

В чем суть KISS:
▫️ Ясность — код должен быть понятным с первого взгляда
▫️ Эффективность — никаких избыточных решений
▫️ Гибкость — простые вещи проще адаптировать
▫️ Удобство — такой код приятно читать и трогать
▫️ Меньше багов — чем проще, тем меньше мест для ошибок

KISS — это стремление найти простое, но рабочее решение. Не примитивное, а минималистичное и логичное.

DRY: Don't Repeat Yourself — Не повторяйся!

DRY — это про избавление от дублирования. Если один и тот же кусок кода повторяется — его нужно вынести в отдельную функцию, класс или модуль. Это не только уменьшает размер кода, но и ускоряет поддержку: меняешь в одном месте — все работает.

Как применять DRY:
▫️ Выделяй повторяющуюся логику в переиспользуемые блоки
▫️ Избегай копипаста — он приводит к ошибкам
▫️ Держи один источник истины для каждой логики или сущности
   
DRY помогает держать код чистым, логичным и легко изменяемым.

YAGNI: You Aren’t Gonna Need It — Тебе это не понадобится!

YAGNI напоминает: не пиши код «на будущее», если в нем нет реальной необходимости прямо сейчас. То, что может вдруг пригодиться, скорее всего не пригодится вовсе. А если и пригодится, то в другой форме.

Что говорит YAGNI:
▫️ Делай только то, что нужно прямо сейчас
▫️ Не добавляй фичи «на будущее», если на них нет задачи
▫️ Сфокусируйся на реальных требованиях, а не на гипотетических сценариях
    
Чем меньше «запасного» кода — тем чище проект, и ,соответственно, меньше багов сейчас и в будущем.

Если ты уже пользуешься этими принципами, ставь ❤️
13🔥41🥴1
Привет, это Катя, Flutter Dev Friflex. Сегодня расскажу про архитектурный подход MVP — один из способов организации кода в приложениях.

Что такое MVP
MVP расшифровывается как Model — View — Presenter и разделяет логику приложения на три компонента:

🔴Model (Модель) — отвечает за данные и бизнес-логику. Она получает или сохраняет информацию, например, из API или локальной базы данных
🔴View (Представление) — пассивный слой, отображающий данные на экране. View не содержит логики — она лишь сообщает презентеру о действиях пользователя и отображает то, что пришло от него
🔴Presenter (Презентер) — посредник между моделью и представлением. Он обрабатывает пользовательские действия, запрашивает данные из модели, форматирует их и передает обратно в View

Поток коммуникации
➡️Пользователь взаимодействует с View (например, нажимает кнопку)
➡️View передает событие презентеру
➡️Презентер запрашивает или обновляет данные в модели
➡️Получив данные, презентер подготавливает их и отправляет обратно во View для отображения

View — самый поверхностный слой, он работает только с презентером, не зная ничего о модели напрямую. Это облегчает тестирование и делает код более модульным.

Пример
Представим экран входа в приложение:
🔸View: показывает два текстовых поля (логин и пароль) и кнопку «Войти». Передает ввод пользователю презентеру при нажатии на кнопку
🔸Presenter: получает логин и пароль, проверяет их (например, что они не пустые), отправляет их в модель
🔸Model: выполняет запрос к серверу, возвращает результат (успешный вход или ошибка)
🔸Presenter: получает результат, сообщает View показать следующий экран или отобразить сообщение об ошибке

Почему это удобно?
🔴Презентер содержит бизнес-логику и отделен от UI
🔴View можно переиспользовать или подменять, не затрагивая логику
🔴Легко писать юнит-тесты, особенно для презентера и модели
🔴Каждый View связан с одним презентером (но при необходимости может использовать несколько)

На блок-схеме можно посмотреть, как компоненты взаимодействуют между собой⬆️
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥62👎1
Привет! С вами вновь Анна, Friflex Flutter Team Lead👋

Flutter-проекты поддерживают три основных режима сборки — debug, profile и release. Сегодня поговорим о каждом из них и разберемся, чем они отличаются и когда их лучше использовать.

◾️Debug-режим
Дословно переводится как «режим отладки». Этот режим самый популярный в работе разработчиков.

Именно debug-режим позволяет запустить приложение на симуляторах, эмуляторах и физических устройствах под полным контролем выполнения кода. В нем легко можно отслеживать значения переменных, останавливать выполнение по breakpoints (точкам останова).

Кроме этого, режим отладки поддерживает hot reload, что позволяет вам без пересборки приложения протестировать измененный код.
Еще одна очень удобная способность debug-режима — вы можете в рантайме подключать дополнительные инструменты, такие как DevTools и Flutter inspector.

◾️Profile-режим
Этот режим — что-то среднее между debug- и release-режимами. Визуально на физическом устройстве profile сборка не особо отличается от release, но в ней сохраняется возможность подключить некоторые иструменты, например, DevTools.

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

Если ваше приложение работает медленно, требуется проверить и оптимизировать сложные анимации, сборка в режиме profile — ваш лучший помощник.

Но здесь важно помнить, что profile-режим не поддерживается эмуляторами и симуляторами. Такие сборки стоит запускать на физических устройствах, чтобы отследить реальную производительность.

◾️Release-режим
Это режим выпуска. Сборки именно в этом режиме загружаются в сторы, поступают конечному пользователю и передаются тестировщикам, если не было запроса на другой режим.

Сборка в release-режиме максимально оптимизирована, в отличие от debug- и profile-сборок. Подключить дополнительные инструменты к такой сборке невозможно, вся отладочная информация полностью очищается, прервать выполнение программы нельзя.

Как запустить сборку в разных режимах?
Все очень просто! Стандартная команда flutter run по умолчанию запускает сборку в debug-режиме. Чтобы запустить в profile-режиме, стоит добавить опцию --profile, в release-режиме — --release.

flutter run 
flutter run --profile
flutter run --release


📎Еще больше деталей — в официальной документации Flutter.
8🔥5❤‍🔥1
Небольшая проверка на внимательность. Какой режим лучше использовать для анализа производительности анимаций?
Anonymous Quiz
14%
Debug
74%
Profile
12%
Release
Всем привет! Это Роза, Flutter Dev в Friflex👋

Мы запускаем серию постов о принципах объектно-ориентированного программирования (ООП). Вы скажете, что это база, но и ее нужно иногда повторять. Поехали🚀

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

Что такое ООП?
Объектно-ориентированное программирование (ООП) — это парадигма, основанная на использовании объектов, которые содержат данные (в виде полей) и поведение (в виде методов).

Звучит сложно? Если проще: объект — это экземпляр класса, а класс описывает структуру и поведение этих объектов.

ООП помогает:
✔️ структурировать код
✔️ переиспользовать функциональность
✔️ облегчить масштабирование и поддержку проекта

Основные принципы ООП:
▫️ наследование
▫️ инкапсуляция
▫️ полиморфизм
▫️ абстракция
    
Сегодня разберем наследование.

Что такое наследование?
Наследование — это механизм, который позволяет одному классу (дочернему) унаследовать свойства и методы другого класса (родительского).

Представьте, что вы пишете посты для Telegram. У вас есть базовый класс Article с методами write(), publish() полями title и author:

class Article {
const Article(this.title, this.author);
  final String title;
  final String author;

  void write() => print('Статья $title ...');
  void publish() => print('Публикация статьи ...');
}


Вместо дублирования логики — создаем дочерний класс и наследуем поведение:

class TutorialArticle extends Article {
  const TutorialArticle(super.title, super.author, this.topic);
   /// Дочерние классы могут вызывать конструкторы родительского класса
  /// с помощью ключевого слова `super`, передавая необходимые параметры.
  /// Это важно для инициализации базового состояния.
  final String topic;

  void explain() => print('Объяснение темы: $topic');

  @override
  void publish() {
    super.publish(); // сохраняем поведение родителя
    print('Дополнительные шаги для публикации туториала по теме: $topic');
  }
}


Используем:

void main() {
  final article = TutorialArticle('ООП в Dart', 'Роза', 'Наследование');
  article.write();       // унаследовано
  article.publish();     // унаследовано
  article.explain();     // новое поведение
}


В итоге:
▫️Базовый класс — содержит общую функциональность
▫️Дочерний класс — расширяет или переопределяет поведение

А что насчет наследования в dart?
В dart наследование реализуется через ключевое слово extends:

class Child extends Parent { ... }


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

Кроме того, Dart использует mixin-based inheritance. Класс может наследовать только один суперкласс (extends), но может использовать несколько миксинов (with) для добавления поведения.
    
Кстати, в Dart можно не только наследовать реализацию через extends, но и реализовывать интерфейсы с помощью implements. Тогда ты сам переопределяешь все методы, но это уже не наследование поведения.

Композиция vs наследование
Наследование — не единственный способ повторного использования кода. Есть композиция, когда один объект включается в другой, как поле. Такой подход часто делает код более гибким и читаемым. Об этом — в следующих постах.

В двух словах:
▪️extends — наследуем реализацию (и можем ее переопределить)
▪️implements — реализуем интерфейс (обязаны реализовать все вручную)
▪️with — подключаем миксины (многоразовые наборы методов)
▪️ композиция — объединяем объекты, как части (вложенность, а не наследование)

На этом все! В следующем посте — инкапсуляция. Подписывайтесь, чтобы не пропустить💙
9👍8🔥6
Привет, это Катя, Flutter Dev Friflex👋Сегодня мы продолжим разбирать базовые принципы ООП, и на очереди — инкапсуляция.

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

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

В языке Dart инкапсуляция реализуется через:
▫️Приватные переменные (с помощью _)
▫️Публичные методы и геттеры/сеттеры для доступа к данным

Зачем нужна инкапсуляция?
Инкапсуляция делает код:
✔️безопасным — защищает данные от нежелательных изменений
✔️удобным — скрывает детали реализации, оставляя только то, что нужно пользователю класса
✔️контролируемым — ты решаешь, как и при каких условиях можно изменять данные

Пример инкапсуляции

class BankAccount {
  double _balance = 0.0;
  double get balance => _balance;
  void deposit(double amount) {
    if (amount > 0) _balance += amount;
  }
  void withdraw(double amount) {
    if (amount > 0 && amount <= _balance) _balance -= amount;
  }
}

void main() {
  final account = BankAccount();
  account.deposit(100); 
  account.withdraw(30);
  print(account.balance);
}


Что тут происходит:
▪️_balance — скрытая реализация. Никто не может напрямую изменить баланс
▪️balance — только для чтения. Пользователь может узнать, сколько денег на счете
▪️deposit и withdraw — контролируют доступ к изменению данных (инкапсуляция в действии)

Надеюсь, стало немного яснее, как работает инкапсуляция и почему она так важна. В следующем посте разберем еще один принцип ООП — абстракцию.
🔥86
Какой баланс у пользователя из примера⬆️
Anonymous Quiz
4%
100.0
88%
70.0
5%
0.0
2%
– 30.0
5👍5
Привет! Это Анна, Friflex Flutter Team Lead👋

Продолжаем разбирать основные принципы объектно-ориентированного программирования. В предыдущих постах мы уже поговорили о наследовании и инкапсуляции. Сегодня рассмотрим еще один принцип — абстракцию.

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

Чем помогает абстракция?
1. Упрощает работу с кодом. Код становится более структурированным и понятным для восприятия
2. Позволяет обезопасить реализации. Все специфические детали реализации скрыты от внешнего воздействия. Это позволяет их сохранить в первозданном виде и снизить вероятность проблем по месту использования
3. Дает возможность безопасно масштабировать проект. Абстракция позволяет снизить зависимость некоторых модулей кода, что упрощает его расширение

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

Подведем итог в виде простого примера реализации абстракции в Dart-коде.
У нас есть задача создать абстракцию оплаты и две реализаци — оплаты по карте и наличными. В самом простом виде получим такой результат.

/// Абстрактный класс — интерфейс оплаты
abstract class Payment {
void pay(double amount);
}

/// Реализация оплаты картой
class CardPayment implements Payment {
@override
void pay(double amount) {
final fee = _calculateFee(amount);
print(
'Оплата $amount руб. с помощью карты. Комиссия составила $fee руб. Итого — ${amount + fee} руб.',
);
}

/// Расчет комиссии за оплату картой - 2%
double _calculateFee(double amount) {
return amount * 0.02;
}
}

/// Реализация оплаты наличными
class CashPayment implements Payment {
@override
void pay(double amount) {
print('Оплата $amount руб. наличными');
}
}

void main() {
final Payment payment1 = CardPayment();
final Payment payment2 = CashPayment();

payment1.pay(
500,
); // Оплата 500 руб. с помощью карты. Комиссия составила 10 руб. Итого — 510 руб.
payment2.pay(200); // Оплата 200 руб. наличными
}


❤️
если было полезно
15❤‍🔥4🔥2
Всем привет! Это Роза, Flutter Dev в Friflex!👋

Продолжаем наше путешествие по миру объектно-ориентированного программирования. Мы уже рассмотрели наследование, инкапсуляцию и абстракцию. Теперь настало время для полиморфизма

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

Полиморфизм буквально означает «много форм». Представьте: у вас есть базовый класс, который описывает общее поведение, а в классах-наследниках вы можете переопределить это поведение в соответствии с конкретной реализацией. Это и есть полиморфизм.

Полиморфизм в Dart
В Dart можно выделить два основных типа полиморфизма:

1. Полиморфизм на основе наследования. Дочерние классы наследуют поведение родителя и могут его переопределять

2. Полиморфизм через интерфейсы. Класс реализует интерфейс и обязан описать все его методы. Это позволяет задать единое поведение для разных реализаций

Помните пример со статьями из поста про наследование? Тогда я создала базовый класс с общими методами. Представим, что материалов стало больше. Теперь хочется разделить их по категориям.

class Article {
  const Article(this.title, this.author);

  final String title;
  final String author;

  void write() => print('Пишем статью: "$title"');
  void publish() => print('Публикация статьи "$title", автор: $author');
  void section() => print('Общий раздел');
}


Теперь создадим подклассы с разными разделами:

class OOPArticle extends Article {
  const OOPArticle(String title, String author) : super(title, author);

  @override
  void section() => print('🔹 Раздел: Объектно-ориентированное программирование');
}

class PatternsArticle extends Article {
  const PatternsArticle(String title, String author) : super(title, author);

  @override
  void section() => print('🔸 Раздел: Паттерны проектирования');
}



Теперь мы можем использовать их полиморфно:

void printArticleInfo(Article article) {
  article.write();
  article.section();
  article.publish();
}

void main() {
  final articles = [
    OOPArticle('Что такое полиморфизм?', 'Роза'),
    PatternsArticle('Паттерн Singleton', 'Роза'),
  ];

  for (final article in articles) {
    printArticleInfo(article);
    print('---');
  }
}

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

Кроме классического полиморфизма существуют и другие его формы:
▫️Полиморфизм подтипов (Subtype polymorphism)
Это наиболее привычный вариант, когда один класс наследуется от другого и переопределяет его поведение. Именно его чаще всего имеют в виду, говоря о полиморфизме в ООП.

▫️Параметрический полиморфизм (Generics)
Позволяет писать универсальный код, работающий с разными типами данных. Например:

T identity<T>(T value) =>  value;

void main() {
  print(identity<int>(10));       // 10
  print(identity<String>('Да'));  // Да
}


Здесь функция identity не зависит от конкретного типа — она просто возвращает то, что получила. Тип указывается как параметр <T>.

▫️Ad hoc полиморфизм
Это перегрузка методов, когда у функции одно имя, но разные параметры. Но в Dart такая перегрузка не поддерживается напрямую.

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

Но есть важное правило:
Не объединяйте все подряд в общее поведение. Интерфейс или базовый класс должен описывать действительно общее поведение. Иначе вы рискуете нарушить принцип единственной ответственности (SRP).

На этом все! Если понравилась статья, то ставьте 💙
8🔥2
This media is not supported in your browser
VIEW IN TELEGRAM
Располагаемся ближе к экрану — Friflex выпустили Flutter Starter. И это —🔥

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

Что вы получаете?
Чистая архитектура: Presentation / Domain / Data — без лишнего хаоса
Локализация, темы, модульность — ready
Стиль кода, который не стыдно показать на ревью
Разделение на сервисы для портирования на разные платформы
Простой DI без лишних пакетов, настроенный навигатор

Для кого?
Для всех, кто хочет собирать приложения быстрее, чище, удобнее.

🔗Качайте, меняйте название пакета и начинайте добавлять фичи:
GitHub Friflex Flutter Starter

Пусть меньше времени уходит на настройку — и больше на создание крутых проектов💜
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥239👍8❤‍🔥2
Привет, это Катя, Flutter Dev в Friflex 👋 Сегодня поговорим про Observer.

Что такое Observer?
Паттерн «Наблюдатель» (Observer) — это поведенческий паттерн проектирования, суть которого в том, что один объект (наблюдаемый) может автоматически уведомлять другие объекты (наблюдатели) о своих изменениях.

Это похоже на подписку: один объект «подписывается» на другой, чтобы быть в курсе изменений. Когда наблюдаемый объект изменяется — все подписчики получают уведомление и могут отреагировать.

Где это полезно?

Этот паттерн часто используется, когда:
▫️у нас есть модель данных, и мы хотим автоматически обновлять UI
▫️нужно разделить логику обработки и отображения данных
▫️необходимо уведомить несколько частей системы об одном событии

Как реализовать Observer?
✔️
Создаем абстракцию Observable (или Subject) — объект, за которым наблюдают
✔️Создаем абстракцию Observer — наблюдатель
✔️Реализуем логику хранения списка наблюдателей в Observable
✔️Реализуем метод update в Observer, чтобы обрабатывать изменения

Пример:
abstract class Observer {
  void update(int data);
}

class Observable {
  final _observers = <Observer>[];
  int _value = 0;

  void addObserver(Observer observer) => _observers.add(observer);
  void removeObserver(Observer observer) => _observers.remove(observer);

  set value(int newValue) {
    _value = newValue;
    _notify();
  }

  void _notify() {
    for (final o in _observers) {
      o.update(_value);
    }
  }
}

class LoggerObserver implements Observer {
  @override
  void update(int data) => print('Updated value: $data');
}

void main() {
  final observable = Observable();
  final observer = LoggerObserver();

  observable.addObserver(observer);

  observable.value = 1;
  observable.value = 2;

  observable.removeObserver(observer);
}


Что тут происходит:
▪️Observer — абстрактный класс с методом update, который вызывается при изменении состояния
▪️Observable — содержит список наблюдателей и уведомляет их при изменении значения переменной value
▪️LoggerObserver — конкретный наблюдатель, который просто печатает обновленное значение

В main() мы добавляем наблюдателя, изменяем значение, и он автоматически получает уведомления. Потом мы его удаляем из списка — и уведомления больше не приходят.

Плюсы:
Слабая связанность — наблюдатель не зависит от внутренней логики наблюдаемого
Гибкость — можно динамически добавлять и удалять наблюдателей
Масштабируемость — несколько объектов могут реагировать на одно событие

Минусы:
⚠️ Перегрузка сообщений — большое количество наблюдателей может вызвать перегрузку
⚠️ Циклические зависимости — если наблюдатель и наблюдаемый зависят друг от друга
⚠️ Порядок уведомлений — не гарантируется порядок вызова update()

А во Flutter?
▫️
ChangeNotifier и ChangeNotifierProvider в Provider
▫️ValueNotifier + ValueListenableBuilder
▫️StreamBuilder, который реагирует на события стрима
▫️InheritedWidget и InheritedNotifier — это тоже про наблюдение за изменениями

Какой паттерн разобрать в следующий раз?🔍
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥114👍3
Всем привет! Это Анна, Friflex Flutter Team Lead💭

В предыдущем посте мы узнали о том, что такое паттерн Observer (Наблюдатель) и как его применять в Dart-коде. Сегодня продолжим обсуждение поведенческих шаблонов проектирования и поговорим о шаблоне Mediator (Посредник).

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

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

✈️Для простоты понимания представим посредником авиационную диспетчерскую вышку, а все остальные объекты в коде — самолетами. Самолеты не общаются друг с другом, так как это общение может привести к хаосу в полете. Всеми вопросами связи занимается именно диспетчер.

А с чем сравнить посредника во Flutter-приложении? Представим, что нам надо на трех разных экранах приложения сделать отображение значения счетчика. При этом счетчик должен быть общим.

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

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

class CounterMediator {
int _counter = 0;
final List<Function(int)> _listeners = [];

/// Значение счетчика
int get counter => _counter;

/// Добавляет слушателя, который будет уведомлен при изменении счетчика
void addListener(Function(int) listener) {
_listeners.add(listener);
}

/// Изменяет значение счетчика и уведомляет всех слушателей
void increment() {
_counter++;
for (final listener in _listeners) {
listener(_counter);
}
}
}


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

class HomeScreen extends StatefulWidget {
const HomeScreen({super.key, required this.mediator});

final CounterMediator mediator;

@override
State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
int _counter = 0;

@override
void initState() {
super.initState();
// Подписываемся на изменения посредника-счетчика
widget.iss.onediator.addListener((newCounter) {
setState(() => _counter = newCounter);
});
}

@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Счетчик: $_counter'),
ElevatedButton(
onPressed: widget.iss.onediator
.increment, // Вызываем метод увеличения значения счетчика
child: const Text('+'),
),
],
);
}
}


❤️
если пост был полезен
Please open Telegram to view this post
VIEW IN TELEGRAM
18🔥2❤‍🔥1💩1
Привет, это Роза, Flutter Dev Friflex!😉

Недавно я решала задачу — нужно было валидировать файлы при загрузке и вызывать для них метод convert. Сложность в том, что у меня было 4-5 форматов, и со временем их могло стать еще больше.

Какие были варианты решения: 
☠️ if-case
Просто перечислить все форматы и вызвать нужный метод:

if (format == FileFormat.arb) {
  validateArb(file);
} else if (format == FileFormat.csv) {
  validateCsv(file);
}
// ...
// И это только validate, а еще есть convert...


В целом это будет работать, но немного отдает болью и будущими проблемами в виде сложно поддерживаемого кода. 
Добавить новый формат — +1  условие. 
Протестировать — сложновато. 
Расширять — ну такое.  

Использовать паттерн проектирования
Я решила, что Стратегия — то, что нужно.

Что говорит теория?
Стратегия — поведенческий паттерн, который:
▫️ выделяет семейство алгоритмов
▫️ выносит каждый из них в отдельный класс
▫️ делает их взаимозаменяемыми

Проще говоря: если у тебя есть несколько вариантов поведения, которые могут меняться, стратегия позволяет:
▪️ вынести каждый вариант в отдельный класс
▪️ использовать их через единый интерфейс
▪️заменять поведение в рантайме без изменения основного кода

В жизни часто можно столкнуться со стратегией: выбор способа оплаты (карта, наличные), выбор транспорта (метро, такси, автобус).

Вернемся к задаче
Я определила общий интерфейс FileConversionStrategy, который описывает, как работать с файлом:

abstract class FileConversionStrategy {
  bool supports(FileFormat format);
  FutureOr<ImportFileModel> convert(
    ImportFileModel file,
    List<String> editedResult,
  );
  void validate(ImportFileModel file);
}


Затем написала реализации под каждый формат. Например, ArbConversionStrategy:

class ArbConversionStrategy implements FileConversionStrategy {
  @override
  bool supports(FileFormat format) => format == FileFormat.arb;

  @override
  FutureOr<ImportFileModel> convert(
    ImportFileModel file,
    List<String> editedResult,
  ) {
    // логика конвертации
  }

  @override
  void validate(ImportFileModel file) {
    // логика валидации
  }
}


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

Но это еще не все. У нас есть реализации, но как выбрать необходимую в нужный момент?

Для этого я написала менеджер стратегий. Он перебирает доступные реализации и возвращает ту, что подходит:

class FileConversionStrategyManager {
  final List<FileConversionStrategy> strategies;

  FileConversionStrategyManager(this.strategies);

  FileConversionStrategy getStrategy(FileFormat format) {
    return strategies.firstWhere(
      (strategy) => strategy.supports(format),
      orElse: () => throw UnsupportedError('No strategy for format $format'),
    );
  }
}


А теперь разберем, как все это работает вместе:

1. Контекст — запрашивает подходящую стратегию и работает с ней через единый интерфейс
2. Интерфейс — объединяет все алгоритмы в общую структуру (validate, convert)
3. Конкретные стратегии — реализуют обработку разных форматов (CSV, ARB, JSON и других)
4. Во время выполнения мы подбираем нужную стратегию
5. Можно легко заменить стратегию на новую

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

Это уже третий паттерн. Здесь подробнее про:
🔗Observer
🔗Mediator
Please open Telegram to view this post
VIEW IN TELEGRAM
8👍6🔥4💅1
Привет, с вами снова Катя, Flutter Dev Friflex!👋

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

Что такое typedef?
Typedef
позволяет дать имя какому-то типу. Это особенно удобно, когда тип громоздкий, например, функция с дженериками, списки функций или колбэки. Сложный тип описывается один раз и дальше используется по имени — лаконично и понятно.

Пример
При работе с БД часто встречаются такие сигнатуры:

Expression<bool> Function(MyTable tbl)

List<OrderingTerm Function(MyTable tbl)>


Чтобы не затаскивать drift в каждый файл и гадать, что за конструкция используется, были созданы именованные типы:

// Вынесенная фильтрация
typedef FilterExpression<Q extends BaseTable> = Expression<bool> Function(Q tbl);

// Вынесенный список для сортировки
typedef OrderByExpressions<Q extends BaseTable> = List<OrderingTerm Function(Q tbl)>;


Как использовать?
Вместо длинных сигнатур теперь в универсальных функциях можно использовать понятные имена:

Future<List<T>> fetchFilteredData<T extends BaseTable>({
  required FilterExpression<T> where,
  OrderByExpressions<T>? orderBy,
}) async {}


Гораздо чище и легче воспринимается при чтении и навигации по коду.

Вывод
Typedef
— небольшой, но полезный инструмент, который помогает писать аккуратный и поддерживаемый код. Особенно полезен при работе с абстракциями, как в случае с Drift, или при проектировании универсальных репозиториев.
Please open Telegram to view this post
VIEW IN TELEGRAM
12🔥5👍4
Привет! Это Анна, Friflex Flutter Team Lead💫

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

Управляемые состояния Flutter-приложения можно разделить на два вида — эфемерное состояние (Ephemeral state) и состояние приложения (App state).

➡️ Эфемерное состояние еще можно назвать локальным состоянием. Это понятие очень точно описывает саму суть — такой вид состояния управляет только одним виджетом и не распространяется вне его контекста. Оно «живет» только вместе с конкретным виджетом, к которому привязано. Официальная документация в пример приводит текущую страницу PageView или выбранную вкладку BottomNavigationBar.

Самая простая реализация эфемерного состояния — использование Stateful-виджета и метода setState().

Для еще более простого понимания рассмотрим пример кастомного текстового поля с кнопкой очистки, которая должна отображаться, когда поле заполняется текстом. Создадим Stateful-виджет CustomTextField, State которого будет выглядеть следующим образом:

class _CustomTextFieldState extends State<CustomTextField> {
late final TextEditingController controller;
bool showClearButton = false;

@override
void initState() {
super.initState();
controller = TextEditingController();
controller.addListener(
() => setState(() => showClearButton = controller.text.isNotEmpty),
);
}

@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
decoration: InputDecoration(
suffixIcon: showClearButton
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
controller.clear();
setState(() {
showClearButton = false;
});
})
: null,
),
);
}

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


Здесь мы видим очень простой пример эфемерного состояния. А что же такое состояние приложения?

➡️App state — это состояние, которое влияет на несколько виджетов. Может затрагивать и разные части приложения, например, разные экраны. Часто управление такими состояниями реализуется с помощью стейт менеджеров, например, Bloc или Redux.

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

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

📎Еще больше примеров и деталей можно найти в официальной документации.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥126👍4❤‍🔥1