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", где мы строим приложение, готовое к производству, используя эти паттерны в гармонии.