再见,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 Flutter:MobX 是一个广受好评的库,它融入函数响应式编程(TFRP)原则简化了状态管理,使其容易扩展。地址:https://mobx.pub/
使用 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';
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/
通常 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 的状态管理很棒,感谢阅读......
英文原文: https://medium.com/future-vision/reactive-programming-in-flutter-state-management-with-mobx-a3a2ae1e8d1e
活动推荐随着 Flutter、Serverless、FaaS、TypeScript 等技术的成熟应用,前端开发上更趋多样化,更注重用户体验。想在学习如何重点聚焦在 Serverless 等技术的应用,前端性能优化,质量提升和工程化方面。请【扫码】或点击【阅读原文】了解详细信息。ArchSummit 全球架构师峰会,7 折限时直降 2640 元!了解详情请联系票务经理灰灰:15600537884 ,微信同号。