内容简介:上一篇中我记录了基于Flutter的开源中国客户端里网络请求和数据存储的部分,本篇记录的是app中插件的使用,由于很多功能并没有内置到Flutter中,所以我们需要引入一些插件来帮助我们完成某些功能,比如app内网页的加载,图库选择照片等。要使用插件,必须知道插件叫什么名字,目前是什么版本,Flutter提供了一个插件仓库,可以去上面搜索相关的插件,仓库地址为:pub.dartlang.org/,但是这个网站在国内可能访问不了,国内可以用Flutter专门为中国开发者提供的网站:pub.flutter-i
上一篇中我记录了基于Flutter的开源中国客户端里网络请求和数据存储的部分,本篇记录的是app中插件的使用,由于很多功能并没有内置到Flutter中,所以我们需要引入一些插件来帮助我们完成某些功能,比如app内网页的加载,图库选择照片等。
搜索插件包
要使用插件,必须知道插件叫什么名字,目前是什么版本,Flutter提供了一个插件仓库,可以去上面搜索相关的插件,仓库地址为:pub.dartlang.org/,但是这个网站在国内可能访问不了,国内可以用Flutter专门为中国开发者提供的网站:pub.flutter-io.cn/。该网站打开后直接在输入框中搜索关键字即可,如下图所示:
比如我们需要在app中用WebView加载网页,可以直接搜索'web view',再或者我们需要调用图库选择图片的功能,可以搜索'image picker',搜索结果可能有一大堆,怎么选择合适的插件呢?
由于我们是开发Flutter应用,所以要在搜索结果中过滤出供Flutter使用的插件,如下图所示:
过滤是第一步,过滤之后,还要查看插件包的更新日期,更新日期不能是很久前,因为很早之前发布的插件包,可能并不适合现在的Flutter版本,另外就是看这个插件后面的数字,数字越大表示插件匹配程度越高,如下图所示:
上面两步过滤之后,选择你觉得合适的插件,点进去看看详情,里面有相关的插件说明,示例用法,确定可以完成你所需要的功能,就可以愉快的在项目中添加插件依赖了。
基本上每个插件的主页都会有说明如何在项目中添加该插件的依赖,比如在我们这个基于Flutter的开源中国客户端中,用到了 flutter_webview_plugin
这个插件,在该插件的主页里,就有怎么引入依赖的说明:
使用flutter_webview_plugin插件
在基于Flutter的开源中国客户端项目中,用户登录和资讯详情等页面都使用了WebView加载网页,使用的是 flutter_webview_plugin
这个插件。该插件主要功能是可以在Flutter页面中加载一个WebView,并且可以监听WebView的各种状态比如加载中,加载完成等,而且还能读取WebView中的cookies,或者通过dart代码调用WebView中的js方法。
开源中国提供的基于oauth的认证流程大致如下:
- 在开源中国后台添加应用,完善应用的信息,最主要的是回调地址,该地址将会在后面用到;
- 使用浏览器或者WebView加载三方认证页面,在该页面中输入开源中国的用户名和密码(输入密码的页面为开源中国提供的页面,第三方是无法获取密码信息的);
- 输入用户名和密码后点击页面上的登录按钮,若登录成功,将会跳转到第一步我们在后台配置的回调地址上,并给该页面传入一个code参数(code参数直接拼接在URL上);
- 在该页面中接收code参数,并根据开源中国后台提供的
client_id
client_secret
等参数换取token信息(这一步就是一个get请求,只不过放在我自己的服务端进行了); - 上面的请求成功后,开源中国的openapi会返回token等信息,在我们的回调页面将这个信息通过js的一个
get()
方法暴露出来,让dart代码去调用。
具体的oauth认证流程可以查看开源中国的文档:文档地址
构造登录页面
在 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)。
监听WebView的加载状态和URL变化
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()
dart调用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'字符串,将会在这里被接收,接收到就可以刷新“我的”页面了(刷新用户昵称和头像)。
使用event_bus插件
上面最后的 _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去获取动弹信息,显示如下界面:
关于动弹列表的加载,这里就不详细说明了,文末会给出源码链接。
使用image_picker插件
在发送动弹的页面,有选择图片的功能,如下图所示:
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中已有内置的组件可直接使,当我们点击:heavy_plus_sign:选择图片时,调用 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支持一下:joy:)
-
本项目中还有一些已知和未知的bug,已知的bug是token过期后没有做自动刷新处理(开源中国给的token是有有效期的,过期后需要使用refresh_token去刷新access_token),未知的一些bug可能会导致app在运行过程中ANR,由于没有对各个机型做测试,所以暂时不知道ANR是什么原因导致的,但是在开发过程中会偶现插件的报错,希望各位发现bug可以及时与我联系(文末留言或者github提issue都行),感谢你们的支持!
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 开源中国 Android 客户端 v2.8.6 代码开源
- 开源中国 iOS 客户端 v3.8.6 代码开源
- 开源中国 Android 客户端 v2.8.9 代码开源
- Flutter豆瓣客户端(仿),诚心开源
- 自己动手做数据库客户端: BashSQL开源数据库客户端
- 开源中国 Android 客户端 v2.8.8 发布
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。