再见,Flutter自带状态管理!你好,MobX库

再见,Flutter自带状态管理!你好,MobX库


再见,Flutter自带状态管理!你好,MobX库




作者 | Ogbonda Chiziaruhoma译者 | 王强编辑 |  Yonie最近,很多开发者都在学习 Flutter 开发跨端应用程序。由于 Flutter 目前尚未成熟,大家在开发的过程中肯定会遇到很多问题。本文重点介绍了一个 MobX 库,用来解决 Flutter 状态管理的技术痛点。

我开始用 Flutter 后,大多数项目都是在 Flutter 中编写的。终于有一天我遇到了 setState() 这座大山,想逃都逃不掉。它会同时处理很多类,带着一大堆动态数据,让代码变得丑陋不堪,写起来也像蜗牛一样慢;而且它会严重拖累应用程序的性能,因为你得不停从头至尾重建小部件树,哪怕变量值稍微改变一下也得折腾一次。

什么是状态管理

先看看这个:https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple。记住新项目中用的 Flutter 样板代码,看下它要改变代码中的变量值时是如何设置 setState 的;

int m = 2;
setState(() {
 m = 5;
});

print(m); // 输出 : 5

Dart 中的 SetState

什么是 MobX

MobX 是一个广受好评的库,它融入函数响应式编程(TFRP)原则简化了状态管理,使其容易扩展。地址:https://mobx.pub/

测试 MobX Flutter:再见,Flutter自带状态管理!你好,MobX库

使用 MobX 的 Crypto 应用

因此我决定构建一个示例应用程序,告诉大家使用 MobX 构建应用有多容易。项目地址:https://github.com/Zfinix/crypto_mobx。

项目结构:再见,Flutter自带状态管理!你好,MobX库

项目结构

打理项目结构是非常重要的,我创建项目时会精心做好这项工作;虽说它可能会随着项目发展而出现变化,但良好的结构会让代码更容易重构,更快找出错误,且更容易理解。注意:.g.dart 是 build_runner 包自动生成的代码,Flutter 新手就不要动它了。

设置依赖关系
dependencies:
 flutter:
   sdk: flutter
 # 下面将 Cupertino Icons 字体添加到你的应用。
 # 使用 CupertinoIcons 类用于 iOS 样式图标。
 cupertino_icons: ^0.1.2
 http: any
 mobx: 0.2.1+1
 flutter_mobx: ^0.2.0
 mobx_codegen: ^0.2.0
 flutter_svg: 0.13.0

dev_dependencies:
 build_runner:

pubspec.yaml

这里 flutter_mobx 是主要的插件,mobx_codegen 和 build_runner 用于代码生成。剧透:MobX 支持代码生成。

下面是我现在的 Dart 版本:

environment:
 sdk: ">=2.1.0 <3.0.0"
自定义间距小部件:
import 'package:flutter/material.dart';

Widget cYM(double y) {
 return SizedBox(
   height: y,
 );
}

你可能会注意到对方法 cYM() 和 cXM() 的引用。学习 Flutter 时,我需要一种方法来轻松地为移动应用添加间距。我知道有一个 Spacer() 小部件可以处理,但它对我来说还不够灵活,而且比较费时间。所以我创建了 cYM(Custom Y Margin,用来添加垂直间距)和 cXM(Custom X Margin,用来添加水平间距)。

设置 API 和 Model 类

我们将使用 Nomics Cryptocurrency & Bitcoin API:http://docs.nomics.com/

这里我们为 GET:/currency/ticker 端点提供了示例 JSON 响应。我们还使用在线工具从给定的 JSON 生成一个 Model 类。另外还有一个来自 Flutter 的 json_serializer 库。

工具: https://javiercbk.github.io/json_to_dart/

再见,Flutter自带状态管理!你好,MobX库

通常 JSON TO Dart 工具能正常工作,但在使用 JSON 数组时有个技巧。这里要用新的对象包装它;

把下面的代码:

[
 {
   "currency": "BTC",
   "id": "BTC",
   "price": "8451.36516421",
   "price_date": "2019-06-14",
   "symbol": "BTC",
   "circulating_supply": "17758462",
   "max_supply": "21000000",
   "name": "Bitcoin",
   "logo_url": "https://s3.us-east-2.amazonaws.com/nomics-api/static/images/currencies/btc.svg",
   "market_cap": "150083247116.70",
   "rank": "1",
   "high": "19404.81116899",
   "high_timestamp": "2017-12-16",
   "1d": {
     "price_change": "269.75208019",
     "price_change_pct": "0.03297053",
     "volume": "1110989572.04",
     "volume_change": "-24130098.49",
     "volume_change_pct": "-0.02",
     "market_cap_change": "4805518049.63",
     "market_cap_change_pct": "0.03 "
   }
 }
]

改成:

{
 “data”: [
 {
   "currency": "BTC",
   "id": "BTC",
   "price": "8451.36516421",
   "price_date": "2019-06-14",
   "symbol": "BTC",
   "circulating_supply": "17758462",
   "max_supply": "21000000",
   "name": "Bitcoin",
   "logo_url": "https://s3.us-east-2.amazonaws.com/nomics-api/static/images/currencies/btc.svg",
   "market_cap": "150083247116.70",
   "rank": "1",
   "high": "19404.81116899",
   "high_timestamp": "2017-12-16",
   "1d": {
     "price_change": "269.75208019",
     "price_change_pct": "0.03297053",
     "volume": "1110989572.04",
     "volume_change": "-24130098.49",
     "volume_change_pct": "-0.02",
     "market_cap_change": "4805518049.63",
     "market_cap_change_pct": "0.03 "
   }
 }
]
}

出现问题是正常的,这个工具还没有解析 JSON。看看我自己的 Model 类:

class CryptoModel {
 List<CryptoData> data;

 CryptoModel({this.data});

 CryptoModel.fromJson(Map<String, dynamic> json) {
   if (json['data'] != null) {
     data = new List<CryptoData>();
     json['data'].forEach((v) {
       data.add(new CryptoData.fromJson(v));
     });
   }
 }

 Map<String, dynamic> toJson() {
   final Map<String, dynamic> data = new Map<String, dynamic>();
   if (this.data != null) {
     data['data'] = this.data.map((v) => v.toJson()).toList();
   }
   return data;
 }
}

class CryptoData {
 String currency;
 String id;
 String price;
 String priceDate;
 String symbol;
 String circulatingSupply;
 String maxSupply;
 String name;
 String logoUrl;
 String marketCap;
 String rank;
 String high;
 String highTimestamp;
 Md md;

 CryptoData(
     {this.currency,
     this.id,
     this.price,
     this.priceDate,
     this.symbol,
     this.circulatingSupply,
     this.maxSupply,
     this.name,
     this.logoUrl,
     this.marketCap,
     this.rank,
     this.high,
     this.highTimestamp,
     this.md});

 CryptoData.fromJson(Map<String, dynamic> json) {
   currency = json['currency'];
   id = json['id'];
   price = json['price'];
   priceDate = json['price_date'];
   symbol = json['symbol'];
   circulatingSupply = json['circulating_supply'];
   maxSupply = json['max_supply'];
   name = json['name'];
   logoUrl = json['logo_url'];
   marketCap = json['market_cap'];
   rank = json['rank'];
   high = json['high'];
   highTimestamp = json['high_timestamp'];
   md = json['1d'] != null ? new Md.fromJson(json['1d']) : null;
 }

 Map<String, dynamic> toJson() {
   final Map<String, dynamic> data = new Map<String, dynamic>();
   data['currency'] = this.currency;
   data['id'] = this.id;
   data['price'] = this.price;
   data['price_date'] = this.priceDate;
   data['symbol'] = this.symbol;
   data['circulating_supply'] = this.circulatingSupply;
   data['max_supply'] = this.maxSupply;
   data['name'] = this.name;
   data['logo_url'] = this.logoUrl;
   data['market_cap'] = this.marketCap;
   data['rank'] = this.rank;
   data['high'] = this.high;
   data['high_timestamp'] = this.highTimestamp;
   if (this.md != null) {
     data['1d'] = this.md.toJson();
   }
   return data;
 }
}

class Md {
 String priceChange;
 String priceChangePct;
 String volume;
 String volumeChange;
 String volumeChangePct;
 String marketCapChange;
 String marketCapChangePct;

 Md(
     {this.priceChange,
     this.priceChangePct,
     this.volume,
     this.volumeChange,
     this.volumeChangePct,
     this.marketCapChange,
     this.marketCapChangePct});

 Md.fromJson(Map<String, dynamic> json) {
   priceChange = json['price_change'];
   priceChangePct = json['price_change_pct'];
   volume = json['volume'];
   volumeChange = json['volume_change'];
   volumeChangePct = json['volume_change_pct'];
   marketCapChange = json['market_cap_change'];
   marketCapChangePct = json['market_cap_change_pct'];
 }

 Map<String, dynamic> toJson() {
   final Map<String, dynamic> data = new Map<String, dynamic>();
   data['price_change'] = this.priceChange;
   data['price_change_pct'] = this.priceChangePct;
   data['volume'] = this.volume;
   data['volume_change'] = this.volumeChange;
   data['volume_change_pct'] = this.volumeChangePct;
   data['market_cap_change'] = this.marketCapChange;
   data['market_cap_change_pct'] = this.marketCapChangePct;
   return data;
 }
}
构建 Controller 类

接下来就是创造奇迹的时刻:你会创建一个新类。

根据 MobX 的文档:我可以按文档说明创建一个 Controller,而 mobx_codegen 将用它来生成 _$CryptoController 类。

这里 part 'homeController.g.dart; 负责指定将由 build_runner 创建的类。

而 part 和 part of 最近更多用于代码生成场景(不再用作已弃用的转换器了)

import 'package:crypto_mobx/models/cryptoModel.dart';
import 'package:mobx/mobx.dart';

part 'homeController.g.dart';

class CryptoController = CryptoControllerBase with _$CryptoController;

abstract class CryptoControllerBase with Store {
 @observable
 List<CryptoData> cryptoData;

 @action
 void changeCryptoData(List<CryptoData> value) => cryptoData = value;

}

查看 MobX Flutter 文档: https://pub.dev/packages/mobx

Build Runner:

运行 build runner 的命令如下:

Ogbondas-MacBook-Pro:crypto_mobx zfinix$ flutter packages pub run build_runner build -v
构建 API 请求处理程序
import 'dart:convert';
import 'package:crypto_mobx/models/cryptoModel.dart';
import 'package:http/http.dart' as http;

class Api {
 static final String API_URL = 'https://api.nomics.com/v1';

 static final String API_KEY = 'YOUR_API_KEY';

 static final String GET_CURRENCIES = '$API_URL/currencies/ticker';

 static Future<CryptoModel> getData(context) async {
   try {
     //POST REQUEST BUILD

     final response = await http.get('$GET_CURRENCIES?key=$API_KEY'
         '&ids=BTC,ETH,ETC,MTC,LTC,ICO,ETC,XRP'
         '&interval=1d,30d&convert=USD');
     print(response.body);

     if (response.statusCode == 200) {
       //  saveItem(item: '${response.body}', key: 'message');
       return CryptoModel.fromJson(json.decode('{"data":${response.body}}'));
     } else {
       return null;
     }
   } catch (e) {
     // 发出请求,服务器返回状态代码
     // 代码超过 2xx 也不是 304
     //if (e.response.body != null) {

     print(e.toString());
   }

   return null;
 }
}

我就喜欢这样做:创建一个返回数据 Model 的自定义类

return CryptoModel.fromJson(json.decode('{"data":${response.body}}'));

记住前面的小技巧:看看我是怎样将响应包装在 JSON 对象中的。

构建主页
class _MyHomePageState extends State<MyHomePage> {
 final _controller = CryptoController();

 @override
 void initState() {
   _loadData();
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     backgroundColor: Color.fromRGBO(245, 240, 240, 1),
     appBar: AppBar(
       brightness: Brightness.light,
       backgroundColor: Colors.white,
       elevation: 1,
       title: Text(
         'Crypto',
         style: TextStyle(color: Colors.black),
       ),
     ),
     body: Center(
       child: Column(
         mainAxisAlignment: MainAxisAlignment.center,
         children: <Widget>[_buildCard(), _buildList()],
       ),
     ),
   );
 }

我首先构建的是 UI,它由两大部分组成:一张卡片和下面的列表。

另外注意要实例化 mobx controller:

final _controller = CryptoController();

导入 MobX 状态 Controller 类

 卡片
  _buildCard() => Flexible(
       flex: 1,
       child: Container(
         width: MediaQuery.of(context).size.width,
         margin: const EdgeInsets.all(18.0),
         decoration: BoxDecoration(
           borderRadius: new BorderRadius.all(const Radius.circular(7.0)),
           gradient: LinearGradient(
             begin: Alignment.topRight,
             end: Alignment.bottomLeft,
             stops: [0.1, 0.5, 0.7, 0.9],
             colors: [
               Colors.pink[300],
               Colors.pink[400],
               Colors.red[300],
               Colors.red[400],
             ],
           ),
         ),
         child: Column(
           crossAxisAlignment: CrossAxisAlignment.center,
           mainAxisAlignment: MainAxisAlignment.center,
           children: <Widget>[
             Text(
               'Total Value',
               style: TextStyle(color: Colors.white),
             ),
             cYM(8),
             Text(
               '\$580.00',
               style: TextStyle(
                   color: Colors.white,
                   fontWeight: FontWeight.w500,
                   fontSize: 35),
             ),
           ],
         ),
       ),
     );

带有渐变背景的简单卡片

 ListView
  _buildList() => Flexible(
       flex: 2,
       child: Observer(
         builder: (_) => ListView.builder(
           itemCount: _controller?.cryptoData?.length ?? 0,
           itemBuilder: (BuildContext context, int i) {
             return CryptoCard(cryptoData: _controller?.cryptoData[i]);
           },
         ),
       ),
     );

为了安全起见,我们一定要设置默认值或回退值,尤其是对动态数据更是如此:所以你会注意到 _controller?.cryptoData?.length ?? 0,我这里正在检查空值,如果有空值就应该返回数组的长度。这些都借助了?? 运算符,如果值不为空就返回值本身,否则返回默认值。

 Observer 小部件:

这个实现是全文重点。它的工作机制是:Observer 小部件不是从上到下重建小部件树,而只重建它包装的小部件。在这种情况下唯一需要观察的值就是 _controller.cryptoData。

 再见了 SetState:

我们需要做的就是

  _loadData() async {
   var load = await Api.getData(context);
   if (load != null) _controller.changeCryptoData(load.data);
 }
}
 最后是 CryptoCard 小部件:
class CryptoCard extends StatelessWidget {
 final CryptoData cryptoData;
 const CryptoCard({
   Key key,
   @required this.cryptoData,
 }) : super(key: key);

 @override
 Widget build(BuildContext context) {
   return Dismissible(
     onDismissed: (DismissDirection direction) {},
     child: Container(
       decoration: BoxDecoration(
         borderRadius: new BorderRadius.all(const Radius.circular(10)),
         color: Colors.white,
       ),
       margin: const EdgeInsets.symmetric(horizontal: 18, vertical: 9),
       child: ListTile(
         contentPadding: EdgeInsets.all(10.0),
         leading: buildImage(),
         title: Row(
           mainAxisAlignment: MainAxisAlignment.spaceEvenly,
           children: <Widget>[
             Flexible(
               child: Column(
                 crossAxisAlignment: CrossAxisAlignment.start,
                 children: <Widget>[
                   Text(cryptoData?.name ?? '',
                       overflow: TextOverflow.clip,
                       style: TextStyle(
                           fontSize: 14, fontWeight: FontWeight.bold)),
                   cYM(8),
                   Text(
                     cryptoData?.symbol ?? '',
                     style: TextStyle(
                         fontWeight: FontWeight.w500,
                         color: Colors.grey,
                         fontSize: 11),
                   ),
                 ],
               ),
             ),
             cXM(8),
             Flexible(
               child: Column(
                 crossAxisAlignment: CrossAxisAlignment.end,
                 children: <Widget>[
                   Text(
                       '\$${double.parse(cryptoData.price).toStringAsFixed(2)}',
                       overflow: TextOverflow.clip,
                       style: TextStyle(
                           fontSize: 14, fontWeight: FontWeight.bold)),
                   cYM(8),
                   Text(
                     'Rank: ${cryptoData?.rank ?? 'NaN'}',
                     style: TextStyle(
                         fontWeight: FontWeight.w500,
                         color: Colors.grey,
                         fontSize: 11),
                   ),
                 ],
               ),
             ),
             Container(),
           ],
         ),
         trailing: Column(
           mainAxisAlignment: MainAxisAlignment.center,
           crossAxisAlignment: CrossAxisAlignment.end,
           children: <Widget>[
             Text(
                 '\$${double?.parse(cryptoData?.md?.priceChange ?? '0.00').toStringAsFixed(2) ?? 0.00}',
                 overflow: TextOverflow.clip,
                 style: TextStyle(
                     fontSize: 14,
                     color: Colors.black,
                     fontWeight: FontWeight.bold)),
             cYM(8),
             Text(
               '${double.parse(cryptoData.high).toStringAsFixed(2)}',
               style: TextStyle(
                   fontWeight: FontWeight.w500,
                   color: Colors.red,
                   fontSize: 11),
             ),
           ],
         ),
       ),
     ),
     key: Key(cryptoData.id),
   );
 }

 buildImage() => Card(
       child: cryptoData.logoUrl != null && cryptoData.logoUrl.contains('svg')
           ? CircleAvatar(
               maxRadius: 21.0,
               child: SvgPicture.network(cryptoData.logoUrl ?? ''),
               backgroundColor: Colors.white,
             )
           : CircleAvatar(
               maxRadius: 21.0,
               backgroundImage: NetworkImage(cryptoData?.logoUrl ??
                   'https://i.pinimg.com/originals/1f/7d/ec/1f7dec824ddfabb03b890b08d6c3e548.png'),
               backgroundColor: Colors.white,
             ),
       elevation: 3.0,
       shape: CircleBorder(),
       clipBehavior: Clip.antiAlias,
     );
}

请注意它所包装的 Dismissible 小部件,它需要的只是一个唯一的 Key,我将其设置为 API 返回的特定 ListItem 的 ID。而且图像是随机的,所以我必须检查它是 svg 还是 png/jpg,这里使用 三元运算符(condition ? return : else return) 返回对应的 SvgPicture.network() 或 NetworkImage()Widget。

另外.toStringAsFixed() 方法可以将 double 舍入到指定的小数位。

最后我想补充一点:MobX 的状态管理很棒,感谢阅读......再见,Flutter自带状态管理!你好,MobX库

英文原文: https://medium.com/future-vision/reactive-programming-in-flutter-state-management-with-mobx-a3a2ae1e8d1e

 活动推荐

随着 Flutter、Serverless、FaaS、TypeScript 等技术的成熟应用,前端开发上更趋多样化,更注重用户体验。想在学习如何重点聚焦在 Serverless 等技术的应用,前端性能优化,质量提升和工程化方面。请【扫码】或点击【阅读原文】了解详细信息。ArchSummit 全球架构师峰会,7 折限时直降 2640 元!了解详情请联系票务经理灰灰:15600537884 ,微信同号。




上一篇:MobX


下一篇:redux和mobx入门使用