[译]Flutter - 使用Provider实现状态管理

栏目: IT技术 · 发布时间: 6年前

内容简介:这篇文章好的的地方在于它不仅讲了Flutter Provider如何管理State的,还讲述了一个Flutter App可以采用哪一种架构。这种架构是基于更加重要的一点,就是本文要讲述的Provider其实就是一种widget。搭配着最后,还是那句话要看原文的请到

这篇文章好的的地方在于它不仅讲了Flutter Provider如何管理State的,还讲述了一个Flutter App可以采用哪一种架构。这种架构是基于 clean architecture 和 FilledStacks 这两种架构原则的(这里可能理解或者表达有误,请指正)。但是文中最后采用的还是 MVVM 的模式。

更加重要的一点,就是本文要讲述的Provider其实就是一种widget。搭配着 Consumer 这个widget一起使用,达到 UI = f(state) 这个 state 变化,UI跟着变的效果。

最后,还是那句话要看原文的请到 这里 ,文章本身有质量,而且写的不难。

正文

Flutter团队建议初学者使用 Provider 来管理state。但是Provider到底是什么,该如何使用?

Provider是一个UI工具。如果你对于架构、state和架构之间有疑惑,那么并不只有你是这样。本文会帮助你理清这些概念,让你知道如何从无到有写一个app。

本文会带你学习Provider管理state的方方面面。这里我们来写一个计算汇率的app,就叫做 MoolaX 。在写这个app的时候你会提升你的Flutter技能:

  1. app架构
  2. 实现一个Provider
  3. 熟练管理app的state
  4. 根据state的更改来更新UI

注意:本文假设你已经知道Dart和如何写一个Flutter的app了。如果在这方面还有不清楚的话请移步 Flutter入门 。

开始

点击“下载材料”来下载项目的代码。然后你就可以一步一步的跟着本文添加代码完成开发。

本文使用了Android Studio,但是Visual Studio Code也是可以用的。(其实VS Code更好用,译者观点)。

在MoolaX里你可以选择不同的货币。App运行起来是这样的:

[译]Flutter - 使用Provider实现状态管理

打开初始项目,解压后的starter目录。Android Studio会出现一个弹出框,点击 Get dependencies 。

在初始项目里已经包含了一部分代码,本教程会带着你添加必要的代码,让你轻松学会下文的内容。

现在这个app运行起来的时候是这样的:

[译]Flutter - 使用Provider实现状态管理

搭建App的架构

如果你没听说过 clean architecture ,再继续之前请阅读这篇文章。

主旨就是把核心业务逻辑从UI、数据库、网络请求和第三方包中分离出来。为什么?核心业务逻辑相对并不会那么频繁的更改。

[译]Flutter - 使用Provider实现状态管理

UI不应该直接请求网络。也不应该把数据库读写的代码写的到处都是。所有的数据都应该从一个统一的地方发出,这就是业务逻辑。

这就形成了一个插件系统。即使你更换了一个数据库,app的其他部分也不会有任何的感知。你可以从一个移动端UI更换的一个桌面UI,app的其他部分也并不用关心。这对于开发一个易于维护、扩展的app来说十分有效。

使用Provider管理state

MoolaX的架构就符合这个原则。业务逻辑处理汇率相关的计算。Local Storage、网络请求和Flutter的UI、Provider这些全部都互相独立。

[译]Flutter - 使用Provider实现状态管理

Local storage使用的是shared preferences,但是这个和app的其他部分没有关联。同理网络请求如何获取数据和app的其他部分也没有任何关联。

接下来要理解的是UI、Flutter和Provider都在同一个部分里。Flutter就是一个UI框架,Provider是这个框架里的一个widget。

Provider是架构吗?不是。

Provider是状态管理吗?不是,至少在这个app里不是。

state是app的变量的当前值。这些变量是app的业务逻辑的一部分,分散、管理在不同的model对象里。所以,业务逻辑管理了state,而不是Provider。

所以,Provider到底是什么呢?

它是 状态管理的helper ,它是一个widget。通过这个widget可以把model对象传递给它的子widget。

Consumer widget,属于 Provider 包 的一部分,监听了Provider暴露的mode值的改变,并重新build它的全部子widget。

[译]Flutter - 使用Provider实现状态管理

使用Provider管理state系列 对state和provider做了更加全面的解析。Provider有很多种,不过多数不在本文的范围内。

和业务逻辑通信

文本的架构模式受到了 FilledStacks 的启发。它可以让架构足够有条理而又不会太过复杂。对初学者也很友好。

这个模型非常类似于 MVVM (Model View ViewModel)。

model就是从数据库或者网络请求得到的数据。 view 就是UI,也可以是一个screen或者widget。 viewmodel 就是在UI和数据中间的业务逻辑,并提供了UI可以展示的数据。但是它对UI并无感知。这一单和 MVP 不同。viewmodel也不应该知道数据从哪里来。

[译]Flutter - 使用Provider实现状态管理

在MoolaX里,每页都有独立的view model。数据可以从网络和本地存储获得。处理这部分内容的类叫做services。MoolaX的架构基本是这样的:

[译]Flutter - 使用Provider实现状态管理

注意如下几点:

  • UI页面监听view model的改变,也会给view model发送事件
  • view model不会感知到UI的具体细节
  • 业务逻辑与货币抽象交互。它不会感知数据是从网络请求得来还是从本地存储得来。

理论部分到此结束,现在开始代码部分!

创建核心业务逻辑

项目的目录结构如下:

[译]Flutter - 使用Provider实现状态管理

Models

我们来看看mdels目录:

[译]Flutter - 使用Provider实现状态管理

这些就是业务逻辑要用到的数据结构了。 类职责协同卡片模型 是一个很好的方法可以确定哪些model是需要的。卡片如下:

[译]Flutter - 使用Provider实现状态管理

最后会用到 Currency 和 Rate 两个model。他们代表了先进和汇率,就算你没哟计算机也需要这两个。

View Model

view mode的职责就是拿到数据,然后转化成UI可用的格式。

展开 view_models 目录。你会看到两个view model,一个是给结算页用的,一个是给选择汇率页用的。

[译]Flutter - 使用Provider实现状态管理

打开 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 来实现UI对view model的监听。这个类在Flutter foundation 包。
  2. view model类继承了 ChangeNotifier 类。另一个选项是使用mixin。 ChangeNotifier 里有一个 notifyListeners() 方法,你后面会用到。
  3. 一个service来负责获取和保存货币以及汇率数据。 CurrencyService 是一个抽象类,它的具体实现隐藏在view model之外。你可以任意更换不同的实现。
  4. 任意可以访问这个view mode的实例都可以访问到一个货币列表,然后从里面选出一个最喜欢的。UI会使用这个列表来创建一个可选的listview。
  5. 在获取到货币列表或者修改了最喜欢的货币之后,都会调用 notifyListeners() 方法发出通知。UI会接受到通知,并作出更新。

在 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,});
}

这个类就是为UI展示用的。这里尽量不保存任何与UI无关的内容。

在 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() 方法把列表转化成UI可以直接显示的格式。 _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);
  }

只要这个方法被调用,view model就会调用货币服务保存新的最喜欢货币。同时因为 notifyListeners 方法也被调用了,所以UI也会立刻显示最新的修改。

恭喜你,你已经完成了view model了。

总结一下,你的view model类需要做的就是继承 ChangeNotifier 类并在需要更新UI的地方调用 notifyListeners() 方法。

Services

我们这里有三种service,分别是:汇率交换,存储以及网络请求。看下面的架构图,所有服务都在右边红色的框表示:

[译]Flutter - 使用Provider实现状态管理

  1. 创建一个抽象类,在里面添加所有会用到的方法
  2. 给抽象类写一个具体的实现类

因为每次创建一个service的方式都差不多,我们就用网络请求为例。初始项目中已经包含了 汇率服务 和 存储服务 了。

创建一个抽象service类

打开web_api.dart:

[译]Flutter - 使用Provider实现状态管理

你会看到如下的代码:

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

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

这是一个抽象类,所以它并不具体做什么。然而,它还是会反映出app需要它做什么:它应该从网络请求一串汇率回来。具体如何实现由你决定。

使用假数据

在 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;
  }

}

这个类实现了抽象 WebApi 类,反回了某些写死的数据。现在你可以继续编写其他部分的代码了,网络请求的部分可以放心了。什么时候准备好了,可以回来实现真正的网络请求。

添加一个Service定位器

即使抽象类都实现了,你还是要告诉app去哪里找这些抽象类的具体实现类。

有一个service定位器可以很快完成这个功能。一个service定位器是 一个依赖注入的替代 。它可以用来把一个service和app的其他部分解耦。

在 ChooseFavoriatesViewModel 里有这么一行:

final CurrencyService _currencyService = serviceLocator<CurrencyService>();

serviceLocator 是一个单例对象,它回到你用到的所有的service。

在 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 的service 定位包。这里已经预先添加到 pubspec.yaml 里了。 get_it 会通过一个全局的单例来保留所有注册的对象。
  2. 这个方法就是用来注册服务的。在构建UI之前就需要调用这个方法了。
  3. 你可以把你的服务注册为延迟加载的单例。注册为单例也就是说你每次取回的是同一个实例。注册为一个延迟加载的单例等于,在第一次使用的时候,只有在用的时候才会初始化。
  4. 你也可以使用service定位器来注册view model。这样在UI里可以很容易拿到他们的引用。当然view models都是注册为一个factory了。每次取回来的都是一个新的view model实例。

注意代码是在哪里调用 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

现在来注册 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';

运行app,点击右上角的心形。

[译]Flutter - 使用Provider实现状态管理

Web API的具体实现

前面注册了假的web api实现,app已经可以运行了。下面就需要从真的web服务器上获取真正的数据了。在 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 获取数据的逻辑。然而,app的其他部分并不知道这一点,所以如果你想换到别的web api,毫无疑问这里就是你唯一可以更改的地方。
  2. exchangeratesapi.io慷慨的提供了给定数据的货币的汇率,都不要额外的token。

打开 service_localtor.dart ,把 FakeWebApi() 修改为 WebApiImp() ,并更新对应的 import 语句。

import 'web_api/web_api_implementation.dart';

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

实现Provider

现在总算轮到Provider了。这篇怎么说也是一个Provider的教程!

我们等了这么久才开始Provider的部分,你应该意识到了Provider其实是一个app的很小一部分。它只是用来方便在更改发生的时候方便把值传递给子widget,但也不是架构或者状态管理的系统。

在 pubspec.yaml 里找到Provider包:

dependencies:
  provider: ^4.0.1

有一个比较特殊的Provider: ChangeNotifierProvider 。它监听实现了 ChangeNotifier 的view model的修改。

在 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. servie定位器返回一个view model的实例
  2. 使用 StatefulWidget ,它包含了 initState() 方法。这里你可以告诉view model加载货币数据。

在 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 ,一个特殊类型的provider,它监听了来自view model的修改。
  2. ChangeNotifierProvider 有一个 create 方法。这个方法给子wdiget提供了view model值。在这里你已经有了view model的引用,那就直接使用。
  3. Consumer ,当view model的 notifyListeners() 告知更改发生的时候重新build界面。Consumer的builder方法向下传递了view model值。这个view model是从 ChangeNotifierProvider 传下来的。
  4. 使用 model 里的数据来重新build界面。注意UI里只有很少的逻辑。
  5. 既然你有了view model的引用,那么完全可以调用里面的方法。 toggleFavoriteStatus() 调用了 notifyListeners() 。

再次运行app。

[译]Flutter - 使用Provider实现状态管理

在大型app中使用Provider

你可以按照本文所述的方式添加更多的界面。一旦你习惯了为每个界面添加view model就可以考虑为某些类创建基类来减少重复代码。本文没有这么做,因为这样的话理解这些代码要花更多的时间。

其他的架构和状态管理方法

如果你不喜欢本文所述的架构,可以考虑BLoC模式。 BLoC模式入门 也是一个很好的起点。你会发现BLoC模式也不像传说的那么难以理解。

还有 其他 的,不过Provider和BLoC是目前最普遍采用的。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。

CSS权威指南(第三版)

CSS权威指南(第三版)

[美] Eric A.Meyer / 侯妍、尹志忠 / 中国电力出版社 / 2007-10 / 58.00

你是否既想获得丰富复杂的网页样式,同时又想节省时间和精力?本书为你展示了如何遵循CSS最新规范(CSS2和CSS2.1)将层叠样式表的方方面面应用于实践。 通过本书提供的诸多示例,你将了解如何做到仅在一处建立样式表就能创建或修改整个网站的外观,以及如何得到HTML力不能及的更丰富的表现效果。 资深CSS专家Eric A.Meyer。利用他独有的睿智和丰富的经验对属性、标记、标记属性和实......一起来看看 《CSS权威指南(第三版)》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具