Что такое ScopedModel и когда его использовать?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Что такое ScopedModel и когда его использовать?
ScopedModel — это устаревший паттерн управления состоянием в Flutter. Хотя сейчас существуют более современные решения (Provider, Riverpod, Bloc), знание ScopedModel остается важным для понимания эволюции state management в Flutter и работы с старыми проектами.
Что такое ScopedModel?
ScopedModel — это паттерн, который позволяет:
- Создать модель состояния (Model)
- Предоставить её дочерним виджетам через ScopedModelDescendant
- Обновлять 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 экосистемы.