[Перевод] Flutter — Реализация управления состоянием с помощью провайдеров

Flutter

В этой статье хорошо то, что в ней рассказывается не только о том, как Flutter Provider управляет состоянием, но и о том, какую архитектуру может принять приложение Flutter. Эта архитектура основана наclean architectureа такжеFilledStacksЭти два архитектурных принципа (здесь могут быть неправильно поняты или выражены, пожалуйста, поправьте). Однако, в конце концов,MVVMрежим.

Что еще более важно, поставщик, который будет описан в этой статье, на самом деле является виджетом. соответствоватьConsumerЭтот виджет используется вместе для достиженияUI = f(state)этоstateИзмените, пользовательский интерфейс изменится с эффектом.

Наконец, это предложение, которое зависит от исходного текста, перейдите кздесь, сама статья качественная, и написать не сложно.

текст

Команда Flutter рекомендует новичкам использоватьProviderуправлять состоянием. Но что такое Provider и как его использовать?

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

Эта статья проведет вас через все аспекты управления состоянием провайдера. Здесь мы напишем приложение, которое вычисляет обменный курс, которое называетсяMoolaX. При написании этого приложения вы улучшите свои навыки Flutter:

  1. архитектура приложения
  2. Реализовать поставщика
  3. Умение управлять состоянием приложения
  4. Обновлять пользовательский интерфейс на основе изменений состояния

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

Начинать

Нажмите «Загрузить материалы», чтобы загрузить код для проекта. Затем вы можете шаг за шагом следовать этой статье, чтобы добавить код для завершения разработки.

В этой статье используется Android Studio, но также доступен Visual Studio Code. (На самом деле лучше использовать VS Code, точка зрения переводчика только для справки).

В Moolax вы можете выбрать разные валюты. Приложение работает так:

最终效果

Откройте исходный проект, разархивированный стартовый каталог. Android Studio покажет всплывающее окно, нажмитеGet dependencies.

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

Вот как теперь выглядит приложение при запуске:

Создайте архитектуру приложения

если ты не слышалclean architecture, пожалуйста, прочитайте эту статью, прежде чем продолжить.

Основная идея состоит в том, чтобы отделить основную бизнес-логику от пользовательского интерфейса, базы данных, сетевых запросов и сторонних пакетов. Почему? Основная бизнес-логика меняется относительно редко.

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

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

Используйте Provider для управления состоянием

Архитектура MoolaX соответствует этому принципу. Бизнес-логика обрабатывает расчеты, связанные с обменным курсом. Локальное хранилище, сетевые запросы, пользовательский интерфейс и поставщик Flutter не зависят друг от друга.

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

Следующее, что нужно понять, это то, что пользовательский интерфейс, трепетание и провайдер находятся в одном разделе. Flutter - это AUI Framework, и провайдер - виджет в этой структуре.

Является ли Provider схемой? нет. Провайдер это госуправление? Нет, по крайней мере, не в этом приложении.

state— текущее значение переменной приложения. Эти переменные являются частью бизнес-логики приложения, они разбросаны по разным объектам модели и управляются ими. Таким образом, бизнес-логика управляет состоянием, а не Provider.

Итак, что же такое провайдер?

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

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

Используйте Provider для управления сериями состоянийБолее полный анализ состояния и провайдера. Существует много типов провайдеров, но большинство из них выходит за рамки этой статьи.

общаться с бизнес-логикой

Архитектурный паттерн текста зависит отFilledStacksвдохновение. Он сохраняет архитектуру достаточно организованной, но не слишком сложной. Он также дружелюбен к новичкам.

Эта модель очень похожа наMVVM(Вид модели ViewModel).

modelЭто данные, полученные из базы данных или сетевого запроса.viewЭто пользовательский интерфейс, или это может быть экран или виджет.viewmodelЭто бизнес-логика между пользовательским интерфейсом и данными, которая предоставляет данные, которые может отображать пользовательский интерфейс. Но он не имеет смысла пользовательского интерфейса. с участиемMVPразные. Модель представления также не должна знать, откуда берутся данные.

В MoolaX каждая страница имеет свою собственную модель просмотра. Данные могут быть получены из сети и локального хранилища. Классы, которые обрабатывают эту часть, называются службами. Архитектура MoolaX в основном такова:

Обратите внимание на следующие моменты:

  • Страница пользовательского интерфейса прослушивает изменения в модели представления, а также отправляет события в модель представления.
  • Модель представления не знает о конкретных деталях пользовательского интерфейса.
  • Бизнес-логика взаимодействует с абстракцией валюты. Не имеет значения, запрашиваются ли данные из сети или хранятся локально.

На этом теоретическая часть завершена, теперь часть кода!

Создайте основную бизнес-логику

Структура каталогов проекта выглядит следующим образом:

Models

Давайте посмотрим на каталог mdels:

Это структуры данных, используемые бизнес-логикой.Модель карты сотрудничества с классовой ответственностьюэто хороший способ определить, какие модели необходимы. Карты следующие:

в конечном итоге будет использоватьсяCurrencyа такжеRateдве модели. Они представляют собой авансовые и обменные курсы, которые вам нужны, даже если у вас нет компьютера.

View Model

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

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

Открытьchoose_favorites_viewmodel.dart. Вы увидите следующий код:

// 1
import 'package:flutter/foundation.dart';

// 2
class ChooseFavoritesViewModel extends ChangeNotifier {
  // 3
  final CurrencyService _currencyService = serviceLocator<CurrencyService>();

  List<FavoritePresentation> _choices = [];
  List<Currency> _favorites = [];

  // 4
  List<FavoritePresentation> get choices => _choices;

  void loadData() async {
    // ...
    // 5
    notifyListeners();
  }

  void toggleFavoriteStatus(int choiceIndex) {
    // ...
    // 5
    notifyListeners();
  }
}

объяснять:

  1. использоватьChangeNotifierРеализовать мониторинг пользовательского интерфейса модели представления. Этот класс находится во флаттереfoundationМешок.
  2. Класс модели представления наследуетChangeNotifierДобрый. Другой вариант — использовать миксин.ChangeNotifierесть одинnotifyListeners()Метод, который вы будете использовать позже.
  3. Служба, отвечающая за получение и сохранение данных о валюте и обменном курсе.CurrencyServiceЯвляется абстрактным классом, конкретная реализация которого скрыта от модели представления. Вы можете свободно переключаться между различными реализациями.
  4. Любой экземпляр, имеющий доступ к этому режиму просмотра, может получить доступ к списку валют и выбрать из него любимую. Пользовательский интерфейс будет использовать этот список для создания необязательного представления списка.
  5. После получения списка валют или изменения любимой валюты она будет называтьсяnotifyListeners()способ выдачи уведомления. Пользовательский интерфейс получит уведомление и сделает обновление.

существуетchoose_favorites_viewmodel.dartВ файле есть другой класс:FavoritePresentation:

class FavoritePresentation {
  final String flag;
  final String alphabeticCode;
  final String longName;
  bool isFavorite;

  FavoritePresentation(
      {this.flag, this.alphabeticCode, this.longName, this.isFavorite,});
}

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

существуетChooseFavoritesViewModel, замените его следующим кодомloadData()метод

void loadData() async {
    final rates = await _currencyService.getAllExchangeRates();
    _favorites = await _currencyService.getFavoriteCurrencies();
    _prepareChoicePresentation(rates);
    notifyListeners();
  }

  void _prepareChoicePresentation(List<Rate> rates) {
    List<FavoritePresentation> list = [];
    for (Rate rate in rates) {
      String code = rate.quoteCurrency;
      bool isFavorite = _getFavoriteStatus(code);
      list.add(FavoritePresentation(
        flag: IsoData.flagOf(code),
        alphabeticCode: code,
        longName: IsoData.longNameOf(code),
        isFavorite: isFavorite,
      ));
    }
    _choices = list;
  }

  bool _getFavoriteStatus(String code) {
    for (Currency currency in _favorites) {
      if (code == currency.isoCode)
        return true;
    }
    return false;
  }

loadDataПолучить список обменных курсов. тогда,_prepareChoicePresentation()Метод преобразует список в формат, который может отображаться непосредственно в пользовательском интерфейсе._getFavoriteStatus()Определяет, является ли валюта любимой валютой.

Затем используйте следующий код для заменыtoggleFavoriteStatus()метод:

void toggleFavoriteStatus(int choiceIndex) {
    final isFavorite = !_choices[choiceIndex].isFavorite;
    final code = _choices[choiceIndex].alphabeticCode;
    _choices[choiceIndex].isFavorite = isFavorite;
    if (isFavorite) {
      _addToFavorites(code);
    } else {
      _removeFromFavorites(code);
    }
    notifyListeners();
  }

  void _addToFavorites(String alphabeticCode) {
    _favorites.add(Currency(alphabeticCode));
    _currencyService.saveFavoriteCurrencies(_favorites);
  }

  void _removeFromFavorites(String alphabeticCode) {
    for (final currency in _favorites) {
      if (currency.isoCode == alphabeticCode) {
        _favorites.remove(currency);
        break;
      }
    }
    _currencyService.saveFavoriteCurrencies(_favorites);
  }

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

Поздравляем, вы завершили модель представления.

Подводя итог, все, что нужно сделать вашему классу модели представления, это наследоватьChangeNotifierкласс и вызов, где необходимо обновить пользовательский интерфейсnotifyListeners()метод.

Services

Здесь у нас есть три сервиса, а именно: курс обмена, хранение и сетевой запрос. Глядя на диаграмму архитектуры ниже, все сервисы представлены красным прямоугольником справа:

  1. Создайте абстрактный класс и добавьте все методы, которые будут в нем использоваться.
  2. Напишите конкретный класс реализации для абстрактного класса

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

Создайте абстрактный класс обслуживания

Открытьweb_api.dart:

Вы увидите такой код:

import 'package:moolax/business_logic/models/rate.dart';

abstract class WebApi {
  Future<List<Rate>> fetchExchangeRates();
}

Это абстрактный класс, поэтому он не делает ничего конкретного. Тем не менее, он по-прежнему отражает то, что нужно приложению: оно должно запрашивать строку обменных курсов из сети. Как это будет реализовано, решать вам.

использовать поддельные данные

существуетweb_api, создайте новый файлweb_api_fake.dart. Затем скопируйте в него следующий код:

import 'package:moolax/business_logic/models/rate.dart';
import 'web_api.dart';

class FakeWebApi implements WebApi {

  @override
  Future<List<Rate>> fetchExchangeRates() async {
    List<Rate> list = [];
    list.add(Rate(
      baseCurrency: 'USD',
      quoteCurrency: 'EUR',
      exchangeRate: 0.91,
    ));
    list.add(Rate(
      baseCurrency: 'USD',
      quoteCurrency: 'CNY',
      exchangeRate: 7.05,
    ));
    list.add(Rate(
      baseCurrency: 'USD',
      quoteCurrency: 'MNT',
      exchangeRate: 2668.37,
    ));
    return list;
  }

}

Этот класс реализует абстракциюWebApiclass, который возвращает некоторые жестко запрограммированные данные. Теперь вы можете продолжать писать другие части кода, часть сетевого запроса можете быть спокойны. Когда он будет готов, он может вернуться, чтобы выполнить реальный сетевой запрос.

Добавить сервисный локатор

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

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

существуетChooseFavoriatesViewModelЕсть эта строка:

final CurrencyService _currencyService = serviceLocator<CurrencyService>();

serviceLocator— это одноэлементный объект, который возвращает все используемые вами сервисы.

существуетservicesПод каталог открытьservice_locator.dart. Вы увидите следующий код:

// 1
GetIt serviceLocator = GetIt.instance;

// 2
void setupServiceLocator() {

  // 3
  serviceLocator.registerLazySingleton<StorageService>(() => StorageServiceImpl());
  serviceLocator.registerLazySingleton<CurrencyService>(() => CurrencyServiceFake());

  // 4
  serviceLocator.registerFactory<CalculateScreenViewModel>(() => CalculateScreenViewModel());
  serviceLocator.registerFactory<ChooseFavoritesViewModel>(() => ChooseFavoritesViewModel());
}

объяснять:

  1. GetItэтоget_itПакет расположения службы. Это было предварительно добавлено вpubspec.yamlв.get_itВсе зарегистрированные объекты сохраняются через глобальный синглтон.
  2. Этот метод используется для регистрации службы. Этот метод необходимо вызывать перед созданием пользовательского интерфейса.
  3. Вы можете зарегистрировать свой сервис как синглтон с ленивой загрузкой. Регистрация в качестве синглтона означает, что вы каждый раз получаете один и тот же экземпляр. Регистрация в качестве синглтона с ленивой загрузкой означает, что при первом использовании он будет инициализирован только тогда, когда он используется.
  4. Вы также можете использовать локатор сервисов для регистрации модели представления. Это упрощает получение их ссылок в пользовательском интерфейсе. Конечно, все модели просмотра зарегистрированы как фабрика. Каждый раз, когда вы получаете новый экземпляр модели представления.

Обратите внимание, где вызывается кодsetupServiceLocator()из. Открытьmain.dartдокумент:

void main() {
  setupServiceLocator(); //              <--- here
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Moola X',
      theme: ThemeData(
        primarySwatch: Colors.indigo,
      ),
      home: CalculateCurrencyScreen(),
    );
  }
}

Зарегистрируйтесь

зарегистрируйтесь сейчасFakeWebApi.

serviceLocator.registerLazySingleton<WebApi>(() => FakeWebApi());

использоватьCurrencyServiceImplзаменятьCurrencyServiceFake:

serviceLocator.registerLazySingleton<CurrencyService>(() => CurrencyServiceImpl());

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

Укажите недостающий класс:

import 'web_api/web_api.dart';
import 'web_api/web_api_fake.dart';
import 'currency/currency_service_implementation.dart';

Запустите приложение и коснитесь сердечка в правом верхнем углу.

Конкретная реализация веб-API

Поддельная реализация веб-API была зарегистрирована ранее, и приложение готово к запуску. Далее вам нужно получить реальные данные с реального веб-сервера. существуетservices/web_apiкаталог, создайте новый файлweb_api_implementation.dart. Добавьте следующий код:

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:moolax/business_logic/models/rate.dart';
import 'web_api.dart';

// 1
class WebApiImpl implements WebApi {
  final _host = 'api.exchangeratesapi.io';
  final _path = 'latest';
  final Map<String, String> _headers = {'Accept': 'application/json'};

  // 2
  List<Rate> _rateCache;

  Future<List<Rate>> fetchExchangeRates() async {
    if (_rateCache == null) {
      print('getting rates from the web');
      final uri = Uri.https(_host, _path);
      final results = await http.get(uri, headers: _headers);
      final jsonObject = json.decode(results.body);
      _rateCache = _createRateListFromRawMap(jsonObject);
    } else {
      print('getting rates from cache');
    }
    return _rateCache;
  }

  List<Rate> _createRateListFromRawMap(Map jsonObject) {
    final Map rates = jsonObject['rates'];
    final String base = jsonObject['base'];
    List<Rate> list = [];
    list.add(Rate(baseCurrency: base, quoteCurrency: base, exchangeRate: 1.0));
    for (var rate in rates.entries) {
      list.add(Rate(baseCurrency: base,
          quoteCurrency: rate.key,
          exchangeRate: rate.value as double));
    }
    return list;
  }
},

Обратите внимание на следующие моменты:

  1. так какFakeWebApi, этот класс также реализуетWebApi. Он содержит изapi.exchangeratesapi.ioЛогика получения данных. Однако остальная часть приложения не знает об этом, поэтому, если вы хотите переключиться на другой веб-API, это, несомненно, единственное место, где вы можете его изменить.
  2. exchangeratesapi.io щедро предоставляет обменные курсы для валюты с заданными данными, и все это без дополнительных токенов.

Открытьservice_localtor.dart,ПучокFakeWebApi()превратиться вWebApiImp()и обновите соответствующийimportутверждение.

import 'web_api/web_api_implementation.dart';

void setupServiceLocator() {
  serviceLocator.registerLazySingleton<WebApi>(() => WebApiImpl());
  // ...
}

Реализовать поставщика

Теперь, наконец, очередь провайдера. Как сказать, это также учебник провайдера!

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

существуетpubspec.yamlНайдите пакет Provider в:

dependencies:
  provider: ^4.0.1

Есть специальный провайдер:ChangeNotifierProvider. Это достигло этого.ChangeNotifierМодификация модели представления.

существуетui/viewsкаталог, открытьchoose_favorites.dartдокумент. Замените содержимое этого файла следующим кодом:

import 'package:flutter/material.dart';
import 'package:moolax/business_logic/view_models/choose_favorites_viewmodel.dart';
import 'package:moolax/services/service_locator.dart';
import 'package:provider/provider.dart';

class ChooseFavoriteCurrencyScreen extends StatefulWidget {
  @override
  _ChooseFavoriteCurrencyScreenState createState() =>
      _ChooseFavoriteCurrencyScreenState();
}

class _ChooseFavoriteCurrencyScreenState
    extends State<ChooseFavoriteCurrencyScreen> {

  // 1
  ChooseFavoritesViewModel model = serviceLocator<ChooseFavoritesViewModel>();

  // 2
  @override
  void initState() {
    model.loadData();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Choose Currencies'),
      ),
      body: buildListView(model),
    );
  }

  // Add buildListView() here.
}

ты найдешьbuildListView()обратите внимание на следующие изменения:

  1. Локатор службы возвращает экземпляр модели представления.
  2. использоватьStatefulWidget, который содержитinitState()метод. Здесь вы можете указать модели представления загружать данные о валюте.

существуетbuild()метод, добавьте следующееbuildListView()выполнить:

Widget buildListView(ChooseFavoritesViewModel viewModel) {
    // 1
    return ChangeNotifierProvider<ChooseFavoritesViewModel>(
      // 2
      create: (context) => viewModel,
      // 3
      child: Consumer<ChooseFavoritesViewModel>(
        builder: (context, model, child) => ListView.builder(
          itemCount: model.choices.length,
          itemBuilder: (context, index) {
            return Card(
              child: ListTile(
                leading: SizedBox(
                  width: 60,
                  child: Text(
                    '${model.choices[index].flag}',
                    style: TextStyle(fontSize: 30),
                  ),
                ),
                // 4
                title: Text('${model.choices[index].alphabeticCode}'),
                subtitle: Text('${model.choices[index].longName}'),
                trailing: (model.choices[index].isFavorite)
                    ? Icon(Icons.favorite, color: Colors.red)
                    : Icon(Icons.favorite_border),
                onTap: () {
                  // 5
                  model.toggleFavoriteStatus(index);
                },
              ),
            );
          },
        ),
      ),
    );
  }

Анализ кода:

  1. Добавить кChangeNotifierProvider, особый тип провайдера, который прослушивает модификации модели представления.
  2. ChangeNotifierProviderесть одинcreateметод. Этот метод предоставляет значение модели представления дочернему компоненту wdiget. Здесь у вас уже есть ссылка на модель представления, просто используйте ее напрямую.
  3. Consumer, когда модель представленияnotifyListeners()Скажите интерфейсу перестраиваться при внесении изменений. Метод построителя Потребителя передает значение модели представления. Эта модель просмотра взята изChangeNotifierProviderпередан.
  4. использоватьmodelперестроить интерфейс. Обратите внимание, что в пользовательском интерфейсе очень мало логики.
  5. Теперь, когда у вас есть ссылка на модель представления, вы можете вызывать методы внутри нее.toggleFavoriteStatus()называетсяnotifyListeners().

Запустите приложение еще раз.

Использование провайдеров в больших приложениях

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

Другие подходы к архитектуре и управлению состоянием

Если вам не нравится архитектура, описанная в этой статье, рассмотрите шаблон BLoC.Начало работы с режимом BLoCТоже хорошая отправная точка. Вы обнаружите, что режим BLoC также не так сложен для понимания, как гласит легенда.

а такжеразноеДа, но чаще всего в настоящее время используются Provider и BLoC.