5.7. IoC: библиотека Provider
Автор: Артём Ершов
В предыдущих статьях мы уже рассматривали понятие инверсии зависимостей и изучали реализацию этого паттерна с помощью InheritedWidget. Хотя это рабочий подход, он требует значительного объёма кода.
В этой статье мы разберём работу с пакетом provider, рассмотрим его основные сущности и поработаем с ним на практике.
Знакомство с пакетом provider
В первую очередь важно запомнить, что пакет provider — это надстройка над InheritedWidget. Для начала разберёмся, зачем нам могут потребоваться такие надстройки.
Вспомним пример из предыдущего статьи
/// InheritedWidget, позволяющий передать вниз по дереву информацию о контроллере.
class _MainWidgetInheritedWidget extends InheritedWidget {
final _MainWidgetState state;
_MainWidgetInheritedWidget({
required this.state,
required super.child,
});
@override
bool updateShouldNotify(_MainWidgetInheritedWidget oldWidget) => true;
}
/// MainWidget и MainWidgetState являются обёрткой, позволяющей хранить состояние counter.
class MainWidget extends StatefulWidget {
...
}
class MainWidgetState extends State<MainWidget> {
int get counter => _counter;
var _counter = 0;
void incrementCounter(){
setState(() {
_counter++;
})
}
@override
Widget build(BuildContext context) => _MainWidgetInheritedWidget(
state: this,
child: const Center(
child: CounterView(),
),
);
}
class CounterView extends StatelessWidget {
const CounterView({super.key});
@override
Widget build(BuildContext context) {
final mainState = context
.dependOnInheritedWidgetOfExactType<_MainWidgetInheritedWidget>()!
.state;
return Text('${mainState.counter}');
}
}
А теперь посмотрим, как мы можем реализовать этот же пример с использованием пакета provider.
Пример с использованием provider
// Создаём ChangeNotifier для управления состоянием счётчика.
class MainModel with ChangeNotifier {
int _counter = 0;
int get counter => _counter;
void increment() {
_counter++;
notifyListeners();
}
}
/// Передаём MainModel вниз по дереву с помощью ChangeNotifierProvider.
class MainWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MainModel(),
child: const Center(
child: CounterView(),
),
);
}
}
class CounterView extends StatelessWidget {
const CounterView({super.key});
@override
Widget build(BuildContext context) {
// Используем Consumer для получения состояния и автоматического обновления виджета.
return Consumer<MainModel>(
builder: (context, model, child) => Text('${model.counter}'),
);
}
}
Таким образом, с помощью пакета provider мы вынесли всю логику работы с счётчиком в виджет MainModel, встроили нашу модель в дерево благодаря ChangeNotifierProvider и организовали прослушивание нашей модели вместе с Consumer. Далее рассмотрим другие типы провайдеров и способы получения значений из них.
Основные концепции
Provider позволяет размещать объекты в структуре виджетов, обеспечивая к ним доступ для всех потомков.
Компонент упрощает управление жизненным циклом таких объектов: автоматически инициализирует их необходимыми данными и выполняет очистку после удаления из дерева виджетов. Пакет provider поставляется с набором классов для гибкой работы с различными типами данных:
Рассмотрим эти классы подробнее.
Provider
Это базовый класс пакета provider, который принимает объект и предоставляет потомкам доступ к нему. Если вы хотите воспользоваться одним из провайдеров, то в первую очередь обратите внимание на класс Provider. В примере ниже мы передаём объект конфигурации AppConfig вглубь по структуре, чтобы отобразить appName и apiEndpoint в виджете AppInfo.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// Объект конфигурации.
class AppConfig {
final String apiEndpoint;
final String appName;
AppConfig({required this.apiEndpoint, required this.appName});
}
void main() {
runApp(
Provider<AppConfig>(
create: (context) => AppConfig(
apiEndpoint: 'https://api.example.com',
appName: 'My Awesome App',
),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Provider Example')),
body: Center(child: AppInfo()),
),
);
}
}
class AppInfo extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Получаем доступ к объекту AppConfig.
final config = Provider.of<AppConfig>(context);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('App Name: ${config.appName}'),
Text('API Endpoint: ${config.apiEndpoint}'),
],
);
}
}
Результат выполнения
ListenableProvider
Этот тип провайдера подходит для работы с Listenable. ListenableProvider прослушивает объект и автоматически уведомляет подписанные на провайдер виджеты о необходимости перестроиться.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// Модель данных, реализующая Listenable.
class Counter extends ValueNotifier<int> {
Counter() : super(0);
void increment() {
value++;
}
}
void main() {
runApp(
// Используем ListenableProvider для управления состоянием.
ListenableProvider<Counter>(create: (context) => Counter(), child: MyApp()),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('ListenableProvider Example')),
body: Center(child: CounterDisplay()),
floatingActionButton: IncrementButton(),
),
);
}
}
class CounterDisplay extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Слушаем изменения в Counter и обновляем UI.
final counterValue = context.watch<Counter>().value;
return Text('Count: $counterValue', style: TextStyle(fontSize: 24));
}
}
class IncrementButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FloatingActionButton(
onPressed: () {
// Получаем объект Counter и вызываем increment.
context.read<Counter>().increment();
},
child: Icon(Icons.add),
);
}
}
Результат выполнения
ChangeNotifierProvider
Это модификация ListenableProvider для работы с ChangeNotifier. Он даёт доступ к методу notifyListeners и автоматически вызовет метод ChangeNotifier.dispose.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// Модель данных, расширяющая ChangeNotifier для уведомления слушателей об изменениях.
class Counter with ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // Уведомляет всех слушателей о том, что состояние изменилось.
}
}
void main() {
runApp(
// Используем ChangeNotifierProvider для управления состоянием.
ChangeNotifierProvider(create: (context) => Counter(), child: MyApp()),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('ChangeNotifierProvider Example')),
body: Center(child: CounterDisplay()),
floatingActionButton: IncrementButton(),
),
);
}
}
class CounterDisplay extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Слушаем изменения в Counter и обновляем UI.
int counterValue = context.watch<Counter>().count;
return Text('Count: $counterValue', style: TextStyle(fontSize: 24));
}
}
class IncrementButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FloatingActionButton(
onPressed: () {
// Получаем объект Counter и вызываем increment.
context.read<Counter>().increment();
},
child: Icon(Icons.add),
);
}
}
Результат выполнения
StreamProvider
Прослушивает Stream и предоставляет доступ к последнему добавленному значению.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// Асинхронный генератор чисел.
// Генерирует новое значение раз в секунду, начиная с 0.
Stream<int> counterStream() async* {
int counter = 0;
while (true) {
await Future.delayed(Duration(seconds: 1));
yield counter++;
}
}
void main() {
runApp(
// Используем StreamProvider для работы с потоком.
StreamProvider<int>(
create: (context) => counterStream(),
initialData:
0, // Начальные данные, до того как в Stream добавится первое значение.
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('StreamProvider Example')),
body: Center(child: CounterDisplay()),
),
);
}
}
class CounterDisplay extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Получаем текущее значение счётчика из StreamProvider.
int counterValue = context.watch<int>();
return Text('Count: $counterValue', style: TextStyle(fontSize: 24));
}
}
Результат выполнения
FutureProvider
Прослушивает Future и обновляет потомков, когда Future выполнится.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// Имитация функции, которая загружает данные асинхронно.
// Тут может быть запрос в Сеть или локальную БД.
Future<int> fetchData() async {
// Симулируем задержку в 2 секунды.
await Future.delayed(Duration(seconds: 2));
// Возвращаем значение, как будто оно было получено из Сети.
return 42;
}
void main() {
runApp(
// Используем FutureProvider для загрузки данных асинхронно.
FutureProvider<int>(
create: (context) => fetchData(),
initialData: -1, // Значение до завершения Future.
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('FutureProvider Example')),
body: Center(child: DataDisplay()),
),
);
}
}
class DataDisplay extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Получаем результат из FutureProvider.
int data = context.watch<int>();
// Если данные пока не загружены, показываем индикатор загрузки.
if (data == -1) {
return CircularProgressIndicator();
}
// Когда данные загружены, показываем их.
return Text('Loaded data: $data', style: TextStyle(fontSize: 24));
}
}
Результат выполнения
MultiProvider
При вводе множества значений в больших приложениях provider может быстро превратиться в довольно сложную конструкцию:
Provider<Something>(
create: (context) => Something(),
child: Provider<SomethingElse>(
create: (context) => SomethingElse(),
child: Provider<AnotherThing>(
create: (context) => AnotherThing(),
child: someWidget,
),
),
),
Такой код называют «адом вложенности» англ. nesting hell за огромное количество вложенных виджетов. Избежать этой проблемы при работе с provider можно с помощью MultiProvider:
MultiProvider(
providers: [
Provider<Something>(create: (context) => Something()),
Provider<SomethingElse>(create: (context) => SomethingElse()),
Provider<AnotherThing>(create: (context) => AnotherThing()),
],
child: someWidget,
)
Однако MultiProvider меняет только внешний вид кода — структура виджетов будет одинакова в обоих случаях.
Конструкторы
Для всех типов провайдеров доступно несколько конструкторов, отвечающих за различное поведение передаваемого объекта.
Для создания нового провайдера используйте базовый конструктор, передав объект в функцию create():
Provider(
create: (context) => MyModel(),
child: ...
)
В этом случае provider будет управлять жизненным циклом MyModel.
Если требуется только встроить объект в дерево виджетов без управления жизненным циклом (например, при передаче провайдера, созданного на предыдущей странице базовым конструктором), воспользуйтесь именованным конструктором Provider.value:
MyModel model;
Provider.value(
value: model,
child: ...
)
Получение значения
В примерах выше мы часто использовали метод watch для работы с провайдером. Однако это не единственный способ. Вы также можете использовать:
- Статический метод
Provider.of<T>(BuildContext. - Расширения контекста:
—context.read<T>— одноразовое получение данных типаT(эквивалентноProvider.of<T>(context, listen: false));
—context.watch<T>— подписка на измененияTс перестроением виджета при обновлении значения (эквивалентноProvider.of<T>(BuildContext));
—context.select<T, R>(R cb(T value))— выбор конкретного параметра изTс последующей подпиской на его изменения.
Эти методы возвращают значение ближайшего провайдера типа T, расположенного выше в структуре виджетов. Если нужный провайдер не найден, возникает ошибка. Операции, как и в случае с InheritedWidget, выполняются за O(1), так как не требуют обхода дерева виджетов.
Вы также можете получить значение провайдера, используя виджеты Consumer и Selector. Их функционал аналогичен context.watch<T> и context.select<T, R>(R cb(T value)) соответственно. Подробнее работу с этими виджетами мы рассмотрим в этой статье позже.
Пример
Для дальнейшего изучения пакета provider создадим базовый пример, который будем постепенно расширять.
Рассмотрим пример приложения — список задач. Наш прототип будет включать модели состояния на основе ChangeNotifier, инициализацию провайдеров через MultiProvider, использование Consumer и Selector для оптимизации перестроек, а также тесты для проверки взаимодействия с интерфейсом.
Управление состоянием
Важно понимать, что провайдер не занимается непосредственным управлением состоянием. Эта ответственность возложена на другие компоненты системы — в нашем примере используется ChangeNotifier. Аналогичные функции могут выполнять ValueListenable или BLoC.
Сам провайдер выступает инструментом для связывания этих зависимостей между собой и с UI. Он позволяет:
- подписаться на изменения стейта;
- получать отдельные сущности из дерева.
Для начала создадим модели данных.
Реализуем модель задачи со следующей структурой:
|
Сущность |
Название |
Тип данных |
|
Заголовок задачи |
|
строка |
|
Уникальный идентификатор |
|
строка |
|
Статус |
|
булевая переменная |
Также добавим метод copyWithCompleted для упрощения изменения статуса модели Todo.
class Todo {
final String id;
final String title;
final bool completed;
Todo({required this.id, required this.title, this.completed = false});
Todo copyWithCompleted({required bool completed}) => Todo(id: this.id, title: this.title, completed: completed);
}
Реализуем модель для управления состоянием списка задач, наследуя ChangeNotifier.
ChangeNotifier — это класс, предоставляющий механизм уведомления слушателей об изменениях состояния. Наследование ChangeNotifier даёт доступ к методу notifyListeners(). Этот метод служит связующим звеном между источником данных (классом ChangeNotifier) и его потребителями (слушателями), обеспечивая синхронизацию состояний и актуальность отображаемой информации.
import 'package:flutter/foundation.dart';
class TodoListModel extends ChangeNotifier {
List<Todo> _todos = [];
List<Todo> get todos => _todos;
void addTodo(Todo todo) {
_todos.add(todo);
notifyListeners();
}
void removeTodo(String id) {
_todos.removeWhere((todo) => todo.id == id);
notifyListeners();
}
}
Модель TodoListModel реализует ChangeNotifier, что позволяет автоматически обновлять интерфейс при изменении состояния списка задач.
Внедрение зависимостей с provider
Вспомним, что provider — это надстройка над InheritedWidget, упрощающая передачу данных вниз по структуре виджетов. Благодаря механизму передачи данных вниз provider становится эффективным инструментом для внедрения зависимостей в UI вашего приложения. Реализация работает по следующему принципу: в дерево помещается виджет типа provider, в поле create которого передаётся метод для создания модели данных, требуемой на более низких уровнях структуры.
Для того чтобы использовать TodoListModel в нашем приложении, необходимо инициализировать ChangeNotifierProvider в функции main(). Это позволит предоставлять доступ к модели состояния из любой части приложения.
Внутри функции main() создаём экземпляр ChangeNotifierProvider и передаём ему экземпляр TodoListModel в поле create:
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => TodoListModel(),
child: MyApp(),
),
);
}
Реализация виджета MyApp может выглядеть следующим образом:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Todo List App',
home: TodoListScreen(),
);
}
}
Теперь, когда у нас есть инициализированный ChangeNotifierProvider в main.dart, мы можем использовать его для получения доступа к TodoListModel в наших виджетах. Это позволит нам отображать список задач и управлять им.
Создадим виджет TodoListScreen, который будет отображать список задач. Этот виджет будет использовать Provider.of<T> для получения доступа к модели TodoListModel.
Реализация TodoListScreen
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class TodoListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final todoListModel = Provider.of<TodoListModel>(context);
return Scaffold(
appBar: AppBar(
title: Text('Todo List'),
),
body: ListView.builder(
itemCount: todoListModel.todos.length,
itemBuilder: (context, index) {
final todo = todoListModel.todos[index];
return ListTile(
title: Text(todo.title),
subtitle: Text('Completed: ${todo.completed}'),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
todoListModel.addTodo(Todo(id: DateTime.now().toString(), title: 'New Todo'));
},
child: Icon(Icons.add),
),
);
}
}
Теперь мы рассмотрим, как обновлять состояние списка задач и заставлять наше приложение реагировать на эти изменения. Мы уже видели, как добавлять задачи, но также нужно уметь удалять и обновлять их состояние.
В модели TodoListModel у нас уже есть методы для добавления и удаления задач. Давайте добавим метод для обновления статуса выполнения задачи.
class TodoListModel extends ChangeNotifier {
// Написанный ранее код.
void updateCompleted({required String id, required bool completed}) {
final index = _todos.indexWhere((t) => t.id == id);
if (index != -1) {
_todos[index] = _todos[index].copyWithCompleted(completed: completed);
notifyListeners();
}
}
}
Теперь мы можем добавить возможность обновлять задачу в нашем виджете TodoListScreen. Для этого добавим кнопку для отметки задачи как выполненной.
Обновлённая реализация TodoListScreen
class TodoListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final todoListModel = Provider.of<TodoListModel>(context);
return Scaffold(
appBar: AppBar(
title: Text('Todo List'),
),
body: ListView.builder(
itemCount: todoListModel.todos.length,
itemBuilder: (context, index) {
final todo = todoListModel.todos[index];
return ListTile(
title: Text(todo.title),
subtitle: Text('Completed: ${todo.completed}'),
trailing: Checkbox(
value: todo.completed,
onChanged: (bool? newValue) {
todoListModel.updateCompleted(
id: todo.id,
completed: newValue ?? false,
);
},
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
todoListModel.addTodo(Todo(id: DateTime.now().toString(), title: 'New Todo'));
},
child: Icon(Icons.add),
),
);
}
}
Теперь наше приложение может не только добавлять задачи, но и обновлять их статус выполнения.
Использование MultiProvider
Теперь мы добавим ещё одну модель состояния, например UserModel, которая будет управлять данными о текущем пользователе. Для этого воспользуемся MultiProvider, чтобы инициализировать обе модели состояния в main.dart.
Создадим класс UserModel, который будет управлять данными о пользователе и наследоваться от ChangeNotifier.
class UserModel extends ChangeNotifier {
String _username = '';
String get username => _username;
void setUsername(String username) {
_username = username;
notifyListeners();
}
}
В функции main используем MultiProvider для инициализации TodoListModel и UserModel.
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => TodoListModel()),
ChangeNotifierProvider(create: (context) => UserModel()),
],
child: MyApp(),
),
);
}
Теперь мы можем использовать UserModel в наших виджетах для получения данных о пользователе.
class ProfileScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final userModel = Provider.of<UserModel>(context);
return Scaffold(
appBar: AppBar(
title: Text('Profile'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Username: ${userModel.username}'),
ElevatedButton(
onPressed: () {
userModel.setUsername('NewUsername');
},
child: Text('Update Username'),
),
],
),
),
);
}
}
Теперь у нас есть возможность управлять данными о пользователе отдельно от списка задач. На следующих этапах мы рассмотрим использование Consumer и Selector для оптимизации перестроек виджетов.
Использование Consumer и Selector
Для оптимизации перестроек виджетов и улучшения производительности приложения можно использовать Consumer и Selector из пакета provider.
Consumer позволяет получить значение от Provider, когда у нас нет контекста, который является потомком указанного Provider, и поэтому мы не можем использовать Provider.of<T>. Такая проблема обычно возникает, когда создающий Provider виджет одновременно один из его потребителей.
Пример:
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => Foo(),
child: Text(Provider.of<Foo>(context).value),
);
}
В этом примере возникнет исключение ProviderNotFoundException, потому что Provider.of<T> вызывается с BuildContext, который является предком Provider. Вместо этого мы можем использовать виджет Consumer, который будет вызывать Provider.of<T> со своим собственным BuildContext.
Используем Consumer. Тогда предыдущий пример будет выглядеть следующим образом:
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => Foo(),
child: Consumer<Foo>(
builder: (context, foo, child) => Text(foo.value),
},
);
}
Другая проблема, которую решает Consumer, — иногда может перестраиваться больше виджетов, чем необходимо:
@override
Widget build(BuildContext context) {
return FooWidget(
child: BarWidget(
bar: Provider.of<Bar>(context),
),
);
}
В приведённом выше коде только BarWidget зависит от значения, возвращаемого Provider.of<T>. Но когда Bar изменяется, то и BarWidget, и FooWidget будут перестроены.
В идеале следует перестраивать только BarWidget . Один из способов добиться этого — использовать Consumer:
@override
Widget build(BuildContext context) {
return FooWidget(
child: Consumer<Bar>(
builder: (context, bar, child) => BarWidget(bar: bar),
),
);
}
В этой ситуации, если бы Bar нужно было обновить, только BarWidget перестроили бы заново.
Мы также можем решить и обратный пример:
@override
Widget build(BuildContext context) {
return Consumer<Foo>(
builder: (context, foo, child) => FooWidget(foo: foo, child: child),
child: BarWidget(),
);
}
В этом примере BarWidget создается вне builder. Затем экземпляр BarWidget передается builder в качестве последнего параметра.
Это означает, что при повторном вызове builder с новыми значениями новый экземпляр BarWidget создаваться не будет. Это позволяет Flutter знать, что ему не нужно перестраивать BarWidget. Следовательно, в такой конфигурации только FooWidget будет перестраиваться при изменении Foo.
Selector же дополнительно позволяет выбирать поля объекта, на изменения которых он будет реагировать:
Selector<Foo, Bar>(
selector: (context, foo) => foo.bar,
builder: (context, data, child) {
return Text('${data.item}');
}
)
Пример использования Consumer в виджете TodoListScreen:
class TodoListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<TodoListModel>(
builder: (context, todoListModel, child) {
return Scaffold(
appBar: AppBar(
title: Text('Todo List'),
),
body: ListView.builder(
itemCount: todoListModel.todos.length,
itemBuilder: (context, index) {
final todo = todoListModel.todos[index];
return ListTile(
title: Text(todo.title),
subtitle: Text('Completed: ${todo.completed}'),
trailing: Checkbox(
value: todo.completed,
onChanged: (bool? newValue) {
todoListModel.updateCompleted(
id: todo.id,
completed: newValue ?? false,
);
},
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
todoListModel.addTodo(Todo(id: DateTime.now().toString(), title: 'New Todo'));
},
child: Icon(Icons.add),
),
);
},
);
}
}
Тестирование с provider
Для обеспечения качества и надёжности приложения важно проводить тестирование. Подробнее о тестировании мы расскажем в одном из следующих статей. Сейчас рассмотрим возможности тестирования с помощью provider. Этот пакет позволяет проверять взаимодействие виджетов с моделями состояния. Например, вы можете проверить, что добавление новой задачи приводит к обновлению списка задач.
Для начала требуется настроить тестовый виджет:
await tester.pumpWidget(
ChangeNotifierProvider(
create: (context) => TodoListModel(),
child: MyApp(),
),
);
Далее воспользуемся методами tester.tap и tester.pump для симуляции нажатия на кнопку и перестройки дерева виджетов после взаимодействия, тогда наш тестовый файл будет выглядеть следующим образом:
void main() {
testWidgets('TodoListScreen adds todo', (WidgetTester tester) async {
await tester.pumpWidget(
ChangeNotifierProvider(
create: (context) => TodoListModel(),
child: MyApp(),
),
);
// Проверяем, что заголовок приложения отображается.
expect(find.text('Todo List'), findsOneWidget);
// Проверяем, что новой задачи ещё нет в списке.
expect(find.text('New Todo'), findsNothing);
// Нажимаем на кнопку добавления задачи.
await tester.tap(find.byIcon(Icons.add));
await tester.pump(); // Перестраиваем дерево виджетов.
// Проверяем, что новая задача появилась в списке.
expect(find.text('New Todo'), findsOneWidget);
});
}
В этой статье мы изучили пакет provider и выяснили, какие преимущества он даёт по сравнению с использованием InheritedWidget. Рассмотрели различные типы провайдеров и сценарии их корректного применения. Также разобрали, как:
- избежать «ада вложенности» с помощью
MultiProvider; - оптимизировать перестроения UI через
ConsumerиSelector; - организовать тестирование приложений, использующих пакет
provider.