Для чего нужен Scoped Model в Flutter?

Ответ

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

Как это работает:

  1. Создаётся модель, расширяющая класс Model.
  2. Модель помещается в дерево виджетов с помощью ScopedModel.
  3. Виджеты, которым нужно состояние, оборачиваются в ScopedModelDescendant и автоматически перестраиваются при вызове notifyListeners().

Пример из моего опыта — счётчик:

// Модель состояния
class CounterModel extends Model {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners(); // Уведомляет подписчиков об изменении
  }
}

// В корне приложения
ScopedModel(
  model: CounterModel(),
  child: MyApp(),
);

// Виджет, который использует состояние
ScopedModelDescendant<CounterModel>(
  builder: (context, child, model) {
    return ElevatedButton(
      onPressed: model.increment,
      child: Text('Count: ${model.count}'),
    );
  },
);

Почему я его выбирал:

  • Минимальный boilerplate-код — быстрее начать, чем с Provider или Bloc.
  • Автоматическое обновление UI — не нужно вручную вызывать setState.
  • Простота для небольших приложений — идеально для прототипов или проектов с 2-3 экранами.

Однако для сложных приложений с множеством моделей и бизнес-логикой я переходил на Provider или Riverpod из-за лучшей масштабируемости и тестируемости.

Ответ 18+ 🔞

А, Scoped Model, говоришь? Ну это такая штука, которая заставляет тебя вспомнить, что Flutter — это всё-таки про виджеты, ёпта. По сути, это как взять InheritedWidget, который обычно выглядит страшнее, чем хуй в пальто, и завернуть его в конфетку, чтобы не так пугало.

Как эта магия работает, если по-простому:

  1. Ты лепишь свою модель, которая наследуется от Model. Это типа хранилище твоего состояния.
  2. Потом ты эту модель засовываешь куда-то повыше в дерево виджетов, обернув всё в ScopedModel. Это как сказать: "Вот, дети, ваш папа-состояние, пользуйтесь".
  3. А дальше любой виджет, которому вдруг приспичило знать, что там в папе-состоянии, оборачивается в ScopedModelDescendant. И когда ты в модели крикнешь notifyListeners(), все эти виджеты-подсосы автоматически перерисуются. Волнение ебать, как просто!

Вот тебе живой пример — счётчик, классика жанра:

// Это наша модель, где живёт счётчик
class CounterModel extends Model {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners(); // Кричим всем: "Эй, обновитесь, пацаны!"
  }
}

// В корне приложения вешаем модель на всё дерево
ScopedModel(
  model: CounterModel(),
  child: MyApp(),
);

// А это виджет-потребитель, который слушает модель
ScopedModelDescendant<CounterModel>(
  builder: (context, child, model) {
    return ElevatedButton(
      onPressed: model.increment,
      child: Text('Count: ${model.count}'),
    );
  },
);

А почему я его вообще брал в работу?

  • Бойлерплейта — овердохуища меньше. По сравнению с тем же Bloc, где нужно городить события, состояния и прочую лапшу, тут всё в пару строк. Для быстрого прототипа — идеально.
  • UI сам обновляется. Не надо самому дёргать setState в каждом углу. Сказал notifyListeners() — и поехали.
  • Для мелких проектов — огонь. Если у тебя два экрана и одна модель, то это хуй с горы, а не архитектура. Всё понятно и быстро.

Но вот когда проект начинает расти, как на дрожжах, тут уже подозрение ебать чувствую. Моделей становится много, они начинают друг на друга ссылаться, логика расползается... И тут уже понимаешь, что доверия ебать ноль к этой простоте. Для серьёзных вещей я потом всё равно переползал на Provider или Riverpod — там и тестировать проще, и масштабируется всё куда лучше. А Scoped Model остаётся в памяти как милая, но немного манда с ушами для старта.