最近,很多开发者都在学习 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 的状态管理很棒,感谢阅读…
更多内容推荐
为啥 Flutter Hooks 没有受到太多关注和青睐?
Flutter Hooks虽然面世已经有一段时间了,但是迄今为止它并没有受到太多关注和青睐。
为什么说 Flutter 让移动开发变得更好?
如果你是Android开发者,那么可能已经听说过Flutter。 这是一个相对较新,用来开发跨平台原生应用的框架。 这不是第一个移动领域用于跨平台开发的框架,但它正在被谷歌使用,得益于谷歌的实力,让Flutter有一定的可信度。 尽管最初持有保留意见,但我决定尝试一下 - 结果Flutter在一周内彻底改变了我对移动开发的看法。 下面是我学到的东西。
RTC Dev Meetup:Flutter 开发实战与前景展望(二)
关键字的定义。Flutter 的启动类用的就是 mixins 方式的封装接口方便调用。条件。就可以通过以下执行
预习篇 · 从 0 开始搭建 Flutter 工程环境
今天这篇文章要达到的目的是,帮助你完成Flutter开发测试环境的安装配置。
2019 年 7 月 1 日
cover-view 原生视图容器
无
2018 年 3 月 30 日
如何使用 BLoC 架构开发 Flutter 应用
尽管 Flutter 还是一项相当年轻的技术,但它正在受到越来越多的欢迎。2015 年,它以 Sky 为名首次出现,2017 年,成为我们熟知和正在使用的 Flutter。Flutter 受到谷歌的支持,它允许开发人员创建感觉像本地那么漂亮而又经济的跨平台应用程序。
关于 SwiftUI,看这一篇就够了
本文介绍Swift 5.1语法新特性和SwiftUI的优势。
iOS 原生、大前端和 Flutter 分别是怎么渲染的?
今天这篇文章,我首先和你说了渲染的原理,然后分别和你展开了原生、大前端、Flutter 是怎么渲染的。
2019 年 6 月 18 日
Vue.js 3:面向未来的编程
Vue.js 3即将发布,将会提供基于函数的API,为代码复用开发提供更好的方案。
Basecamp 发布 JavaScript 框架 Stimulus 1.0
Basecamp全新推出Stimulus 1.0,该产品强调HTML页面上JavaScript轻量级的实现,取代了固有的全功能JavaScript应用程序。Basecamp称其为“你所拥有的最适用于HTML的JavaScript框架”。
Vue 3 Composition API 实战前瞻
本文作者对 Composition API 与当前的 Options API 作出了一番对比,并对 Composition API 的特征与用途进行了通俗易懂的总结。
特别放送 | 温故而知新,与你说说专栏的那些思考题
为了帮助你更好地理解专栏的核心知识点,我特意整理了每篇文章的课后思考题,并结合大家在留言区的回答情况做一次分析与扩展。
2019 年 11 月 20 日
MTFlutter:美团外卖 Flutter 容器化生态建设实践
无
2020 年 1 月 20 日
写给前端工程师的 Flutter 教程(下)
Vue、React。Flutter。图啥?低成本地为用户带来更优秀的用户体验。目前来说Flutter可能是其中最优秀的一种方案了。
用 React Hooks 重构你的小程序
在 GMTC 北京 2019 大会上,来自京东的余澈讲师做了《用 React Hooks 重构你的小程序》主题演讲。
使用 AngularJS 构建大型 Web 应用
AngularJS是由Google创建的一种JS框架,使用它可以扩展应用程序中的HTML词汇,从而在web应用程序中使用HTML声明动态内容。在该团队工作的软件工程师Brian Ford近日撰写了一篇blog,分享了如何使用AngularJS构建大型Web应用的经验。这些经验对于使用其他JS框架构建大型应用的开发者也极具借鉴意义。
为什么原生应用开发者需要关注 Flutter
Flutter是由谷歌创建的一个移动应用SDK,用于构建“现代移动应用”。目前它还处于alpha阶段,不过它的文档和相关工具十分齐全,有些移动应用已经在使用Flutter。在这篇文章里,Animesh Jain将分享使用Flutter开发一个移动应用的愉快经历,并告诉大家为什么我这么喜欢Flutter。
我用 React 和 Vue 构建了同款应用,来看看哪里不一样
我阅读了很多React文档并观看了一些教学视频,它们的确很棒,但我真正想知道的是React与Vue在代码层面有哪些不同。
如何构造炫酷的动画效果?
在今天的这篇文章中,我会向你介绍Flutter中动画的实现方法,看看如何让我们的页面动起来。
2019 年 8 月 17 日
推荐阅读
如何手撸一个 Vue 3.0 异步函数 API?
用户交互事件该如何响应?
2019 年 8 月 10 日
如何在原生应用中混编 Flutter 工程?
2019 年 8 月 31 日
谷歌的跨平台移动 UI 框架 Flutter 开始 Beta 测试
Flutter 状态 State 的 5 种应对方法
React Hooks 会取代 Redux 吗?
除了 Cocoa,iOS 还可以用哪些 GUI 框架开发?
2019 年 4 月 27 日
电子书
大厂实战PPT下载
换一换 贾岩涛 | 华为 中央软件院知识图谱首席技术专家
王喆 | Roku 资深机器学习工程师 《深度学习推荐系统实战》专栏作者
郭忆 | 网易大数据专家 《数据中台实战课》作者
评论