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

最近,很多开发者都在学习 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:

使用 MobX 的 Crypto 应用

因此我决定构建一个示例应用程序,告诉大家使用 MobX 构建应用有多容易。项目地址: https://github.com/Zfinix/crypto_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';

WidgetcYM(doubley){
returnSizedBox(
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/

通常 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 类:

复制代码

classCryptoModel{
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() {
finalMap<String,dynamic>data= new Map<String,dynamic>();
if(this.data!=null) {
data['data'] =this.data.map((v) => v.toJson()).toList();
}
returndata;
}
}

classCryptoData{
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() {
finalMap<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();
}
returndata;
}
}

classMd{
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() {
finalMap<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;
returndata;
}
}

构建 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';

classCryptoController=CryptoControllerBasewith_$CryptoController;

abstractclassCryptoControllerBasewithStore{
@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 runbuild_runnerbuild-v

构建 API 请求处理程序

复制代码

import'dart:convert';
import'package:crypto_mobx/models/cryptoModel.dart';
import'package:http/http.dart'as http;

class Api {
staticfinalStringAPI_URL ='https://api.nomics.com/v1';

staticfinalStringAPI_KEY ='YOUR_API_KEY';

staticfinalStringGET_CURRENCIES ='$API_URL/currencies/ticker';

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

finalresponse = 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');
returnCryptoModel.fromJson(json.decode('{"data":${response.body}}'));
}else{
returnnull;
}
}catch(e) {
// 发出请求,服务器返回状态代码
// 代码超过 2xx 也不是 304
//if (e.response.body != null) {

print(e.toString());
}

returnnull;
}
}

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

复制代码

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

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

构建主页

复制代码

class_MyHomePageState extendsState<MyHomePage> {
final _controller =CryptoController();
{1}
@override
void initState() {
_loadData();
super.initState();
}
{1}
@override
Widgetbuild(BuildContextcontext) {
returnScaffold(
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()],
),
),
);
}
{1}

我首先构建的是 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(constRadius.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: (BuildContextcontext, int i) {
return CryptoCard(cryptoData: _controller?.cryptoData[i]);
},
),
),
);

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

Observer 小部件:

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

再见了 SetState:

我们需要做的就是

复制代码

_loadData()async {
var load = awaitApi.getData(context);
if(load != null)_controller.changeCryptoData(load.data);
}
}

最后是 CryptoCard 小部件:

复制代码

classCryptoCardextendsStatelessWidget{
finalCryptoDatacryptoData;
constCryptoCard({
Keykey,
@requiredthis.cryptoData,
}) : super(key:key);
{1}
@override
Widgetbuild(BuildContextcontext) {
returnDismissible(
onDismissed: (DismissDirectiondirection) {},
child:Container(
decoration:BoxDecoration(
borderRadius:newBorderRadius.all(constRadius.circular(10)),
color:Colors.white,
),
margin: constEdgeInsets.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),
);
}
{1}
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,
);
}
{1}

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

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

最后我想补充一点:MobX 的状态管理很棒,感谢阅读…

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

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章