上一篇中我记录了基于Flutter的开源中国客户端里网络请求和数据存储的部分,本篇记录的是app中插件的使用,因为不少功能并无内置到Flutter中,因此咱们须要引入一些插件来帮助咱们完成某些功能,好比app内网页的加载,图库选择照片等。php
要使用插件,必须知道插件叫什么名字,目前是什么版本,Flutter提供了一个插件仓库,能够去上面搜索相关的插件,仓库地址为:pub.dartlang.org/,可是这个网站在国内可能访问不了,国内能够用Flutter专门为中国开发者提供的网站:pub.flutter-io.cn/。该网站打开后直接在输入框中搜索关键字便可,以下图所示:前端
好比咱们须要在app中用WebView加载网页,能够直接搜索'web view',再或者咱们须要调用图库选择图片的功能,能够搜索'image picker',搜索结果可能有一大堆,怎么选择合适的插件呢?git
因为咱们是开发Flutter应用,因此要在搜索结果中过滤出供Flutter使用的插件,以下图所示:github
过滤是第一步,过滤以后,还要查看插件包的更新日期,更新日期不能是好久前,由于很早以前发布的插件包,可能并不适合如今的Flutter版本,另外就是看这个插件后面的数字,数字越大表示插件匹配程度越高,以下图所示:web
上面两步过滤以后,选择你以为合适的插件,点进去看看详情,里面有相关的插件说明,示例用法,肯定能够完成你所须要的功能,就能够愉快的在项目中添加插件依赖了。json
基本上每一个插件的主页都会有说明如何在项目中添加该插件的依赖,好比在咱们这个基于Flutter的开源中国客户端中,用到了flutter_webview_plugin
这个插件,在该插件的主页里,就有怎么引入依赖的说明:api
在基于Flutter的开源中国客户端项目中,用户登陆和资讯详情等页面都使用了WebView加载网页,使用的是flutter_webview_plugin
这个插件。该插件主要功能是能够在Flutter页面中加载一个WebView,而且能够监听WebView的各类状态好比加载中,加载完成等,并且还能读取WebView中的cookies,或者经过dart代码调用WebView中的js方法。浏览器
开源中国提供的基于oauth的认证流程大体以下:bash
client_id
client_secret
等参数换取token信息(这一步就是一个get请求,只不过放在我本身的服务端进行了);get()
方法暴露出来,让dart代码去调用。具体的oauth认证流程能够查看开源中国的文档:文档地址cookie
在lib/pages/
目录下新建LoginPage.dart
文件,并使用flutter_webview_plugin
插件提供的WebviewScaffold
组件,该组件会在页面上渲染一个WebView用于加载某个URL,代码以下:
@override
Widget build(BuildContext context) {
List<Widget> titleContent = [];
titleContent.add(new Text(
"登陆开源中国",
style: new TextStyle(color: Colors.white),
));
if (loading) {
// 若是还在加载中,就在标题栏上显示一个圆形进度条
titleContent.add(new CupertinoActivityIndicator());
}
titleContent.add(new Container(width: 50.0));
// WebviewScaffold是插件提供的组件,用于在页面上显示一个WebView并加载URL
return new WebviewScaffold(
key: _scaffoldKey,
url: Constants.LOGIN_URL, // 登陆的URL
appBar: new AppBar(
title: new Row(
mainAxisAlignment: MainAxisAlignment.center,
children: titleContent,
),
iconTheme: new IconThemeData(color: Colors.white),
),
withZoom: true, // 容许网页缩放
withLocalStorage: true, // 容许LocalStorage
withJavascript: true, // 容许执行js代码
);
}
复制代码
上面的代码中,咱们给AppBar组件上加了标题,还加了一个圆形的进度条,用于指示WebView加载的状态,若是在加载中,就显示进度条,不然就隐藏进度条(因此LoginPage类应该继承StatefulWidget)。
flutter_webview_plugin
插件提供的api能够监听WebView加载的状态和URL的变化,主要代码以下:
// 登陆页面,使用网页加载的开源中国三方登陆页面
class LoginPage extends StatefulWidget {
@override
State<StatefulWidget> createState() => new LoginPageState();
}
class LoginPageState extends State<LoginPage> {
// 标记是不是加载中
bool loading = true;
// 标记当前页面是不是咱们自定义的回调页面
bool isLoadingCallbackPage = false;
GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey();
// URL变化监听器
StreamSubscription<String> _onUrlChanged;
// WebView加载状态变化监听器
StreamSubscription<WebViewStateChanged> _onStateChanged;
// 插件提供的对象,该对象用于WebView的各类操做
FlutterWebviewPlugin flutterWebViewPlugin = new FlutterWebviewPlugin();
@override
void initState() {
super.initState();
// 监听WebView的加载事件,该监听器已不起做用,不回调
_onStateChanged = flutterWebViewPlugin.onStateChanged.listen((WebViewStateChanged state) {
// state.type是一个枚举类型,取值有:WebViewState.shouldStart, WebViewState.startLoad, WebViewState.finishLoad
switch (state.type) {
case WebViewState.shouldStart:
// 准备加载
setState(() {
loading = true;
});
break;
case WebViewState.startLoad:
// 开始加载
break;
case WebViewState.finishLoad:
// 加载完成
setState(() {
loading = false;
});
if (isLoadingCallbackPage) {
// 当前是回调页面,则调用js方法获取数据
parseResult();
}
break;
}
});
_onUrlChanged = flutterWebViewPlugin.onUrlChanged.listen((url) {
// 登陆成功会跳转到自定义的回调页面,该页面地址为http://yubo725.top/osc/osc.php?code=xxx
// 该页面会接收code,而后根据code换取AccessToken,并将获取到的token及其余信息,经过js的get()方法返回
if (url != null && url.length > 0 && url.contains("osc/osc.php?code=")) {
isLoadingCallbackPage = true;
}
});
}
}
复制代码
上面代码的逻辑是:
parseResult()
方法调用js代码获取token信息了。parseResult()
方法中就是dart调用js代码的逻辑了,flutter_webview_plugin
插件提供了API供咱们很方便的用dart代码调用js代码,下面是parseResult()
方法的代码:
// 解析WebView中的数据
void parseResult() {
flutterWebViewPlugin.evalJavascript("get();").then((result) {
// result json字符串,包含token信息
if (result != null && result.length > 0) {
// 拿到了js中的数据
try {
// what the fuck?? need twice decode??
var map = json.decode(result); // s is String
if (map is String) {
map = json.decode(map); // map is Map
}
if (map != null) {
// 登陆成功,取到了token,关闭当前页面
DataUtils.saveLoginInfo(map);
Navigator.pop(context, "refresh");
}
} catch (e) {
print("parse login result error: $e");
}
}
});
}
复制代码
主要方法是flutterWebViewPlugin.evalJavascript()
传入的参数是一个字符串,表示要执行的js代码。上面的代码意思是执行页面中的get()
方法,在该方法中返回了token等信息,而后在then
中解析这些信息,并调用DataUtils.saveLoginInfo(map);
保存登陆信息,这就到了上一篇中我记录的数据保存的部分了。数据保存后调用Navigator.pop(context, "refresh");
方法将当前页推出栈,后面的"refresh"参数有什么做用呢?
"refresh"的做用就是为了让上一个页面刷新(这里只是一个字符串参数,定义成什么样子彻底取决于你本身)。若是是作过Android开发的朋友,应该会很熟悉,咱们要把当前页的数据传递给上一个页面,通常会在上一个页面用startActivityForResult方法启动当前页,上一个页面会在onActivityResult回调方法中接收参数。Flutter的作法跟这个有点相似,在“个人”页面中打开登陆页时,使用下面的方法:
_login() async {
// 打开登陆页并处理登陆成功的回调
final result = await Navigator
.of(context)
.push(new MaterialPageRoute(builder: (context) {
return new LoginPage();
}));
// result为"refresh"表明登陆成功
if (result != null && result == "refresh") {
// 刷新用户信息
getUserInfo();
// 通知动弹页面刷新
Constants.eventBus.fire(new LoginEvent());
}
}
复制代码
上面的代码应该很明了了吧,Navigator
的push
方法返回的是一个Future对象,因此咱们能够在then里面处理登陆页返回的信息,登陆页pop时传入的'refresh'字符串,将会在这里被接收,接收到就能够刷新“个人”页面了(刷新用户昵称和头像)。
上面最后的_login()
方法的代码中,咱们收到了"refresh"参数后,获取并刷新了页面的用户信息,而后还调用了一行代码用于刷新动弹页面:
Constants.eventBus.fire(new LoginEvent());
复制代码
这行代码就用到了另一个框架:event_bus
若是作过Android开发或者前端开发,应该对这个框架不陌生。EventBus是一个发布/订阅模式的框架,用于在某个页面订阅某个事件,而后在另外的地方触发这个事件,订阅这个事件的方法就会被执行。
该框架在pub仓库的主页是:pub.flutter-io.cn/packages/ev…
该插件的用法很简单,首先是导入包:
import 'package:event_bus/event_bus.dart';
复制代码
若是要订阅某个事件,使用下面的代码:
new EventBus().on(MyEvent).listen((event) {
// 处理事件
});
复制代码
其中MyEvent
是自定义的一个类,表示惟一的一个事件。若是要监听全部的事件,on
方法中能够不传参数。
要发送某个事件,能够用以下代码:
new EventBus().fire(new MyEvent());
复制代码
使用fire
方法发送某个事件,参数就是这个自定义的事件对象,能够在这个对象中加入任何你须要的参数。
在基于Flutter的开源中国客户端项目中,能够只用到一个EventBus对象,不必在每次用的时候都new EventBus()
,因此咱们在lib/constants/Constants.dart
中定义了一个静态的eventBus变量,全局均可以共用这一个对象:
static EventBus eventBus = new EventBus();
复制代码
在登陆成功后,调用以下代码来通知动弹列表刷新:
Constants.eventBus.fire(new LoginEvent());
复制代码
LoginEvent是一个空的类,表示登陆成功的事件。
在动弹列表页,还要为登陆成功的事件加上监听:
Constants.eventBus.on(LoginEvent).listen((event) {
setState(() {
this.isUserLogin = true;
});
});
复制代码
动弹列表页根据上面的isUserLogin变量加载不一样的页面,若是该变量为false,表示当前没有登陆,则显示以下界面:
若是该变量为true,则会调用开源中国的api去获取动弹信息,显示以下界面:
关于动弹列表的加载,这里就不详细说明了,文末会给出源码连接。
在发送动弹的页面,有选择图片的功能,以下图所示:
Flutter并无提供相关API供咱们操做移动设备的图库,因此这里又用到了image_picker插件,该插件的地址在这里:pub.flutter-io.cn/packages/im…
导入插件的代码以下:
import 'package:image_picker/image_picker.dart';
复制代码
插件的使用方法也比较简单,以下代码:
// source是一个枚举值,可取值有ImageSource.camera和ImageSource.gallery,分别表明调用相机和图库
_imageFile = ImagePicker.pickImage(source: source);
复制代码
上图中的弹出菜单在Flutter中已有内置的组件可直接使,当咱们点击➕选择图片时,调用pickImage
方法,代码以下:
// 相机拍照或者从图库选择图片
pickImage(ctx) {
// 若是已添加了9张图片,则提示不容许添加更多
num size = fileList.length;
if (size >= 9) {
Scaffold.of(ctx).showSnackBar(new SnackBar(
content: new Text("最多只能添加9张图片!"),
));
return;
}
// Flutter提供的API,用于显示一个底部弹出的Dialog
showModalBottomSheet<void>(context: context, builder: _bottomSheetBuilder);
}
// 自定义底部菜单的布局
Widget _bottomSheetBuilder(BuildContext context) {
return new Container(
height: 182.0,
child: new Padding(
padding: const EdgeInsets.fromLTRB(0.0, 30.0, 0.0, 30.0),
child: new Column(
children: <Widget>[
_renderBottomMenuItem("相机拍照", ImageSource.camera),
new Divider(height: 2.0,),
_renderBottomMenuItem("图库选择照片", ImageSource.gallery)
],
),
)
);
}
// 渲染底部菜单的每一个item
_renderBottomMenuItem(title, ImageSource source) {
var item = new Container(
height: 60.0,
child: new Center(
child: new Text(title)
),
);
return new InkWell(
child: item,
onTap: () {
// 点击菜单item,关闭这个底部弹窗并调用相机或者图库
Navigator.of(context).pop();
setState(() {
_imageFile = ImagePicker.pickImage(source: source);
});
},
);
}
复制代码
上面代码中的_imageFile
是一个Future<File>
对象,由于选择图片的操做是异步的,那么在什么地方接收选择的图片呢?不管是拍照仍是图库选择,最后调用ImagePicker.pickImage(source: source)
返回的都是一个文件对象,在image_picker
主页给出的示例代码中,是以组件的形式返回一个FutureBuilder<File>
对象,在该对象的builder
方法中接收返回的图片文件的。
在基于Flutter的开源中国客户端项目中,接收选择的图片是放在build方法中的,PublishTweetPage页面的build
方法代码以下:
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("发布动弹", style: new TextStyle(color: Colors.white)),
iconTheme: new IconThemeData(color: Colors.white),
actions: <Widget>[
new Builder(
builder: (ctx) {
return new IconButton(icon: new Icon(Icons.send), onPressed: () {
// 发送动弹
DataUtils.isLogin().then((isLogin) {
if (isLogin) {
return DataUtils.getAccessToken();
} else {
return null;
}
}).then((token) {
sendTweet(ctx, token);
});
});
},
)
],
),
// 在这里接收选择的图片
body: new FutureBuilder(
future: _imageFile,
builder: (BuildContext context, AsyncSnapshot<File> snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.data != null && _imageFile != null) {
// 选择了图片(拍照或图库选择),添加到List中
fileList.add(snapshot.data);
_imageFile = null;
}
// 返回的widget
return getBody();
},
),
);
}
复制代码
在AppBar的右边添加了一个按钮,用于发送动弹信息。在body部分返回了一个FutureBuilder
对象,在该对象的builder
方法中接收了选中的图片文件,并将该文件加入到图片列表中,而后调用getBody()
方法返回整个页面,这么作的缘由是由于每次选中一张图片后,都须要将页面刷新,在getBody()
方法中会用到fileList
变量,getBody()
方法代码以下:
Widget getBody() {
// 输入框
var textField = new TextField(
decoration: new InputDecoration(
hintText: "说点什么吧~",
hintStyle: new TextStyle(
color: const Color(0xFF808080)
),
border: new OutlineInputBorder(
borderRadius: const BorderRadius.all(const Radius.circular(10.0))
)
),
// 最多显示6行文本(不表明最多只能输入6行)
maxLines: 6,
// 最多输入的文字数
maxLength: 150,
// 经过_controller.text能够获取输入框中输入的文本
controller: _controller,
);
// gridView用来显示选择的图片
var gridView = new Builder(
builder: (ctx) {
return new GridView.count(
// 分4列显示
crossAxisCount: 4,
children: new List.generate(fileList.length + 1, (index) {
// 这个方法体用于生成GridView中的一个item
var content;
if (index == 0) {
// 添加图片按钮
var addCell = new Center(
child: new Image.asset('./images/ic_add_pics.png', width: 80.0, height: 80.0,)
);
content = new GestureDetector(
onTap: () {
// 添加图片
pickImage(ctx);
},
child: addCell,
);
} else {
// 被选中的图片
content = new Center(
child: new Image.file(fileList[index - 1], width: 80.0, height: 80.0, fit: BoxFit.cover,)
);
}
return new Container(
margin: const EdgeInsets.all(2.0),
width: 80.0,
height: 80.0,
color: const Color(0xFFECECEC),
child: content,
);
}),
);
},
);
var children = [
new Text("提示:因为OSC的openapi限制,发布动弹的接口只支持上传一张图片,本项目可添加最多9张图片,但OSC只会接收最后一张图片。", style: new TextStyle(fontSize: 12.0),),
textField,
new Container(
margin: const EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 0.0),
height: 200.0,
child: gridView
)
];
if (isLoading) { // 上传图片可能会比较慢,因此这里显示loading
children.add(new Container(
margin: const EdgeInsets.fromLTRB(0.0, 20.0, 0.0, 0.0),
child: new Center(
child: new CircularProgressIndicator(),
),
));
} else { // 上传成功后显示msg
children.add(new Container(
margin: const EdgeInsets.fromLTRB(0.0, 20.0, 0.0, 0.0),
child: new Center(
child: new Text(msg),
)
));
}
return new Container(
padding: const EdgeInsets.all(5.0),
child: new Column(
children: children,
),
);
}
复制代码
获取到了选择的图片和输入的动弹内容,下一步是发送动弹,发送动弹调用的是开源中国的openapi,这里涉及到使用dart上传图片的问题,下面先上代码:
sendTweet(ctx, token) async {
// 未登陆或者未输入动弹内容时,使用SnackBar提示用户
if (token == null) {
Scaffold.of(ctx).showSnackBar(new SnackBar(
content: new Text("未登陆!"),
));
return;
}
String content = _controller.text;
if (content == null || content.length == 0 || content.trim().length == 0) {
Scaffold.of(ctx).showSnackBar(new SnackBar(
content: new Text("请输入动弹内容!"),
));
}
// 下面是调用接口发布动弹的逻辑
try {
Map<String, String> params = new Map();
params['msg'] = content;
params['access_token'] = token;
// 构造一个MultipartRequest对象用于上传图片
var request = new MultipartRequest('POST', Uri.parse(Api.PUB_TWEET));
request.fields.addAll(params);
if (fileList != null && fileList.length > 0) {
// 这里虽然是添加了多个图片文件,可是开源中国提供的接口只接收一张图片
for (File f in fileList) {
// 文件流
var stream = new http.ByteStream(
DelegatingStream.typed(f.openRead()));
// 文件长度
var length = await f.length();
// 文件名
var filename = f.path.substring(f.path.lastIndexOf("/") + 1);
// 将文件加入到请求体中
request.files.add(new http.MultipartFile(
'img', stream, length, filename: filename));
}
}
setState(() {
isLoading = true;
});
// 发送请求
var response = await request.send();
// 解析请求返回的数据
response.stream.transform(utf8.decoder).listen((value) {
print(value);
if (value != null) {
var obj = json.decode(value);
var error = obj['error'];
setState(() {
if (error != null && error == '200') {
// 成功
setState(() {
isLoading = false;
msg = "发布成功";
fileList.clear();
});
_controller.clear();
} else {
setState(() {
isLoading = false;
msg = "发布失败:$error";
});
}
});
}
});
} catch (exception) {
print(exception);
}
}
复制代码
使用dart上传图片的代码和普通的get/post请求是彻底不同的,上传图片须要构造一个Request对象:
var request = new MultipartRequest('POST', Uri.parse(Api.PUB_TWEET));
复制代码
添加普通的参数须要调用request.field.addAll方法:
request.fields.addAll(params); // params是参数map
复制代码
添加文件参数时,须要调用request.files.add方法:
request.files.add(new http.MultipartFile(
'img', stream, length, filename: filename));
复制代码
解析返回的数据时须要使用以下代码:
// 发送请求
var response = await request.send();
// 解析请求返回的数据
response.stream.transform(utf8.decoder).listen((value) {})
复制代码
关于发送动弹的详细代码,能够参考文末的源码连接,这里再也不说明。
本篇相关的全部源码都在GitHub上flutter-osc项目。
本篇主要记录的是基于Flutter的开源中国客户端app中的各类插件的使用。
二维码扫描的插件使用在本篇中没有作记录,各位小伙伴可自行上pub仓库搜索插件用法。
本系列博客并未将全部功能的实现方法都记录下来,只是有选择性的记录了一部分功能的实现。
本项目中还有不少功能暂未实现,好比动弹大图预览、我的信息页的展现等。大部分的功能都是以WebView的形式加载的,因此总体来看app的实现并不复杂,代码量也并很少,开源出来但愿给学习Flutter的小伙伴们一点帮助。(若是对你有帮助,请在github给个start支持一下😂)
本项目中还有一些已知和未知的bug,已知的bug是token过时后没有作自动刷新处理(开源中国给的token是有有效期的,过时后须要使用refresh_token去刷新access_token),未知的一些bug可能会致使app在运行过程当中ANR,因为没有对各个机型作测试,因此暂时不知道ANR是什么缘由致使的,可是在开发过程当中会偶现插件的报错,但愿各位发现bug能够及时与我联系(文末留言或者github提issue都行),感谢大家的支持!
上一篇 |
---|
从0开始写一个基于Flutter的开源中国客户端(7)——App网络请求和数据存储 |