11 шаблонов проектирования Flutter, которые сделают ваш код в 5 раз более поддерживаемым

11 шаблонов проектирования Flutter, которые сделают ваш код в 5 раз более поддерживаемым

FlutterPulse

Эта статья переведена специально для канала FlutterPulse. В этом канале вы найдёте много интересных вещей, связанных с Flutter. Не забывайте подписываться! 🚀

АРХИТЕКТУРА FLUTTER

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

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

Главный разработчик на Flutter DevSummit сказал что-то, что изменило мой подход навсегда: "Шаблон проектирования, который вы выбираете в начале, либо укрепит, либо обездвижит ваше приложение, когда оно масштабируется." — Flutter DevSummit 2023

Она была права. После анализа нашего кода я обнаружил, что 70% наших ошибок произошло из-за плохих архитектурных решений, принятых на ранних стадиях.

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

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

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

Если вы хотите строить приложения Flutter, которые не рухнут под своей сложностью, когда они растут, продолжайте читать.

️ 1. BLoC (Бизнес-логический компонент) — разделение бизнес-логики от UI. Шаблон BLoC, вероятно, является первым продвинутым шаблоном проектирования, с которым сталкиваются большинство разработчиков Flutter, и это не удивительно: он обеспечивает разделение проблем.

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<IncrementEvent>((event, emit) => emit(state + 1));
    on<DecrementEvent>((event, emit) => emit(state - 1));
  }
}

Преимущества:

  • Разделяет бизнес-логику от кода UI
  • Значительно упрощает тестирование
  • Отлично подходит для сложного управления состоянием
  • Реактивный подход хорошо подходит для перестроения виджетов Flutter

Недостатки:

  • Более крутая кривая обучения для начинающих
  • Может быть многословным для простых изменений состояния
  • Требует дополнительных пакетов и boilerplate

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

Реальная речь: я когда-то рефакторил монолитное приложение в 10 000 строк с помощью шаблона BLoC. Ошибки уменьшились на 60%, и добавление новых функций стало в 3 раза быстрее.

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

class CounterProvider with ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

Преимущества:

  • Простой API с минимальным количеством кода
  • Рекомендуется официальной командой Flutter
  • Легко понять и реализовать
  • Отлично подходит для внедрения зависимостей

Недостатки:

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

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

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

3. Redux — Предсказуемый контейнер состояния Когда состояние вашего приложения становится сложным, Redux предлагает высоко предсказуемый поток данных в одном направлении.

class AppState {
  final int counter;
  AppState({this.counter = 0});
}
enum Actions { Increment, Decrement }
AppState reducer(AppState state, dynamic action) {
  if (action == Actions.Increment) {
    return AppState(counter: state.counter + 1);
  }
  return state;
}

Преимущества:

  • Единый источник истины для состояния
  • Отладка во времени
  • Предсказуемые изменения состояния
  • Отлично подходит для сложных взаимодействий состояния

Недостатки:

  • Многословный по сравнению с другими решениями
  • Крутая кривая обучения
  • Много кода-болванки

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

Я переписал коммерческое приложение с использованием Redux, и это полностью исключило ошибки, связанные с состоянием, которые мучили нас в течение месяцев.

4. MVVM (Model-View-ViewModel) — Чистое разделение для тестирования MVVM обеспечивает четкое разделение между интерфейсом пользователя (View), бизнес-логикой (ViewModel) и данными (Model).

class UserViewModel {
  final UserRepository _repository;

  UserViewModel(this._repository);

  Future<List<User>> getUsers() async {
    return await _repository.fetchUsers();
  }
}

Преимущества:

  • Ясное разделение проблем
  • Высокая тестируемость архитектуры
  • Знакомо разработчикам с других платформ
  • Хорошо работает с привязкой данных

Недостатки:

  • Может ввести ненужную сложность для простых приложений
  • Требует тщательной реализации, чтобы избежать утечки абстракций
  • Иногда создает код-болванку

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

После внедрения MVVM для нашего медицинского приложения, покрытие тестирования увеличилось с 30% до 85% всего за две недели.

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

class UserRepository {
  final ApiService _api;
  final LocalDatabase _db;

  Future<List<User>> getUsers() async {
    try {
      final users = await _api.fetchUsers();
      await _db.saveUsers(users);
      return users;
    } catch (e) {
      return await _db.getUsers(); // Откат к сохраненным данным
    }
  }
}

Плюсы:

  • Абстрагирует детали реализации источника данных
  • Облегчает переключение источников данных
  • Централизует логику доступа к данным
  • Идеален для стратегий offline-first

Минусы:

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

Лучше всего подходит для: Приложений с несколькими источниками данных или которым необходимо обрабатывать offline-сценарии. Избегайте при: Ваше приложение имеет очень простые требования к данным.

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

💉 6. Внедрение зависимостей — Связывание для тестируемости DI позволяет компонентам получать свои зависимости, а не создавать их, что делает код более модульным и тестируемым.

class GetIt {
  static final GetIt I = GetIt._internal();
  factory GetIt() => I;
  GetIt._internal();

  void setup() {
    registerSingleton<ApiService>(ApiServiceImpl());
    registerFactory<UserRepository>(() => UserRepositoryImpl(get<ApiService>()));
  }

  T get<T>() { /*...*/ }
}

Плюсы:

  • Значительно облегчает тестирование
  • Содействует слабой связи между компонентами
  • Упрощает переключение реализаций
  • Позволяет осуществлять надлежащее управление жизненным циклом

Минусы:

  • Добавляет сложность и bojлерплейт
  • Может быть трудно отлаживать
  • Чрезмерно для небольших приложений

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

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

⚙️ 7. Шаблон фабрики — Динамическое создание объектов Когда вам необходимо создавать объекты без указания их точного класса, шаблон фабрики бесценен.

abstract class Button {
  Widget build();
}
class ButtonFactory {
  static Button createButton(ButtonType type) {
    switch (type) {
      case ButtonType.elevated:
        return ElevatedButtonImpl();
      case ButtonType.outlined:
        return OutlinedButtonImpl();
      default:
        return TextButtonImpl();
    }
  }
}

Преимущества:

  • Создает объекты без раскрытия логики создания
  • Позволяет принимать решения во время выполнения о типах объектов
  • Содействует повторному использованию кода
  • Инкапсулирует логику инстанцирования

Недостатки:

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

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

Я использовал шаблон Factory для обработки вариаций тем во всем приложении, сократив темозависимый код на 40%.

8. Singleton — Контроль единственного экземпляра Когда вам нужно ровно один экземпляр класса во всем приложении, Singleton обеспечивает согласованность.

class Logger {
  static final Logger _instance = Logger._internal();

  factory Logger() => _instance;

  Logger._internal();

  void log(String message) {
    print('LOG: $message');
  }
}

Преимущества:

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

Недостатки:

  • Может затруднить тестирование
  • Часто неправильно используется или чрезмерно используется
  • Вводит глобальное состояние
  • Может скрыть зависимости

Лучше всего подходит для: Сервисов, которые логически должны иметь единственный экземпляр (ведение журнала, подключения к базе данных). Избегайте, когда: Нет логической причины иметь только один экземпляр.

Я перераотал нашу систему аналитики, чтобы использовать Singleton, и устранить многочисленные ошибки согласованности, вызванные несколькими экземплярами трекера.

9. Composite Pattern — Деревоподобные структуры, упрощенные Шаблон Composite позволяет составлять объекты в деревоподобные структуры для представления иерархий.

abstract class Component {
  void render();
}
class Leaf implements Component {
  final String name;
  Leaf(this.name);

  @override
  void render() {
    print('Leaf: $name');
  }
}
class Composite implements Component {
  final List<Component> _children = [];

  void add(Component component) {
    _children.add(component);
  }

  @override
  void render() {
    for (var child in _children) {
      child.render();
    }
  }
}

Преимущества:

  • Упрощает работу с деревоподобными структурами
  • Клиенты могут единообразно обрабатывать отдельные объекты и композиции
  • Облегчает добавление новых типов компонентов
  • Естественно подходит для иерархий компонентов интерфейса

Недостатки:

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

Лучше всего подходит для: UI с сложными вложенными компонентами или любой树оподобной структурой данных. Избегайте, когда: Ваша иерархия компонентов проста и фиксирована.

Я использовал паттерн Composite для редактора документов с вложенными разделами, сократив код рендеринга на 60%.

🔄 10. Паттерн Observer — Ясное реактивное программирование Когда один объект должен уведомлять другие об изменениях, паттерн Observer создает чистую систему подписки.

class Subject {
  final List<Observer> _observers = [];

  void addObserver(Observer observer) {
    _observers.add(observer);
  }

  void notify(String event) {
    for (var observer in _observers) {
      observer.update(event);
    }
  }
}
abstract class Observer {
  void update(String event);
}

Преимущества:

  • Устанавливает четкие зависимости один-ко-многим
  • Поддерживает широковещательную связь
  • Децублирует субъекты от наблюдателей
  • Основой для реактивного программирования

Недостатки:

  • Может вызывать непредвиденные обновления и побочные эффекты
  • Утечки памяти, если наблюдатели не удаляются правильно
  • Последовательность обновлений может быть непредсказуемой

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

Я переписал инструмент реального времени с помощью паттерна Observer, и баги синхронизации уменьшились на 80%.

🎭 11. Паттерн Strategy — Алгоритмы, которые можно заменить на лету Паттерн Strategy позволяет определить семейство алгоритмов и сделать их взаимозаменяемыми.

abstract class SortStrategy {
  List<int> sort(List<int> data);
}
class QuickSort implements SortStrategy {
  @override
  List<int> sort(List<int> data) {
    // Реализация
    return sortedData;
  }
}
class DataProcessor {
  SortStrategy _sortStrategy;

  DataProcessor(this._sortStrategy);

  void changeSortStrategy(SortStrategy sortStrategy) {
    _sortStrategy = sortStrategy;
  }

  List<int> process(List<int> data) {
    return _sortStrategy.sort(data);
  }
}

Преимущества:

  • Позволяет переключать алгоритмы во время выполнения
  • Избегает взрыва условий
  • Содействует переиспользованию кода
  • Облегчает добавление новых стратегий

Недостатки:

  • Увеличивает количество объектов в системе
  • Клиент должен знать о разных стратегиях
  • Может быть чрезмерным, когда алгоритмы не меняются

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

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

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

Вот моя структура принятия решений:

Для небольших приложений (< 5 экранов, простое состояние):

  • Поставщик для управления состоянием
  • Простые репозитории, если необходимо
  • Минимальные паттерны в целом

Для средних приложений (5–15 экранов, умеренная сложность):

  • BLoC или MVVM для состояния и разделения
  • Паттерн репозитория для источников данных
  • Фабрика и Стратегия, где это уместно

Для крупных приложений (15+ экранов, высокая сложность):

  • Комбинация BLoC, Repository и DI
  • Осторожное применение Composite и Observer
  • Стратегическое использование всех паттернов на основе конкретных потребностей

Помните, что сказал старший инженер Flutter в Google I/O: "Признак зрелого разработчика Flutter не в том, чтобы знать каждый паттерн, а в том, чтобы знать, когда каждый паттерн стоит своей сложности."

Перестаньте слепо следовать туториалам по архитектуре и начните строить с правильными паттернами для ваших конкретных задач.

Какой паттерн проектирования вы будете реализовывать в своем следующем проекте Flutter?

Если вы испытываете трудности с эволюцией архитектуры Flutter, когда ваше приложение растет, проверьте мой предстоящий курс "Мастерство архитектуры Flutter", где мы строим приложение, готовое к производству, используя эти паттерны в гармонии.

Report Page