Как стать автором
Обновить
937.25
Рейтинг
OTUS
Цифровые навыки от ведущих экспертов

Flutter.dev: Простое управление состоянием приложения

Время прочтения 8 мин
Просмотры 9.5K
Блог компании OTUS Программирование *Разработка мобильных приложений *Flutter *
Перевод
Автор оригинала: flutter.dev
Всем привет. В сентябре OTUS запускает новый курс «Flutter Mobile Developer». В преддверии старта курса мы традиционно подготовили для вас полезный перевод.




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

Мы будем использовать пакет provider. Если вы новичок во Flutter и у вас нет веских причин для выбора другого подхода (Redux, Rx, хуков и т. д.), это, вероятно, самый лучший подход для старта. Пакет provider прост в освоении и не требует большого количества кода. Он также оперирует концепциями, которые применимы во всех других подходах.

Тем не менее, если у вас уже есть большой опыт в управлении состоянием из других реактивных фреймворков, вы можете поискать другие пакеты и руководства, перечисленные на странице опций.

Пример




В качестве примера рассмотрим следующее простое приложение.

В приложении есть два отдельных экрана: каталог и корзина (представленные виджетами MyCatalog и MyCart соответственно). В данном случае это приложение для покупок, но вы можете представить ту же структуру в простом приложении социальной сети (замените каталог на «стену», а корзину на «избранное»).

Экран каталога включает настраиваемую панель приложения (MyAppBar) и прокручиваемое представление множества элементов списка (MyListItems).

Вот это приложение в виде дерева виджетов:



Итак, у нас есть как минимум 5 подклассов Widget. Многим из них нужен доступ к состоянию, которому они не принадлежат. Например, каждый MyListItem должен иметь возможность добавить себя в корзину. Они также могут нуждаться в проверке, находится ли уже отображаемый в настоящее время товар в корзине.

Это подводит нас к нашему первому вопросу: куда мы должны поместить текущее состояние корзины?

Повышение состояния


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

Зачем? В декларативных фреймворках, таких как Flutter, если вы хотите изменить пользовательский интерфейс, вам придется его перестроить. Нельзя просто взять и написать MyCart.updateWith(somethingNew). Другими словами, сложно принудительно изменить виджет извне, вызвав на нем метод. И даже если бы вы могли заставить это работать, вы бы боролись с фреймворком вместо того, чтобы позволить ему помочь вам.

// ПЛОХО: НЕ ДЕЛАЙТЕ ТАК
void myTapHandler() {
  var cartWidget = somehowGetMyCartWidget();
  cartWidget.updateWith(item);
}


Даже если вы заставите приведенный выше код работать, вам придется иметь дело в виджете MyCart со следующим:

// ПЛОХО: НЕ ДЕЛАЙТЕ ТАК
Widget build(BuildContext context) {
  return SomeWidget(
// Изначальное состояние корзины.
  );
}

void updateWith(Item item) {
// Каким-то образом отсюда вам нужно изменить UI.
}


Вам нужно будет принять во внимание текущее состояние пользовательского интерфейса и применить к нему новые данные. Здесь будет сложно избежать ошибок.

Во Flutter вы создаете новый виджет каждый раз, когда его содержимое изменяется. Вместо MyCart.updateWith(somethingNew) (вызова метода) вы используете MyCart(contents) (конструктор). Поскольку вы можете создавать новые виджеты только в методах сборки их родителей, если вы хотите изменить contents, он должен находиться в родительском элементе MyCart или выше.

// ХОРОШО
void myTapHandler(BuildContext context) {
  var cartModel = somehowGetMyCartModel(context);
  cartModel.add(item);
}


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

// ХОРОШО
Widget build(BuildContext context) {
  var cartModel = somehowGetMyCartModel(context);
  return SomeWidget(
    // Просто создайте пользовательский интерфейс единожды, используя текущее состояние корзины.
    // ···
  );
}


В нашем примере contents должно находиться в MyApp. Каждый раз, когда он изменяется, он перестраивает MyCart сверху (подробнее об этом позже). Благодаря этому MyCart не нужно беспокоиться о жизненном цикле — он просто объявляет, что нужно показывать для любого заданного contents. Когда он изменится, старый виджет MyCart исчезнет и будет полностью заменен новым.



Это то, что мы имеем в виду, когда говорим, что виджеты неизменяемы (immutable). Они не меняются — их заменяют.

Теперь, когда мы знаем, куда поместить состояние корзины, давайте посмотрим, как получить к ней доступ.

Доступ к состоянию


Когда пользователь нажимает на один из элементов в каталоге, он добавляется в корзину. Но поскольку тележка находится над MyListItem, как нам это сделать?

Простой вариант — предоставить колбек, который MyListItem может вызывать при нажатии. Dart функции являются объектами первого класса, поэтому вы можете передавать их любым способом. Итак, внутри MyCatalog вы можете определить следующее:

@override
Widget build(BuildContext context) {
  return SomeWidget(
   // Создаем виджет, передавая ему ссылку на метод выше.
    MyListItem(myTapCallback),
  );
}

void myTapCallback(Item item) {
  print('user tapped on $item');
}


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

К счастью, у Flutter есть механизмы, позволяющие виджетам предоставлять данные и сервисы своим потомкам (другими словами, не только своим потомкам, но и любым нижестоящим виджетам). Как и следовало ожидать от Flutter, где Everything is a Widget, эти механизмы представляют собой просто особые виды виджетов: InheritedWidget, InheritedNotifier, InheritedModel и другие. Мы не будем описывать их здесь, потому что они немного не соответствуют тому, что мы пытаемся сделать.

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

С provider вам не нужно беспокоиться о колбеках или InheritedWidgets. Но вам нужно понимать 3 концепции:

  • ChangeNotifier
  • ChangeNotifierProvider
  • Consumer


ChangeNotifier


ChangeNotifier — это простой класс, включенный в Flutter SDK, который предоставляет своим слушателям уведомление об изменении состояния. Другими словами, если что-то является ChangeNotifier, вы можете подписаться на его изменения. (Это некая форма Observable — для тех, кто знаком с этим термином.)

ChangeNotifier в provider это один из способов инкапсулировать состояние приложения. Для очень простых приложений вы можете обойтись одним ChangeNotifier. В более сложных у вас будет несколько моделей и, следовательно, несколько ChangeNotifiers. (Вам вообще не нужно использовать ChangeNotifier с provider, но с этим классом легко работать.)

В нашем примере приложения для покупок мы хотим управлять состоянием корзины в ChangeNotifier. Мы создаем новый класс, который расширяет его, например:

class CartModel extends ChangeNotifier {
/// Внутреннее приватное состояние корзины.
  final List<Item> _items = [];

  /// Неизменяемое представление товаров в корзине.
  UnmodifiableListView<Item> get items => UnmodifiableListView(_items);

  /// Текущая общая цена всех предметов (при условии, что все предметы стоят по 42 доллара).
  int get totalPrice => _items.length * 42;

  /// Добавляет [item] в корзину. Это и [removeAll] - единственные способы изменить корзину снаружи.
  void add(Item item) {
    _items.add(item);
    // Этот вызов сообщает виджетам, которые слушают эту модель, о необходимости перестроения.
    notifyListeners();
  }

  /// Удаляет все товары из корзины.
  void removeAll() {
    _items.clear();
    // Этот вызов сообщает виджетам, которые слушают эту модель, о необходимости перестроения.
    notifyListeners();
  }
}


Единственный фрагмент кода, специфичный для ChangeNotifier, — это вызов notifyListeners(). Вызывайте этот метод каждый раз, когда модель изменяется таким образом, чтобы это может отразиться в UI вашего приложения. Все остальное в CartModel — это сама модель и ее бизнес-логика.

ChangeNotifier является частью flutter:foundation и не зависит от каких-либо классов более высокого уровня во Flutter. Его легко тестировать (для этого даже не нужно использовать тестирование виджетов ). Например, вот простой модульный тест CartModel:

test('adding item increases total cost', () {
  final cart = CartModel();
  final startingPrice = cart.totalPrice;
  cart.addListener(() {
    expect(cart.totalPrice, greaterThan(startingPrice));
  });
  cart.add(Item('Dash'));
});


ChangeNotifierProvider


ChangeNotifierProvider — это виджет, который предоставляет экземпляр ChangeNotifier своим потомкам. Он поставляется в пакете provider.

Мы уже знаем, где разместить ChangeNotifierProvider: над виджетами, которым нужен доступ к нему. В случае CartModel это подразумевает что-то выше MyCart и MyCatalog.

Вы не хотите размещать ChangeNotifierProvider выше, чем необходимо (потому что вы не хотите загрязнять область действия). Но в нашем случае единственный виджет, который находится поверх MyCart и MyCatalog — это MyApp.

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CartModel(),
      child: MyApp(),
    ),
  );
}


Обратите внимание, что мы определяем конструктор, который создает новый экземпляр CartModel. ChangeNotifierProvider достаточно умен, чтобы не перестраивать CartModel без крайней необходимости. Он также автоматически вызывает dispose() в CartModel, когда экземпляр больше не нужен.

Если вы хотите предоставить более одного класса, вы можете использовать MultiProvider:

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => CartModel()),
        Provider(create: (context) => SomeOtherClass()),
      ],
      child: MyApp(),
    ),
  );
}


Consumer


Теперь, когда CartModel предоставляется виджетам в нашем приложении через объявление ChangeNotifierProvider вверху, мы можем начать его использовать.

Это делается через виджет Consumer.

return Consumer<CartModel>(
  builder: (context, cart, child) {
    return Text("Total price: ${cart.totalPrice}");
  },
);


Мы должны указать тип модели, к которой мы хотим получить доступ. В данном случае нам нужна CartModel, поэтому мы пишем Consumer<CartModel>. Если вы не укажете универсальный (<CartModel>), пакет provider не сможет вам помочь. provider основан на типах, и без типа он не поймет, что вы хотите.

Единственный обязательный аргумент виджета Consumer — это builder. Builder — это функция, которая вызывается при изменении ChangeNotifier. (Другими словами, когда вы вызываете notifyListeners() в своей модели, вызываются все методы builder всех соответствующих виджетов Consumer.)

Конструктор вызывается с тремя аргументами. Первый — context, который вы также получаете в каждом билд методе.
Второй аргумент функции builder — это экземпляр ChangeNotifier. Это то, о чем мы просили с самого начала. Вы можете использовать данные модели, чтобы определить, как пользовательский интерфейс должен выглядеть в любой заданной точке.

Третий аргумент — child, он нужен для оптимизации. Если у вас есть большое поддерево виджетов под вашим Consumer, которое не изменяется при изменении модели, вы можете построить его один раз и получить через builder.

return Consumer<CartModel>(
  builder: (context, cart, child) => Stack(
        children: [
          // Здесь используется SomeExhibitedWidget, без перестраивания каждый раз.
          child,
          Text("Total price: ${cart.totalPrice}"),
        ],
      ),
  // Здесь создаем дорогой виджет.
  child: SomeExpensiveWidget(),
);


Лучше всего размещать свои виджеты Consumer как можно глубже в дереве. Вы не хотите перестраивать большие части пользовательского интерфейса только потому, что где-то изменились какие-то детали.

// НЕ ДЕЛАЙТЕ ТАК
return Consumer<CartModel>(
  builder: (context, cart, child) {
    return HumongousWidget(
      // ...
      child: AnotherMonstrousWidget(
        // ...
        child: Text('Total price: ${cart.totalPrice}'),
      ),
    );
  },
);


Вместо этого:

// ДЕЛАЙТЕ ТАК
return HumongousWidget(
  // ...
  child: AnotherMonstrousWidget(
    // ...
    child: Consumer<CartModel>(
      builder: (context, cart, child) {
        return Text('Total price: ${cart.totalPrice}');
      },
    ),
  ),
);


Provider.of


Иногда вам не очень нужны данные в модели для изменения пользовательского интерфейса, но вам все равно нужен доступ к ним. Например, кнопка ClearCart позволяет пользователю удалить все из корзины. Нет необходимости отображать содержимое корзины, достаточно вызвать метод clear().

Мы могли бы использовать Consumer<CartModel> для этого, но это было бы расточительно. Мы бы попросили фреймворк перестроить виджет, который не нужно перестраивать.

Для этого варианта использования мы можем использовать Provider.of с параметром listen, установленным на false.

Provider.of<CartModel>(context, listen: false).removeAll();


Использование указанной выше строки в билд методе не приведет к перестройке этого виджета при вызове notifyListeners .

Собираем все вместе


Вы можете ознакомиться с примером, рассмотренным в этой статье. Если вам нужно что-то попроще, посмотрите, как выглядит простое приложение Counter, созданное с помощью provider.

Когда вы будете готовы поиграть с provider самостоятельно, не забудьте сначала добавить его зависимость в свой pubspec.yaml.

name: my_name
description: Blah blah blah.

# ...

dependencies:
  flutter:
    sdk: flutter

  provider: ^3.0.0

dev_dependencies:
  # ...


Теперь вы можете 'package:provider/provider.dart'; и запускать построение…

Теги:
Хабы:
Всего голосов 8: ↑7 и ↓1 +6
Комментарии 3
Комментарии Комментарии 3

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
51–100 человек
Местоположение
Россия
Представитель
OTUS