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

2019 年 8 月 21 日

再见,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';
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


2019 年 8 月 21 日 08:104280

评论

发布
暂无评论
发现更多内容

第二周 框架设计 学习总结

应鹏

学习 极客大学架构师训练营

架构一期二班 - 吴水金 - 第二课总结

吴水金

第二周课后练习 - 作业 2

致星海

架构师训练营第 1 期 -Week2 - 框架设计学习总结

鲁小鲁

极客大学架构师训练营 面向对象设计原则 框架设计

作业二:第二周学习总结

静海

逆序局部链表、Paxos算法原理、架构师发现问题所在 John 易筋 ARTS 打卡 Week 19

John(易筋)

ARTS 打卡计划 发现问题的真正所在 逆序局部链表

架构师训练营第一期 - 第二周学习总结

卖猪肉的大叔

第二周作业

架构师训练营第一期第2周作业及总结

木头发芽

训练营-第二周-作业一

行者

第二周总结

_

极客大学架构师训练营 第二周总结

第二周作业 (作业二)

Geek_83908e

极客大学架构师训练营

week2

Geek_deb968

架构第二周课后练习

Geek_Gu

极客大学架构师训练营

架构师训练营第 1 期 -Week2 - 课后练习

鲁小鲁

极客大学架构师训练营 依赖倒置原则 接口隔离原则 框架设计 软件设计原则

架构训练营第二周练习

灰羽零

用十六年时间,造一座声音“博物馆”:OPPO的影音进击之路

脑极体

medo 支付系统架构设计

陈皮

第二周课后练习-作业1

致星海

架构师训练营Week02作业

IT老兵重开始

极客大学架构师训练营

第二周总结

【架构师训练营第 1 期 02 周】 学习总结

Bear在挨踢

极客大学架构师训练营

架构师训练营作业:第二周

m

艺术家们手握“飞桨” 划出金秋UCCA最值得一看的AI艺术展

脑极体

架构训练营第二周学习总结

灰羽零

week02总结

xxx

【FastDFS】面试官:如何实现文件的大规模分布式存储?(全程实战)

冰河

高并发 高性能 分布式存储 fastdfs 海量存储

第二周学习总结

alpha

极客大学架构师训练营

Week 2 作業一 : OOD 5 principles [SOLID]

--------世界中心---------

OOD SOLID 架構師 面向對象編程 面向對象編程原則

week02 题目

xxx

问题集锦:DBType not support : null

互联网应用架构

mybaitsplus

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