Как использовать функцию `Provider.of` во Flutter для управления состоянием виджетов?

Как можно использовать функцию Provider.of во Flutter для управления состоянием виджетов?


Ответы (1 шт):

Автор решения: Falchio

How to use Provider: Context.read, watch and select

источник

Базовое управление состоянием во Flutter

Далее несколько вольный перевод хорошей статьи из источника:

Provider.of<T> возвращает класс типа T. Обычно это класс-наследник ChangeNotifier.

Пример с гипотетическим классом UserProvider.

User:

class User {
  final String name;
  final int age;

  User({required this.name, required this.age});
}

UserProvider:

import 'package:flutter/foundation.dart';

class UserProvider extends ChangeNotifier {
  User? _currentUser;

  User? get currentUser => _currentUser;

  void setCurrentUser(User newUser) {
    _currentUser = newUser;
    notifyListeners();
  }

  void removeCurrentUser() {
    _currentUser = null;
    notifyListeners();
  }
}

Это позволяет получить доступ к управляющему состоянием классу. Можно задать текущего пользователя через setCurrentUser(), затем пересобрать виджет, который слушает это состояние.

Provider.of может принимать параметр listen. Это означает, что он прослушивает изменение, заданное notifyListeners, и перестраивает виджет. По умолчанию listen имеет значение true. Вы можете переопределить это и установить его как false, чтобы виджет не перестраивался без необходимости.

Пример удаления текущего пользователя:

ElevatedButton( 
  onPressed: () { 
    Provider.of<UserProvider>(context, listen: false ) 
      .removeCurrentUser(); 
  }, 
  child: Text( 'Удалить текущего пользователя' ), 
),

Не нужно вызывать Provider.of(context, listen: false) внутри build.

Context.read

Context.read похож на Provider.of(context, listen: false), но с некоторыми ограничениями.

Поскольку это не приводит к перестройке виджета, вы, скорее всего, будете использовать это для вызова событий, которым не нужно перестраивать текущий виджет.

Предположим, у вас есть некая логика для входа пользователя в систему.

class  UserProvider  extends  ChangeNotifier  {
  User? _loginUser;

  User? get loginUser => _loginUser;

  Future< void > loginUser(User newLoginUser) async {
    _loginUser = newLoginUser;
     await someMethodWithNetwork();
  }
}

Вам не захочется перестраивать виджет с loginUser, так как вы в любом случае перенаправите пользователя после успешного входа.

ElevatedButton( 
  onPressed: () { 
    context.read<UserProvider>().loginUser(newLoginUser); 
    Navigator.pushReplacementNamed(context, '/' ); 
  }, 
  child: Text( 'Login' ), 
),

В документации упоминается, что вы не должны вызывать это в build поскольку это небезопасно. Если забыть об этом можно потратить часы на отладку. Вызывать можно к примеру в initState().

@override
initState() { 
  final userProvider = context.read<UserProvider>(); 
  userProvider.loginUser(widget.initialUser); 
}

Context.watch

Context.watch аналогично Provider.of(context) перестраивает виджет.

К примеру, есть класс:

import 'package:flutter/foundation.dart';

class TemperatureProvider extends ChangeNotifier {
  double _currentTemperature = 17.0;

  double get currentTemperature => _currentTemperature;

  void updateTemperature(double newTemperature) {
    _currentTemperature = newTemperature;
    notifyListeners();
  }
}

То отображать текущую температуру можно так:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class TemperatureDisplayScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final temperatureProvider = context.watch<TemperatureProvider>();

    return Scaffold(
      appBar: AppBar(
        title: Text('Temperature Display'),
      ),
      body: Center(
        child: Column(
          children: [
            Text('Current Temperature:'),
            Text('${temperatureProvider.currentTemperature} °C'),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () {
           double newTemperature = temperatureProvider.currentTemperature + 1;
           temperatureProvider.updateTemperature(newTemperature);
        },
      ),
    );
  }
}

В отличие от context.read можно использовать context.watch внутри build. Так как функция context.watch нужна для пересборки виджета, то можно использовать её внутри build.

Context.select

Предположим что нужен доступ к значению, которое меняется. Но при этом нет необходимости пересобирать виджет каждый раз когда изменяется состояние не значимых полей объекта. Функция Context.select позволяет пересобирать виджет при изменении конкретного поля.

Допустим есть Provider, который содержит различные параметры погоды.

import 'package:flutter/foundation.dart';

class WeatherProvider extends ChangeNotifier {
  double _temperature = 10.0;
  double _humidity = 11.0;
  double _windSpeed = 12.0;

  double get temperature => _temperature;
  double get humidity => _humidity;
  double get windSpeed => _windSpeed;

  void updateTemperature(double newTemperature) {
    _temperature = newTemperature;
    notifyListeners();
  }

  void updateHumidity(double newHumidity) {
    _humidity = newHumidity;
    notifyListeners();
  }

  void updateWindSpeed(double newWindSpeed) {
    _windSpeed = newWindSpeed;
    notifyListeners();
  }
}

Допустим необходимо пересобирать виджет только при обновлении определенного поля класса. Скажем температуры или давления или скорости ветра.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class WeatherDisplayScreen extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
   // используем context.select
   final windSpeed = context.select((WeatherProvider i) => i.windSpeed);

    return Scaffold(
      appBar: AppBar(
        title: Text('Weather Display'),
      ),
      body: Center(
        child: Column(
          children: [
            Text('Current Wind Speed:'),
            Text('$windSpeed'),
            // this will rebuild the widget
            TextButton(
              onPressed: () {
                context.read<WeatherProvider>().updateWindSpeed(1.5);
              },
              child: Text('Update Wind Speed'),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Text('Update Humidity'),
        // this wil not rebuild the widget
        onPressed: () {
          context.read<WeatherProvider>().updateHumidity(1.5);
        },
      ),
    );
  }
}

Таким образом можно оптимизировать пересборку виджетов и предотвратить ненужные действия. Context.select может быть вызван несколько раз для каждого нужного состояния.

Итог

  • context.read - используется для событий, которые не должны пересобирать виджет.
  • context.watch - для отображения значений с пересборкой виджетов.
  • context.select - для отображения значений и выборочной пересборкой виджета при изменении конкретного поля класса.
→ Ссылка