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

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

В прошлом посте я рассказывала о принципе единственной ответственности (S в SOLID). Сегодня разбираем следующую букву — O (Open/Closed Principle), принцип открытости/закрытости.

Что такое Open/Closed Principle?
Принцип открытости/закрытости гласит: программные сущности (классы, модули, функции) должны быть открыты для расширения, но закрыты для модификации. Это означает, что вы должны иметь возможность добавлять новую функциональность без изменения существующего кода.

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

Пример
Нарушение OCP


class DiscountCalculator {
  double calculate(double price, String customerType) {
    if (customerType == 'regular') {
      return price * 0.95;
    } else if (customerType == 'premium') {
      return price * 0.90;
    } else if (customerType == 'vip') {
      return price * 0.80;
    }
    return price;
  }
}


Метод нужно менять при добавлении новых правил.

Правильное применение OCP

abstract class DiscountStrategy {
  double apply(double price);
}

class RegularDiscount implements DiscountStrategy {
  @override
  double apply(double price) => price * 0.95;
}

class PremiumDiscount implements DiscountStrategy {
  @override
  double apply(double price) => price * 0.90;
}

class VipDiscount implements DiscountStrategy {
  @override
  double apply(double price) => price * 0.80;
}

class DiscountCalculator {
  double calculate(double price, DiscountStrategy strategy) {
    return strategy.apply(price);
  }
}


Открыты для расширения, закрыты для модификации

Преимущества OCP
✔️
Меньше ошибок при внесении изменений
✔️Не требуется регрессионное тестирование существующего кода
✔️Более простой процесс расширения функциональности
✔️Повышение стабильности и надежности системы

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

В следующем посте я расскажу о принципе подстановки Барбары Лисков (L в SOLID).

Подписывайтесь, чтобы не пропустить
Please open Telegram to view this post
VIEW IN TELEGRAM
👍12🔥5🤔4
This media is not supported in your browser
VIEW IN TELEGRAM
🛫Привет! Это Анна, Flutter Team Lead Friflex

Hot Restart и Hot Reload — такие знакомые каждому Flutter-разработчику понятия. Без них сейчас разработку уже и сложно представить — они делают ее значительно быстрее и проще.

Несмотря на популярность этих понятий, начинающие разработчики часто их путают, не до конца понимают в чем их отличия и как они работают под капотом. Разберемся!

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

Hot Reload не перезапускает программу с нуля. Он лишь «подмешивает» измененный код в уже запущенный процесс. Состояние приложения сохраняется.

Под капотом работает так:
1. Вы вносите изменения в код и запускаете Hot Reload
2. Flutter изучает, какие именно файлы вы изменили
3. Затем перекомпилируются только важные части: библиотеки с измененным кодом, основная библиотека и все библиотеки, связанные с измененными
4. Обновленный код преобразуется в специальный kernel-файл и поступает в Dart VM
5. Dart VM подтягивает новые версии библиотек, при этом сохраняя текущее состояние приложения
6. Flutter вызывает перестройку тех виджетов, которые были затронуты изменениями

Еще больше внутрянки можно узнать в статье Станислава Чернышева.

В итоге вы видите измененный интерфейс, функции работают по-новому, но состояние не сброшено.

При использовании Hot Reload очень важно помнить, что не все изменения могут примениться корректно. Он точно вам не подойдет, если:
✔️ вы меняете структуру класса (добавляете/удаляете поля, меняете их типы, меняете конструкторы)
✔️ вы заменяете глобальные переменные и синглтоны (например, экземпляры глобальных сервисов, которые могут повлиять на всю работу приложения)
✔️ вы обновляете код, выполняемый до runApp()

Это только часть случаев, поэтому вам важно самостоятельно внимательно отслеживать, как ведет себя ваше приложение при ряде Hot Reload

🔄 Hot Restart полностью перезапускает приложение. При этом сбрасываются абсолютно все состояния и данные. Проще этот механизм представить перезапуском main() метода. Но важно, что сборка заново не пересобирается.

Под капотом работает так:
1. Вы вносите изменения в код и запускаете Hot Restart
2. Dart VM очищает все сохраненные состояния, все значения переменных, пересоздает все объекты заново
3. Заново запускается метод main()
4. Приложение запускается как будто с нуля, но не затрагивается нативный код и движок Flutter

Визуально вам кажется, что приложение было закрыто и собирается заново, но это не так. И это важно понимать, потому что Hot Restart не может вам помочь, если вы:
✔️ обновили нативный код
✔️ внесли изменения в pubspec.yaml, подключили новые плагины или обновили старые, добавили новые assets
✔️ изменили платформенные зависимости (Gradle/Pods)
✔️ поменяли работу платформенных каналов (например, изменили названия)
✔️ обновили версии Dart или Flutter

Во всех этих перечисленных случаях вам потребуется полная остановка процесса и повторный запуск.

Если сравнивать по времени выполнения:
◾️ Hot Reload считается самым быстрым
◾️ Hot Restart быстрый, но медленнее, чем Hot Reload
◾️ Full Restart самый медленный

❤️ — если было полезно
Please open Telegram to view this post
VIEW IN TELEGRAM
19🔥5🤩3👎1
This media is not supported in your browser
VIEW IN TELEGRAM
Разыгрываем книгу «Основы Flutter»

Побывали на презентации книги во время CrossConf и подписали ее у всех четверых авторов.

Как принять участие:
➡️Подписаться на канал @flutterfriendly
➡️Подписаться на каналы авторов книги @mobile_developing, @frezycode, @ftl_notes и @madteacher_channel
➡️Нажать «участвую» под этим постом

Узнаем победителя 4 декабря и отправим ему эксклюзивный вариант книги 🚀
Please open Telegram to view this post
VIEW IN TELEGRAM
8🔥6👍5
Всем привет, с вами Роза, Flutter Dev Friflex

Если вы когда-либо работали с данными, отправляли что-то на сервер или получали ответ от API, то наверняка сталкивались с jsonEncode и jsonDecode. Это методы из библиотеки dart:convert, которые отвечают за кодирование и декодирование данных. Давайте рассмотрим подробнее!

Библиотека dart:convert содержит конвертеры для JSON и UTF-8, а также инструменты для создания собственных преобразований.

Поскольку серверы не умеют работать с объектами Dart напрямую, данные перед отправкой нужно превратить в строку (обычно в JSON). Для этого используют jsonEncode, а чтобы получить структуру данных обратно из строки — jsonDecode. 

Например:

import 'dart:convert';

void main() {
  final data = {'name': 'Helen', 'age': 22};
  final encoded = jsonEncode(data);
  print(encoded); // {"name":"Helen","age":22}

  final decoded = jsonDecode(encoded);
  print(decoded['name']); // Helen
}


Нужно помнить, что jsonDecode возвращает значение типа dynamic: 
◾️для объекта это обычно Map<String, dynamic>, 
◾️для массива — List<dynamic>. 

Поэтому после декодирования полезно явно приводить типы, чтобы избежать ошибок:

final name = decoded['name'] as String; 


JSON поддерживает только простые типы: int, double, String, bool, null, List, Map<String, dynamic>. Когда в модели появляются вложенные объекты или кастомные типы, простая сериализация перестает работать.

class User {
  final String name;
  final int age;
  final Address address;

  User(this.name, this.age, this.address);
}


Когда jsonEncode обрабатывает объект User, он ищет стандартный способ преобразования каждого поля. Для String и int он его находит. Но для поля address типа Address Dart не имеет встроенного правила. В отсутствие метода toJson(), он выбрасывает UnsupportedError.

В таких ситуациях нужно вручную реализовать toJson()/fromJson() (рекурсивно, если структура вложенная):

Map<String, dynamic> toJson() => {
  'name': name,
  'age': age,
  'address': address.toJson(),
};
// и аналогично для fromJson.


То же самое касается и нестандартных типов: DateTime, enum, Uri и других. JSON их не понимает, поэтому вам нужно вручную преобразовывать их в строку:

'createdAt': createdAt.toIso8601String(), 
'status': status.name, // enum Status {online, offline}
'url': url.toString(),


Но ручное преобразование может быть рутинным и долгим, особенно если у вас огромный пакет, где 50+ моделей и сложные вложенные структуры. 

Для этого в Dart есть библиотеки. Наиболее популярные:
✔️json_serializable — генерирует toJson() и fromJson() 
✔️ freezed — генерирует иммутабельные модели, копирование, сравнение и также JSON-сериализацию

Пример с freezed выглядит так:

@freezed
class User with _$User {
  const factory User({
    required String name,
    required int age,
  }) = _User;

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}


После генерации сериализация сводится к паре строк:

final json = user.toJson();
final user2 = User.fromJson(json);


Но перед использованием библиотек советую прочитать их документацию, так как у них есть свои нюансы.

❤️ — если было полезно
13👍6🔥2
SOLID: Принцип подстановки Барбары Лисков (L)

Привет, с вами Катя, Flutter Dev Friflex. В предыдущем посте я рассказывала о принципе открытости/закрытости (O). Сегодня продолжаем серию и разбираем третью букву — L (Liskov Substitution Principle), принцип подстановки Барбары Лисков.​

Что такое Liskov Substitution Principle?
Принцип подстановки Лисков гласит: объекты подклассов должны быть взаимозаменяемы с объектами их базового класса без нарушения корректности работы программы. Другими словами, если класс B наследуется от класса A, то в любом месте программы, где используется класс A, можно безопасно подставить класс B, и программа продолжит работать корректно.

Почему это важно?
Нарушение LSP приводит к непредсказуемому поведению программы. Код начинает проверять типы объектов с помощью if/else или is, что противоречит принципу открытости/закрытости и делает систему хрупкой. Соблюдение LSP гарантирует, что полиморфизм работает правильно и подклассы действительно являются специализацией базового класса.

Пример нарушения LSP:

Плохо:

class Bird {
  void fly() {
    print('Flying');
  }
}

class Penguin extends Bird {
  @override
  void fly() {
    throw Exception('Cannot fly'); // Нарушение LSP
  }
}


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

Хорошо:

abstract class Bird {
  void move();
}

class Sparrow extends Bird {
  @override
  void move() {
    print('Flying');
  }
}

class Penguin extends Bird {
  @override
  void move() {
    print('Swimming');
  }
}


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

Принцип подстановки Лисков — это основа правильного полиморфизма. Подклассы должны расширять поведение базового класса, не ломая ожидания клиентского кода. Если вы не можете безопасно подставить подкласс вместо базового класса, значит, наследование используется неправильно — подумайте о композиции или другой абстракции.​

В следующем посте я расскажу о принципе разделения интерфейсов (I в SOLID).

А как вы работаете с наследованием в своих проектах? Сталкивались ли с нарушениями LSP? Делитесь в комментариях своим опытом применения принципа подстановки Лисков
6👍4🔥4
This media is not supported in your browser
VIEW IN TELEGRAM
👍Привет! С вами Анна, Flutter Team Lead Friflex

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

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

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

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

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

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

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

1️⃣ Добавьте механизм отправки отзыва из приложения
Реализация может быть любой: всплывающее уведомление, диалоговое окно, баннер на странице. Что действительно важно — запрос должен быть мягким. Не стоит сразу кидать юзера на сложную форму, лучше простыми вопросами подвести его к этому.

Для Flutter-проектов могу порекомендовать специальный плагин in_app_review. Он позволит интегрировать в приложение нативные окна для публикации отзыва.

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

3️⃣ Отправляйте позитивные отзывы из приложения в стор, а негативные — напрямую в техподдержку
Хитрость? Да. Но объективно для продвижения приложения это лучший вариант. Оценка в сторе не будет снижаться, а техподдержка может получить максимум информации сразу из приложения, ведь вместе с отзывом можно отправлять много другой дополнительной информации.

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

✍️Делитесь в комментариях самыми забавными, необычными и интересными отзывами, которые когда-либо получали ваши продукты!
Please open Telegram to view this post
VIEW IN TELEGRAM
10🔥9👍5🤩1
🪙Привет, это Роза, Flutter Dev Friflex

Каждое мобильное приложение живет своей собственной жизнью: его могут открыть, свернуть, перейти в другое приложение, вернуться через минуту или через час. Чтобы корректно реагировать на эти события, Flutter-приложение условно проходит через ряд состояний, которые определяются перечислением AppLifecycleState:
✔️ resumed —  приложение находится на переднем плане, видно на экране и готово к взаимодействию с пользователем
✔️ inactive — приложение временно неактивно, например, при поступлении звонка. Оно не получает событий от пользователя
✔️ paused — приложение уходит в фон, становится невидимым и не реагирует на действия пользователя. В это время стоит приостанавливать ресурсоемкие процессы
✔️ hidden — приложение скрыто от пользователя. Процесс остается в памяти и готов к быстрому возобновлению. На некоторых платформах это состояние заменяет paused
✔️ detached — приложение больше не активно и готовится к завершению

Чтобы отслеживать изменения этих состояний, используется WidgetsBindingObserver. Его метод didChangeAppLifecycleState вызывается каждый раз, когда система переводит приложение между состояниями. Соответственно, мы можем реагировать на его изменения: обновлять данные, останавливать процессы, освобождать ресурсы или, наоборот, перезапускать необходимые операции.

Для регистрации наблюдателя нужно добавить миксин WidgetsBindingObserver к классу, далее вызвать WidgetsBinding.instance.addObserver(this); в методе initState.

Работа с самим didChangeAppLifecycleState выглядит так:
@override  
void didChangeAppLifecycleState(AppLifecycleState state) {
  super.didChangeAppLifecycleState(state);
  setState(() {
    if (state == AppLifecycleState.resumed) {
      appState = 'Возобновлено';
    } else if (state == AppLifecycleState.inactive) {
      appState = 'Неактивно';
    } else if (state == AppLifecycleState.paused) {
      appState = 'Приостановлено';
    } else if (state == AppLifecycleState.detached) {
      appState = 'Отключено';
    } else if (state == AppLifecycleState.hidden) {
      appState = 'Скрыто';
    }
  });
}


Представим ситуацию: у нас подключены OTA-переводы, и требуется обновлять их как при запуске приложения, так и каждый раз, когда пользователь возвращается на экран из состояния hidden. Для этого в состоянии resumed мы повторно вызываем метод для их обновления:
class MyHomePage extends StatefulWidget { 
  const MyHomePage({Key? key}) : super(key: key); 

  @override 
  State<MyHomePage> createState() => _MyHomePageState(); 


class _MyHomePageState extends State<MyHomePage> 
    with WidgetsBindingObserver { 

  @override 
  void initState() { 
    super.initState(); 
    WidgetsBinding.instance.addObserver(this); 
    _updateTranslations(); 
  } 

  void _updateTranslations() { 
    ...
  } 

  @override 
  void didChangeAppLifecycleState(AppLifecycleState state) { 
    super.didChangeAppLifecycleState(state); 
    if (state == AppLifecycleState.resumed) { 
      _updateTranslations(); 
    } 
  }
}


При работе с жизненным циклом приложения стоит учитывать несколько моментов:
 ▪️ состояния paused и inactive могут вести себя по-разному на разных платформах, поэтому не всегда стоит запускать в них ресурсоемкие операции
▪️ в состоянии hidden приложение остается в памяти, но интерфейс еще может быть не готов, поэтому лучше не обращаться к виджетам до полной инициализации
▪️ метод didChangeAppLifecycleState вызывается в главном потоке UI, поэтому любые тяжелые операции стоит выполнять асинхронно или через отдельные сервисы, чтобы не блокировать интерфейс

❤️ — если было полезно
Please open Telegram to view this post
VIEW IN TELEGRAM
17👍13🔥8
This media is not supported in your browser
VIEW IN TELEGRAM
Привет, с вами Катя, Flutter Dev Friflex. В предыдущем посте я рассказывала о принципе подстановки Барбары Лисков (L). Сегодня разбираем четвертую букву — I (Interface Segregation Principle), принцип разделения интерфейса.

Что такое Interface Segregation Principle?
Принцип разделения интерфейса гласит: клиенты не должны зависеть от методов, которые они не используют. Другими словами, лучше иметь несколько узких специализированных интерфейсов, чем один универсальный «толстый» интерфейс, который заставляет классы реализовывать ненужные им методы.

Почему это важно?
Когда класс вынужден реализовывать методы, которые ему не нужны, это приводит к нескольким проблемам:​
▪️ Ненужные зависимости — класс зависит от функциональности, которую не использует
▪️ Пустые реализации — приходится писать заглушки или выбрасывать исключения
▪️ Сложность тестирования — нужно мокать методы, которые не используются
▪️ Нарушение SRP — класс получает ответственность, которая ему не нужна
▪️ Хрупкость кода — изменения в неиспользуемых методах могут сломать работу класса

Нарушение принципа
Рассмотрим классический пример:

// "Толстый" интерфейс с множеством методов
abstract class Worker {
  void work();
  void eat();
  void sleep();
}

// Человек использует все методы
class Human implements Worker {
  @override
  void work() {
    print('Человек работает');
  }
  
  @override
  void eat() {
    print('Человек ест');
  }
  
  @override
  void sleep() {
    print('Человек спит');
  }
}

// Робот вынужден реализовывать ненужные методы
class Robot implements Worker {
  @override
  void work() {
    print('Робот работает');
  }
  
  @override
  void eat() {
    throw Exception('Робот не ест!'); // Бесполезная реализация
  }
  
  @override
  void sleep() {
    throw Exception('Робот не спит!'); // Бесполезная реализация
  }
}


Проблема: интерфейс Worker слишком широкий. Робот вынужден реализовывать методы eat() и sleep(), которые ему не нужны, и либо бросает исключения, либо оставляет пустые реализации. Это нарушение ISP.

Правильное применение принципа
Разделим «толстый» интерфейс на несколько узких специализированных интерфейсов:

// Узкие интерфейсы с конкретными обязанностями
abstract class Workable {
  void work();
}

abstract class Eatable {
  void eat();
}

abstract class Sleepable {
  void sleep();
}

// Человек реализует все три интерфейса
class Human implements Workable, Eatable, Sleepable {
  @override
  void work() {
    print('Человек работает');
  }
  
  @override
  void eat() {
    print('Человек ест');
  }
  
  @override
  void sleep() {
    print('Человек спит');
  }
}

// Робот реализует только то, что ему нужно
class Robot implements Workable {
  @override
  void work() {
    print('Робот работает');
  }
}

// Использование
void makeWork(Workable worker) {
  worker.work();
}

void feedWorker(Eatable worker) {
  worker.eat();
}

void main() {
  final human = Human();
  final robot = Robot();
  
  makeWork(human); // Работает
  makeWork(robot); // Работает
  
  feedWorker(human); // Работает
  // feedWorker(robot); // Ошибка компиляции - правильно!
}


Теперь каждый класс реализует только те интерфейсы, которые ему нужны. Робот не притворяется, что умеет есть и спать. Принцип разделения интерфейса — это про создание чистых и понятных контрактов. Вместо универсальных «комбайнов» создавайте специализированные интерфейсы, каждый из которых отвечает за конкретную роль.

В следующем посте я расскажу о последнем принципе SOLID — принципе инверсии зависимостей (D).
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥97👍5
💎Привет! Это Анна, Flutter Team Lead Friflex

Перед публикацией любого приложения в Google Play и AppStore необходимо создать аккаунт разработчика (Developer Account). Этот аккаунт может быть двух видов — индивидуальный аккаунт и аккаунт юридического лица. Сегодня поговорим, чем оба аккаунта отличаются и какой стоит выбирать.

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

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

Недостатки:
Имеются существенные ограничения в доступах. Даже если вы получите роль администратора, многие важные функции вам останутся недоступны, например, выпуск идентификаторов, API-ключей и сертификатов
В опубликованной карточке приложения указывается имя владельца аккаунта, что влияет на репутацию продукта в целом. Больше доверия вызывают официальные организации
Доход поступает как доход физического лица
Требуется выполнение особых условий для публикации. Например, в Google Play при публикации новых приложений под аккаунтом физического лица требуется прохождение закрытого тестирования в течение 14 дней в составе группы не менее 12 тестировщиков.

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

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

Преимущества:
✔️ Доверие клиентов выше, так как они видят перед собой целую компанию, а не одного человека. Особенно это важно для серьезных коммерческих продуктов, например, B2B и финансовой сферы
✔️ Огромный выбор различных ролевых моделей в управлении проектом. Вы можете четко разграничивать права и зону ответственности через выдачу конкретной роли
✔️ Нет проблем с делегированием ключевых прав в доступах. В отличие от индивидуального аккаунта, сертификаты и прочее может выпускать не только владелец
✔️ Удобное ведение бизнеса, отслеживание поступающих платежей и аналитика приложения

Недостатки:
Сложно зарегистрировать аккаунт. При регистрации требуется много различных документов, а сам процесс может затянуться на пару недель
У сторов более строгие требования к соответствию внутренним правилам. Нарушения воспринимаются жестче, чем у индивидуальных аккаунтов
Если компания зарегистрирована в РФ, в сторе могут быть ограничения для такого разработчика

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

❤️ — если было полезно
Please open Telegram to view this post
VIEW IN TELEGRAM
17👍7🔥4🤩2🤝1
💭Привет! С вами Роза, Flutter Dev Friflex

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

desktop_drop
— это пакет для drag-and-drop на вебе и десктоп-платформах. Основной виджет пакета — DropTarget, который определяет область, на которую можно перетаскивать файлы. Сам по себе виджет невидим, он просто определяет область для перетаскивания вокруг своего child. Основные свойства DropTarget включают:
✔️ onDragEntered — вызывается, когда файл входит в область виджета
✔️ onDragExited — вызывается, когда файл покидает область виджета
✔️ onDragDone — вызывается после того, как файл был отпущен внутри области
✔️ onDragUpdated — вызывается при движении файла внутри области
✔️ child — виджет, который отображается внутри области DropTarget

DropTarget(
  onDragDone: (detail) {
    for (final file in detail.files) {
      print(file.path);
    }
  },
  onDragEntered: (detail) => print('Файл в области'),
  onDragExited: (detail) => print('Файл вне области'),
  child: // some child
)

Также можно визуально показывать, что область готова принять файл. Для этого необходимо использовать состояния, отслеживаемые через onDragEntered и onDragExited.

class _FileDropAreaState extends State<FileDropArea> {
  bool _isDragging = false;

  @override
  Widget build(BuildContext context) {
    return DropTarget(
      onDragEntered: (detail) {
        setState(() => _isDragging = true);
      },
      onDragExited: (detail) {
        setState(() => _isDragging = false);
      },
      onDragDone: (detail) {
       // onDragDone logic
      },
      child: //some child
    );
  }
}


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

Для полноценного функционала работы с файлами desktop_drop можно комбинировать с file_picker и другими пакетами.

❤️ — если было полезно
Please open Telegram to view this post
VIEW IN TELEGRAM
7👍2🔥1
❄️Привет, с вами Катя, Flutter Dev Friflex

В предыдущем посте я рассказывала о принципе разделения интерфейса (I). Сегодня разбираем последнюю букву — D (Dependency Inversion Principle), принцип инверсии зависимостей.

Что такое Dependency Inversion Principle?
Принцип инверсии зависимостей говорит о двух вещах:
✔️высокоуровневые модули не должны зависеть от низкоуровневых — и те, и другие должны зависеть от абстракций
✔️абстракции не должны зависеть от деталей, детали должны зависеть от абстракций.
По-простому: «верх» приложения (экран, бизнес-логика) не должен быть привязан к конкретным реализациям «низа» (HTTP‑клиент, база данных, SharedPreferences и другим), он должен зависеть только от интерфейсов.

Почему это важно?
Когда высокоуровневый код напрямую знает о конкретных классах нижнего уровня, это приводит к проблемам:

◾️Любое изменение реализации «внизу» (REST → gRPC, другая БД, кэш) требует правок в бизнес-логике
◾️Код сложно тестировать — приходится тянуть реальные репозитории/сети вместо заглушек
◾️Система становится хрупкой: одна деталь «внизу» ломает много кода «наверху»
◾️Нарушаются другие принципы SOLID — растет связанность, падает переиспользуемость.

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

Нарушение принципа
Рассмотрим типичный пример с авторизацией:

class AuthRepository {
  Future<void> login(String email, String password) async {
    // Здесь конкретная реализация:
    // HTTP-запрос, парсинг ответа, сохранение токена и т.д.
  }
}

class LoginViewModel {
  Future<void> login(String email, String password) async {
    final repo = AuthRepository(); // Жёсткая зависимость
    await repo.login(email, password);
  }
}


Проблемы:
✖️LoginViewModel сам создает AuthRepository и жестко на него завязан
✖️Нельзя легко подменить репозиторий в тестах (например, на фейковый, который не ходит в сеть)
✖️Любое изменение механизма авторизации требует лезть в LoginViewModel
✖️Высокоуровневый модуль (view model) зависит от конкретной детали (репозитория), а не от абстракции

Правильное применение принципа
Введем абстракцию и будем передавать зависимость извне (через конструктор):

// Абстракция (контракт), от которой зависит верхний уровень
abstract class IAuthRepository {
  Future<void> login(String email, String password);
}

// Низкоуровневая реализация для реального API
class NetworkAuthRepository implements IAuthRepository {
  @override
  Future<void> login(String email, String password) async {
    // HTTP, обработка ошибок, сохранение токена и т.д.
  }
}

// Другая реализация, например, фейковая для тестов
class FakeAuthRepository implements IAuthRepository {
  @override
  Future<void> login(String email, String password) async {
    // Ничего не делает или имитирует успешный логин
  }
}

// Высокоуровневый модуль зависит только от интерфейса
class LoginViewModel {
  final IAuthRepository _authRepository;

  LoginViewModel(this._authRepository);

  Future<void> login(String email, String password) async {
    await _authRepository.login(email, password);
  }
}


Теперь:
✔️LoginViewModel не знает, как именно реализован репозиторий — он видит только интерфейс
✔️В проде можно передать NetworkAuthRepository, в тестах — FakeAuthRepository
✔️Изменения в реализации репозитория не требуют правок в бизнес-логике
✔️Мы перевернули зависимость: конкретные реализации зависят от абстракции IAuthRepository, а не наоборот

Если вы видите в коде new/() внутри бизнес‑логики или ViewModel, создающий конкретные репозитории, сервисы и клиенты, — это хороший сигнал задуматься: не пора ли ввести интерфейс и развернуть зависимость?

✍️Повторим все принципы SOLID:
Single Responsibility Principle
Open/Closed Principle
Liskov Substitution Principle
Interface Segregation Principle
Dependency Inversion Principle
Please open Telegram to view this post
VIEW IN TELEGRAM
5👍4🔥4