Это вторая часть серии уроков по управлению состоянием Flutter с помощью пакета provider. В первой части мы узнали, как мы можем управлять состоянием наших виджетов из классов моделей, которые наследуются от ChangeNotifier
, а также об использовании ChangeNotifierProvider
для обеспечения нашего дерева виджетов этими классами, и, наконец, об использовании класса Consumer
для чтения этих изменений состояния и реакции на них.
📽 Видеоверсия доступна на YouTube и Odysee
В этой второй части мы углубимся в следующие концепции:
- Сначала мы посмотрим, как заставить несколько виджетов реагировать на изменения состояния на основе одного провайдера.
- Мы узнаем, как использовать провайдер в качестве фреймворка для инъекции зависимостей
- Наконец, мы будем использовать
MultiProvider
, чтобы сделать наш код более читабельным.
Если вы хотите узнать, как управлять состоянием с помощью провайдера, но еще не читали первую часть, я советую вам сначала посмотреть ее здесь. Если вы уже прочитали ее и готовы продолжить обучение, приступайте.
Управление состоянием нескольких виджетов с помощью одного провайдера состояния
Давайте начнем с тестового приложения из предыдущего урока, вы можете клонировать, скачать или форкнуть его отсюда.
У нас есть базовое приложение счетчика. Я хочу добиться того, чтобы иметь возможность хранить числовой счетчик и показывать его в различных виджетах, а также иметь возможность увеличивать его из всех этих виджетов.
Сначала скопируйте MyHomePage
из main.dart
в новый файл second_counter.dart
и переименуйте его в SecondCounter
, примерно так:
// lib/second_counter.dart
import 'package:flutter/material.dart';
import 'package:flutter_state_management_easy_provider/main_model.dart';
import 'package:provider/provider.dart';
class SecondCounter extends StatefulWidget {
const SecondCounter({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<SecondCounter> createState() => _SecondCounterState();
}
class _SecondCounterState extends State<SecondCounter> {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => MainModel(),
child: Consumer<MainModel>(
builder: (context, model, child) => Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'${model.counter}',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => model.counter++,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
),
),
);
}
}
Теперь добавьте новый элемент в Column
в MyHomePage
в виде кнопки, чтобы открыть этот новый экран:
// main.dart
// [...]
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => MainModel(),
child: Consumer<MainModel>(
builder: (context, model, child) => Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'${model.counter}',
style: Theme.of(context).textTheme.headline4,
),
// Add this button to open SecondCounter
Padding(
padding: const EdgeInsets.only(top: 20.0),
child: TextButton(
child: const Text('Open counter 2'),
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SecondCounter(
title: 'Second counter',
),
),
),
),
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => model.counter++,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
),
),
);
}
}
Попробуйте запустить приложение. Попробуйте увеличить значение счетчика, а затем перейдите на второй экран и попробуйте увеличить значение счетчика оттуда. Каков результат? Ну, у нас есть два экрана, и каждый из них с независимым значением, а это не то, чего мы хотим. Почему это происходит? Причина в том, что мы фактически создаем два экземпляра MainModel
, один в MyHomePage
и другой в SecondCounter
, каждый из которых предоставляется своим виджетам-потомкам через ChangeNotifierProvider
.
Для того чтобы иметь уникальный счетчик во всем приложении, нам придется переместить этот ChangeNotifierProvider
на один уровень вверх, пока оба экрана не повиснут на нем, например, мы можем разместить его прямо над MyHomePage
, в классе, который обертывает MaterialApp
(файл main.dart
):
// main.dart
// [...]
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<MainModel>(
create: (_) => MainModel(),
child: MaterialApp(
title: 'Flutter State Management Basic',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter State Management Basic'),
),
);
}
}
// [...]
Теперь удалите ChangeNotifierProvider
из MyHomePage
и SecondCounter
, чтобы методы build()
напрямую возвращали Consumer<MainModel>
.
Запустите приложение снова и попробуйте увеличить счетчики, вы увидите, что теперь есть только один уникальный счетчик. Это потому, что существует только один экземпляр MainModel
, и любой виджет ниже ChangeNotifierProvider
, который его предоставляет, может получить к нему доступ, чтобы использовать его состояние.
Провайдер как фреймворк для инъекции зависимостей
Инъекция зависимостей – это техника разработки программного обеспечения, при которой зависимости класса инжектируются извне, а не инстанцируются внутри него. Я мог бы написать несколько статей об этой технике и ее применении, но для того, чтобы понять друг друга и сосредоточиться на рассматриваемой теме, мы научимся использовать provider
для создания экземпляра и включения его в дерево виджетов, чтобы его можно было использовать из любой точки приложения.
Для этого я собираюсь привести пример пакета logs, который я использую, – logger. Я хочу создать экземпляр и записывать в журнал трассировку, когда счетчик увеличивается на первом экране, а также на втором экране, и я хочу, чтобы этот экземпляр был общим для всего приложения. Первым делом установите пакет:
flutter pub add logger
Сейчас мы будем использовать класс Provider
для выполнения этой задачи. Это основной класс, который дал название этому пакету, на самом деле и ChangeNotifierProvider
, и другие типы провайдеров, которые мы увидим в этой серии уроков, являются вариациями или формами этого оригинального класса:
// main.dart
// [...]
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<MainModel>(
create: (_) => MainModel(),
child: Provider<Logger>(
create: (_) => Logger(),
child: MaterialApp(
title: 'Flutter State Management Basic',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter State Management Basic'),
),
),
);
}
}
// [...]
Как видите, чтобы сделать “дыру” для этого нового провайдера, мы просто вставляем один в другой. Теперь мы собираемся изменить FloatingActionButton
каждого экрана, чтобы, помимо увеличения счета, печатать трассировку журнала. Чтобы получить этот Logger
, который мы только что создали, мы используем Provider.of<Logger>(context, listen: false)
. Этим вызовом мы указываем тип класса, который хотим получить, передаем текущий context
и указываем listen: false
. Мы увидим, почему последний аргумент имеет такое значение позже в этой серии уроков, а пока оставьте его в значении false
:
// main.dart
// Alter the existent FloatingActionButton:
floatingActionButton: FloatingActionButton(
onPressed: () {
model.counter++;
Provider.of<Logger>(context, listen: false).d(
'Increment counter from MyHomePage',
);
},
tooltip: 'Increment',
child: const Icon(Icons.add),
),
// second_counter.dart
// Alter the existent FloatingActionButton:
floatingActionButton: FloatingActionButton(
onPressed: () {
model.counter++;
Provider.of<Logger>(context, listen: false).d(
'Increment counter from SecondCounter',
);
},
tooltip: 'Increment',
child: const Icon(Icons.add),
),
Как вы видите, с помощью класса Provider
вы можете предоставить экземпляры классов, которые вы хотите, любому виджету вашего приложения.
Сделайте код более читабельным с помощью MultiProvider
То, что мы видели сегодня, очень хорошо и очень полезно, но обратите внимание, что обычные приложения не имеют только 2 зависимости, как в нашем примере, они могут иметь 5, 10, 100 или более различных зависимостей. Представьте, как каждая из них будет включена в следующую, как сейчас в MyApp
, это было бы очень сложно поддерживать и читать, и именно поэтому существует MultiProvider
.
MultiProvider
– это способ объявить все ваши провайдеры в массиве. Давайте посмотрим его в действии, изменим MyApp
следующим образом:
// main.dart
// [...]
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<MainModel>(create: (_) => MainModel()),
Provider<Logger>(
create: (_) => Logger(),
),
],
child: MaterialApp(
title: 'Flutter State Management Basic',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter State Management Basic'),
),
);
}
}
// [...]
Это намного лучше. Каждый провайдер является элементом массива, указанного в аргументе providers
, поэтому ваш код будет гораздо лучше организован. Каждый из провайдеров, указанных в этом массиве, будет доступен вашему приложению, как и раньше.
Заключение
В этом втором уроке мы рассмотрели основные концепции пакета провайдеров. Мы увидели, как управлять состоянием в различных точках с помощью единственного ChangeNotifier
, расположенного в верхней части дерева виджетов. Мы также использовали Provider
для предоставления отдельных экземпляров приложению и организовали все наши провайдеры с помощью MultiProvider
.
Пакет provider
содержит другие провайдеры, которые могут облегчить вашу жизнь во время разработки, я расскажу о них в следующих главах этой серии уроков.
Если у вас есть какие-либо вопросы или предложения, не стесняйтесь оставлять комментарии ниже.
Вы можете найти финальное приложение этого учебника здесь.
Спасибо за чтение и удачного кодинга 🙂