Flutter 实战详解 -- 高仿好奇心日报,kotlin 核心编程
dependencies: json_annotation: ^2.0.0 dev_dependencies: build_runner: ^1.0.0 json_serializable: ^2.0.0
创建一个 model.dart 文件 引入文件
import 'package:json_annotation/json_annotation.dart'; part 'model.g.dart';
其中这个 model.g.dart 等会儿会自动生成.这里需要掌握两个知识点
1.@JsonSerializable() 这是表示告诉编译器这个类是需要生成 Model 类的 2,@JsonKey 由于服务器返回的部分数据名称在 Dart 语言中是不被允许的,比如 has_more,Dart 中命名不能出现下划线,所以就需要用到 @JsonKey 来告诉编译器这个参数对于 json 中的哪个字段
@JsonSerializable()class Feed {String image;int type;@JsonKey(name: 'index_type')int indexType;Post post;@JsonKey(name: 'news_list')List<News> newsList;Feed(this.image,this.type,this.post,this.indexType,this.newsList);factory Feed.fromJson(Map<String,dynamic> json) => _FeedFromJson(json);Map<String, dynamic> toJson() => _FeedToJson(this);}
好了,写完后会报错,因为FeedFromJson
和FeedToJson
没有找到,这个时候在控制到输入flutter packages pub run build_runner build
指令后会自动生成一个moded.g.dart
文件,于是在网络请求下来数据后就可以用Feed feed = Feed.fromJson(data)
这个方法来将 Json 中数据转换保存在 Feed 这个实例中了.在 model 类中还有些复杂的 Json 嵌套,但是也都很简单,大家看一眼应该就会了,哈哈.[JSON 和序列化具体教程](
)
5.轮播图
Flutter 中的轮播图我用到了[Flutter_Swiper](
)这个组件,这里设置小圆点属性的时候稍微麻烦了点,网上好像也没有讲到,我这里讲一下. 首先要创建 DotSwiperPaginationBuilder
DotSwiperPaginationBuilder builder = DotSwiperPaginationBuilder(color: Colors.white,//未选中圆点颜色 activeColor: Colors.yellow,//选中圆点颜色 size:7,//未选中大小 activeSize: 7,//选中圆点大小 space: 5//圆点间距);
然后在 Swiper 中的 pagination 属性中设置它
pagination: new SwiperPagination(builder: builder,),
网络请求 首先,展示页面要继承自
StatefulWidget
,因为需要动态更新数据和列表. 网络请求插件我用的[Dio](
),非常好用. 在initState
方法中请求数据表示刚加载页面的时候进行网络请求,请求数据方法如下
void getData()async{if (lastKey == '0'){dataList = [];//下拉刷新的时候将 DataList 制空}Dio dio = new Dio();Response response = await dio.get("lastKey.json");Reslut reslut = Reslut.fromJson(response.data);if(!reslut.response.hasMore){return;//如果没有数据就不继续了}if(reslut.response.columns != null) {columnList = reslut.response.columns;}lastKey = reslut.response.lastKey;//更新 lastkeysetState(() {if (reslut.response.banners != null){banners = reslut.response.banners;//给轮播图赋值}dataList.addAll(reslut.response.feeds);//给数据源赋值});}
因为用到了 setState()方法,所以在该方法中改变了的数据会对其相应的地方进行刷新,比如设置了 ListView 的 itemCount 个数为 dataList.length,如果在 SetState 方法中 dataList.length 改变了,那么 ListView 的 itemCount 树也会自动改变并刷新 ListView.
7. 上拉刷新与加载
Flutter 中有RefreshIndicator
用于下拉刷新,它有个onRefresh
闭包方法,表示下拉的时候执行的方法,一般用于网络请求.onRefresh 方法如下
Future<void> handleRefresh() {final Completer<void> completer = Completer<void>();Timer(const Duration(seconds: 1), () {completer.complete();});return completer.future.then<void>(() {lastKey = '0';getData();});}
下拉加载的话需要初始化一个ScrollController
,将它设为 ListView 的 controller,并对其进行监听,当滑动到最底部的时候进行网络请求.
@overridevoid initState() {url = widget.url;getData();_scrollController.addListener(() {///判断当前滑动位置是不是到达底部,触发加载更多回调 if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {getData();}});}final ScrollController _scrollController = new ScrollController();
上拉加载 loading 框用到了[flutter_spinkit](
)插件,提供了大量的加载样式.
![](https://static001.infoq.cn/static/write/img/img-copy-disabled.4f2g7h.png)
代码如下
///上拉加载更多 Widget _buildProgressIndicator() {///是否需要显示上拉加载更多的 loadingWidget bottomWidget = new
Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[///loading 框 new SpinKitThreeBounce(color: Color(0xFF24292E)),new Container(width: 5.0,),]);return new Padding(padding: const EdgeInsets.all(20.0),child: new Center(child: bottomWidget,),);}
8. ListView 赋值
由于最上面有一个轮播图,最下面有加载框,所以 ListView 的 itemCount 个数为 dataList.length+2,又因为每个 item 之间都有一个浅灰色的风格线,所以需要用到ListView.separated
,具体代码如下:
Widget build(BuildContext context) {return RefreshIndicator(onRefresh:(()=> _handleRefresh()),color: Colors.yellow,//刷新控件的颜色 child: ListView.separated(physics: const AlwaysScrollableScrollPhysics(),itemCount: _getListCount(),//item 个数 controller: _scrollController,//用于监听是否滑到最底部 itemBuilder: (context,index){if(index == 0){return SwiperWidget(context, banners);//如果是第一个,则展示 banner}else if(index < dataList.length + 1){return WidgetUtils.GetListWidget(context, dataList[index - 1]);//展示数据}else {return _buildProgressIndicator();//展示加载 loading 框}},separatorBuilder: (context,idx){//分割线 return Container(height: 5,color: Color.fromARGB(50,183, 187, 197),);},),);}
9. ListView 嵌套横向滑动 ListView
这种的话也稍微复杂一点,有两种样式.并且到滑到最右边的时候可以继续请求并加载数据.
![](https://static001.infoq.cn/static/write/img/img-copy-disabled.4f2g7h.png)
![](https://static001.infoq.cn/static/write/img/img-copy-disabled.4f2g7h.png)
首先来分析一下数据
![](https://static001.infoq.cn/static/write/img/img-copy-disabled.4f2g7h.png)
这个 colunmns 就是横向滑动列表的重要数据.
![](https://static001.infoq.cn/static/write/img/img-copy-disabled.4f2g7h.png)
里面的 id 是请求参数,show_type 表示列表的样式,location 表示插入的位置.而且通过抓取接口发现,当横向列表快要展示出来的时候,才会去请求横向列表的具体接口. 那么思路就很清晰了,在请求获得数据后遍历 colunmns,根据每个 colunmn 的 location 插入一个 Map,如下
data.insert(colunm.location, {'id':colunm.id,'showType':colunm.showType});
,再创建一个ColumnsListWidget
类,继承自 StatefulWidget,是一个新 item,在滑动到该列表的位置的时候,会将该 Map 数据传给ColumnsListWidget
,这个时候ColumnsListWidget
就会加载数据并展示出来了,滑到最右边的时候加载和滑到最底部加载的方法一样,就不多说了.具体可以查看源码,关键代码如下:
static Widget GetListWidget(BuildContext context, dynamic data) {Widget widget;if(data.runtimeType == Feed) {if (data.indexType != null) {widget = NewsListWidget(context, data);} else if (data.type == 2) {widget = ListImageTop(context, data);} else if (data.type == 0) {widget = ActivityWidget(context, data);} else if (data.type == 1) {widget = ListImageRight(context, data);}}else{widget = ColumnsListWidget(id: data['id'],showType: data['showType'],);}
1.横向 ListView 外需要用
Flexible
包裹,Flexible 组件可以使 Row、Column、Flex 等子组件在主轴方向有填充可用空间的能力(例如,Row 在水平方向,Column 在垂直方向),但是它与[Expanded](
)组件不同,它不强制子组件填充可用空间。 2.ListView 初始位置用到padding: new EdgeInsets.symmetric(horizontal: 12.0)
,用padding: EdgeInsets.only(left: 12)
的话会让 ListView 和最左边一直有条线
10.webview 加载复杂的 Html 字段
![](https://static001.infoq.cn/static/write/img/img-copy-disabled.4f2g7h.png)
获取到网页详情的数据发现是 Html 字段,并且其中的 css 是 url 地址,试了很多 Flutter 加载 Html 的插件发现样式都不正确,最后决定使用原生和 Flutter 混编,这时候发现[flutter_webview_plugin](
)这个插件是使用原生网页的,不过它只支持加载 url,于是就需要做一些修改.
iOS 在
FlutterWebviewPlugin.m
文件中的- (void)navigate:(FlutterMethodCall*)call
方法中的最后一排,将[self.webview loadRequest:request]
方法改为[self.webview loadHTMLString:url baseURL:nil]
Android 在
WebViewManager.java
文件中webView.loadUrl(url)
方法改为webView.loadData(url, "text/html", "UTF-8")
,以及下面那排的void reloadUrl(String url) { webView.loadUrl(url); }
改为void reloadUrl(String url) { webView.loadData(url, "text/html", "UTF-8"); }
由于服务器端返回的 Html 中的 css 和 js 文件地址是/assets/app3
开头的,所以需要替换成绝对路径,所以要用到这个方法htmlBody.replaceAll( '/assets/app3','http://app3.qdaily.com/assets/app3')
好了,这下就可以呈现出漂亮的网页了.
11.ListView 嵌套 GridView
在点击横向滑动列表的总标题的时候,会进入到相关栏目的详情页,如图
![](https://static001.infoq.cn/static/write/img/img-copy-disabled.4f2g7h.png)
这个 ListView 包含上下两部分.上面这部分为:
![](https://static001.infoq.cn/static/write/img/img-copy-disabled.4f2g7h.png)
结构如下
![](https://static001.infoq.cn/static/write/img/img-copy-disabled.4f2g7h.png)
下面就是一个 GridView,不过有时候下面会是 ListView,根据 showType 字段来判断,GridView 的代码如下:
评论