← Назад к вопросам

Что такое ScopedModel и когда его использовать?

2.0 Middle🔥 121 комментариев
#State Management

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что такое ScopedModel и когда его использовать?

ScopedModel — это устаревший паттерн управления состоянием в Flutter. Хотя сейчас существуют более современные решения (Provider, Riverpod, Bloc), знание ScopedModel остается важным для понимания эволюции state management в Flutter и работы с старыми проектами.

Что такое ScopedModel?

ScopedModel — это паттерн, который позволяет:

  1. Создать модель состояния (Model)
  2. Предоставить её дочерним виджетам через ScopedModelDescendant
  3. Обновлять UI при изменении модели
ScopedModel предоставляет данные через иерархию виджетов

MyApp
 └─ ScopedModel<CounterModel>
     ├─ ScopedModelDescendant → видит CounterModel
     ├─ ScopedModelDescendant → видит CounterModel
     └─ Child
         └─ ScopedModelDescendant → видит CounterModel

Структура ScopedModel

1. Создаем Model (класс состояния)

import 'package:scoped_model/scoped_model.dart';

class CounterModel extends Model {
  int _counter = 0;
  
  int get counter => _counter;
  
  void increment() {
    _counter++;
    notifyListeners(); // Оповещаем слушателей об изменении
  }
  
  void decrement() {
    _counter--;
    notifyListeners();
  }
}

2. Предоставляем модель приложению

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  final counterModel = CounterModel();
  
  @override
  Widget build(BuildContext context) {
    return ScopedModel<CounterModel>(
      model: counterModel,
      child: MaterialApp(
        home: HomeScreen(),
      ),
    );
  }
}

3. Используем модель в виджетах

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Counter')),
      body: Center(
        child: ScopedModelDescendant<CounterModel>(
          builder: (context, child, model) {
            return Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text('Counter: ${model.counter}'),
                SizedBox(height: 20),
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    ElevatedButton(
                      onPressed: model.decrement,
                      child: Text('-'),
                    ),
                    SizedBox(width: 20),
                    ElevatedButton(
                      onPressed: model.increment,
                      child: Text('+'),
                    ),
                  ],
                ),
              ],
            );
          },
        ),
      ),
    );
  }
}

ScopedModelDescendant vs ScopedModel.of

Вариант 1: ScopedModelDescendant (рекомендуется)

ScopedModelDescendant<CounterModel>(
  builder: (context, child, model) {
    return Text('${model.counter}');
  },
)

Вариант 2: ScopedModel.of (менее популярный)

class DisplayCounter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final model = ScopedModel.of<CounterModel>(context);
    return Text('${model.counter}');
  }
}

Полный пример: Todo приложение

// Model
class Todo {
  final String id;
  final String title;
  bool completed;
  
  Todo({
    required this.id,
    required this.title,
    this.completed = false,
  });
}

class TodoModel extends Model {
  List<Todo> _todos = [
    Todo(id: '1', title: 'Learn Flutter'),
    Todo(id: '2', title: 'Build App'),
  ];
  
  List<Todo> get todos => _todos;
  
  int get completedCount => _todos.where((t) => t.completed).length;
  
  void addTodo(String title) {
    _todos.add(Todo(
      id: DateTime.now().toString(),
      title: title,
    ));
    notifyListeners();
  }
  
  void toggleTodo(String id) {
    final index = _todos.indexWhere((t) => t.id == id);
    if (index != -1) {
      _todos[index].completed = !_todos[index].completed;
      notifyListeners();
    }
  }
  
  void removeTodo(String id) {
    _todos.removeWhere((t) => t.id == id);
    notifyListeners();
  }
}

// App
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ScopedModel<TodoModel>(
      model: TodoModel(),
      child: MaterialApp(
        home: TodoScreen(),
      ),
    );
  }
}

// UI
class TodoScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Todos'),
      ),
      body: ScopedModelDescendant<TodoModel>(
        builder: (context, child, model) {
          return Column(
            children: [
              Padding(
                padding: EdgeInsets.all(16),
                child: Text(
                  'Completed: ${model.completedCount}/${model.todos.length}',
                ),
              ),
              Expanded(
                child: ListView.builder(
                  itemCount: model.todos.length,
                  itemBuilder: (context, index) {
                    final todo = model.todos[index];
                    return ListTile(
                      title: Text(todo.title),
                      leading: Checkbox(
                        value: todo.completed,
                        onChanged: (_) {
                          model.toggleTodo(todo.id);
                        },
                      ),
                      trailing: IconButton(
                        icon: Icon(Icons.delete),
                        onPressed: () {
                          model.removeTodo(todo.id);
                        },
                      ),
                    );
                  },
                ),
              ),
            ],
          );
        },
      ),
      floatingActionButton: AddTodoButton(),
    );
  }
}

class AddTodoButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      onPressed: () {
        final model = ScopedModel.of<TodoModel>(context, listen: false);
        model.addTodo('New Todo');
      },
      child: Icon(Icons.add),
    );
  }
}

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

Простота — легко начать ✅ Понятность — прямолинейный паттерн ✅ Работает — для простых приложений ✅ Минимум кода — не требует много boilerplate

Недостатки ScopedModel

Устаревший — больше не поддерживается активно ❌ Prop drilling — все равно нужно передавать контекст ❌ Нет type safety — могут быть ошибки при использовании ❌ Сложная отладка — непредсказуемые проблемы ❌ Плохая производительность — перестраивает лишние виджеты

Сравнение с современными подходами

ScopedModel (OLD):

ScopedModel<CounterModel>(
  model: CounterModel(),
  child: MyApp(),
)

// Использование
ScopedModelDescendant<CounterModel>(
  builder: (context, child, model) => Text('${model.counter}'),
)

Provider (MODERN):

final counterProvider = StateProvider<int>((ref) => 0);

// Использование
Consumer(
  builder: (context, ref, child) {
    final counter = ref.watch(counterProvider);
    return Text('$counter');
  },
)

Riverpod (BEST):

final counterProvider = StateProvider((ref) => 0);

// Использование
final counter = ref.watch(counterProvider);
return Text('$counter');

Когда МОЖНО использовать ScopedModel

  • Старые проекты — если проект уже использует ScopedModel
  • Очень простые приложения — для обучения
  • Legacy код — когда нельзя менять архитектуру
  • Понимание истории — для изучения эволюции Flutter

Когда НЕ использовать ScopedModel

❌ В новых проектах — используй Provider или Riverpod ❌ Для сложных приложений — нужна более мощная архитектура ❌ Если есть выбор — выбери современное решение

Миграция с ScopedModel на Provider

// ScopedModel (OLD)
class CounterModel extends Model {
  int _counter = 0;
  void increment() {
    _counter++;
    notifyListeners();
  }
}

// Provider (NEW)
final counterProvider = StateProvider<int>((ref) => 0);

// Использование
ref.read(counterProvider.notifier).state++;

Вывод

ScopedModel — это исторически важный паттерн, который показал, как можно управлять состоянием в Flutter. Однако:

  • В новых проектах используй Provider или Riverpod
  • Для старых проектов понимание ScopedModel помогает в поддержке
  • Для обучения полезно изучить, чтобы понять эволюцию

Сегодня это устаревший подход, но знание его остается полезным для работы с legacy кодом и понимания истории развития Flutter экосистемы.