一. JSON读取和解析
在开发中,我们经常会使用本地JSON或者从服务器请求数据后回去到JSON,拿到JSON后通常会将JSON转成Model对象来进行后续的操作,因为这样操作更加的方便,也更加的安全。
所以学习JSON的相关操作以及读取JSON后如何转成Model对象对于Flutter开发也非常重要。
1.1. JSON资源配置
JSON也属于一种资源,所以在使用之前需要先进行相关的配置
我们之前在学习使用Image组件时,用到了本地图片,本地图片必须在pubspec.yaml
中进行配置:
1.2. JSON读取解析
JSON资源读取
如果我们希望读取JSON资源,可以使用package:flutter/services.dart
包中的rootBundle
。
在rootBundle
中有一个loadString
方法,可以去加载JSON资源
- 但是注意,查看该方法的源码,你会发现这个操作是一个异步的。
- 关于Future和async,这里就不再展开讲解,可以去查看之前的dart语法。
Future<String> loadString(String key, { bool cache = true }) async { ...省略具体代码,可以自行查看源码 }
代码如下:(不要试图拷贝这个代码去运行,是没办法运行的)
import'package:flutter/services.dart' show rootBundle; // 打印读取的结果是一个字符串 rootBundle.loadString("assets/yz.json").then((value) => print(value));
JSON字符串转化
拿到JSON字符串后,我们需要将其转成成我们熟悉的List和Map类型。
我们可以通过dart:convert
包中的json.decode
方法将其进行转化
代码如下:
// 1.读取json文件 String jsonString = await rootBundle.loadString("assets/yz.json"); // 2.转成List或Map类型 final jsonResult = json.decode(jsonString);
对象Model定义
将JSON转成了List和Map类型后,就可以将List中的一个个Map转成Model对象,所以我们需要定义自己的Model
class Anchor { String nickname; String roomName; String imageUrl; Anchor({ this.nickname, this.roomName, this.imageUrl }); Anchor.withMap(Map<String, dynamic> parsedMap) { this.nickname = parsedMap["nickname"]; this.roomName = parsedMap["roomName"]; this.imageUrl = parsedMap["roomSrc"]; } }
1.3. JSON解析代码
上面我们给出了解析的一个个步骤,下面我们给出完整的代码逻辑
这里我单独创建了一个anchor.dart的文件,在其中定义了所有的相关代码:
- 之后外界只需要调用我内部的
getAnchors
就可以获取到解析后的数据了
import'package:flutter/services.dart' show rootBundle; import'dart:convert'; import'dart:async'; class Anchor { String nickname; String roomName; String imageUrl; Anchor({ this.nickname, this.roomName, this.imageUrl }); Anchor.withMap(Map<String, dynamic> parsedMap) { this.nickname = parsedMap["nickname"]; this.roomName = parsedMap["roomName"]; this.imageUrl = parsedMap["roomSrc"]; } } Future<List<Anchor>> getAnchors() async { // 1.读取json文件 String jsonString = await rootBundle.loadString("assets/yz.json"); // 2.转成List或Map类型 final jsonResult = json.decode(jsonString); // 3.遍历List,并且转成Anchor对象放到另一个List中 List<Anchor> anchors = newList(); for (Map<String, dynamic> map in jsonResult) { anchors.add(Anchor.withMap(map)); } return anchors; }
二. ListView组件
移动端数据量比较大时,我们都是通过列表来进行展示的,比如商品数据、聊天列表、通信录、朋友圈等。
在Android中,我们可以使用ListView或RecyclerView来实现,在iOS中,我们可以通过UITableView来实现。
在Flutter中,我们也有对应的列表Widget,就是ListView。
2.1. ListView基础
2.1.1 ListView基本使用
ListView可以沿一个方向(垂直或水平方向,默认是垂直方向)来排列其所有子Widget。
一种最简单的使用方式是直接将所有需要排列的子Widget放在ListView的children属性中即可。
我们来看一下直接使用ListView的代码演练:
- 为了让文字之间有一些间距,我使用了Padding Widget
class MyHomeBody extends StatelessWidget { final TextStyle textStyle = TextStyle(fontSize: 20, color: Colors.redAccent); @override Widget build(BuildContext context) { return ListView( children: <Widget>[ Padding( padding: const EdgeInsets.all(8.0), child: Text("人的一切痛苦,本质上都是对自己无能的愤怒。", style: textStyle), ), Padding( padding: const EdgeInsets.all(8.0), child: Text("人活在世界上,不可以有偏差;而且多少要费点劲儿,才能把自己保持到理性的轨道上。", style: textStyle), ), Padding( padding: const EdgeInsets.all(8.0), child: Text("我活在世上,无非想要明白些道理,遇见些有趣的事。", style: textStyle), ) ], ); } }
2.2.2. ListTile的使用
在开发中,我们经常见到一种列表,有一个图标或图片(Icon),有一个标题(Title),有一个子标题(Subtitle),还有尾部一个图标(Icon)。
这个时候,我们可以使用ListTile来实现:
class MyHomeBody extends StatelessWidget { @override Widget build(BuildContext context) { return ListView( children: <Widget>[ ListTile( leading: Icon(Icons.people, size: 36,), title: Text("联系人"), subtitle: Text("联系人信息"), trailing: Icon(Icons.arrow_forward_ios), ), ListTile( leading: Icon(Icons.email, size: 36,), title: Text("邮箱"), subtitle: Text("邮箱地址信息"), trailing: Icon(Icons.arrow_forward_ios), ), ListTile( leading: Icon(Icons.message, size: 36,), title: Text("消息"), subtitle: Text("消息详情信息"), trailing: Icon(Icons.arrow_forward_ios), ), ListTile( leading: Icon(Icons.map, size: 36,), title: Text("地址"), subtitle: Text("地址详情信息"), trailing: Icon(Icons.arrow_forward_ios), ) ], ); } }
2.2.3. 垂直方向滚动
我们可以通过设置 scrollDirection
参数来控制视图的滚动方向。
我们通过下面的代码实现一个水平滚动的内容:
- 这里需要注意,我们需要给Container设置width,否则它是没有宽度的,就不能正常显示。
- 或者我们也可以给ListView设置一个itemExtent,该属性会设置滚动方向上每个item所占据的宽度。
class MyHomeBody extends StatelessWidget { @override Widget build(BuildContext context) { return ListView( scrollDirection: Axis.horizontal, itemExtent: 200, children: <Widget>[ Container(color: Colors.red, width: 200), Container(color: Colors.green, width: 200), Container(color: Colors.blue, width: 200), Container(color: Colors.purple, width: 200), Container(color: Colors.orange, width: 200), ], ); } }
2.2. ListView.build
通过构造函数中的children传入所有的子Widget有一个问题:默认会创建出所有的子Widget。
但是对于用户来说,一次性构建出所有的Widget并不会有什么差异,但是对于我们的程序来说会产生性能问题,而且会增加首屏的渲染时间。
我们可以ListView.build来构建子Widget,提供性能。
2.2.1. ListView.build基本使用
ListView.build适用于子Widget比较多的场景,该构造函数将创建子Widget交给了一个抽象的方法,交给ListView进行管理,ListView会在真正需要的时候去创建子Widget,而不是一开始就全部初始化好。
该方法有两个重要参数:
- itemBuilder:列表项创建的方法。当列表滚动到对应位置的时候,ListView会自动调用该方法来创建对应的子Widget。类型是IndexedWidgetBuilder,是一个函数类型。
- itemCount:表示列表项的数量,如果为空,则表示ListView为无限列表。
我们还是通过一个简单的案例来认识它:
class MyHomeBody extends StatelessWidget { @override Widget build(BuildContext context) { return ListView.builder( itemCount: 100, itemExtent: 80, itemBuilder: (BuildContext context, int index) { return ListTile(title: Text("标题$index"), subtitle: Text("详情内容$index")); } ); } }
2.2.2. ListView.build动态数据
在之前,我们搞了一个yz.json数据,我们现在动态的来通过JSON数据展示一个列表。
思考:这个时候是否依然可以使用StatelessWidget
:
答案:不可以,因为当前我们的数据是异步加载的,刚开始界面并不会展示数据(没有数据),后面从JSON中加载出来数据(有数据)后,再次展示加载的数据。
- 这里是有状态的变化的,从无数据,到有数据的变化。
- 这个时候,我们需要使用
StatefulWidget
来管理组件。
展示代码如下:
import'model/anchor.dart'; ...省略中间代码 class MyHomeBody extends StatefulWidget { @override State<StatefulWidget> createState() { return MyHomeBodyState(); } } class MyHomeBodyState extends State<MyHomeBody> { List<Anchor> anchors = []; // 在初始化状态的方法中加载数据 @override void initState() { getAnchors().then((anchors) { setState(() { this.anchors = anchors; }); }); super.initState(); } @override Widget build(BuildContext context) { return ListView.builder( itemBuilder: (BuildContext context, int index) { return Padding( padding: EdgeInsets.all(8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Image.network( anchors[index].imageUrl, fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width, ), SizedBox(height: 8), Text(anchors[index].nickname, style: TextStyle(fontSize: 20),), SizedBox(height: 5), Text(anchors[index].roomName) ], ), ); }, ); } }
2.2.3. ListView.separated
ListView.separated
可以生成列表项之间的分割器,它除了比ListView.builder
多了一个separatorBuilder
参数,该参数是一个分割器生成器。
下面我们看一个例子:奇数行添加一条蓝色下划线,偶数行添加一条红色下划线:
class MySeparatedDemo extends StatelessWidget { Divider blueColor = Divider(color: Colors.blue); Divider redColor = Divider(color: Colors.red); @override Widget build(BuildContext context) { return ListView.separated( itemBuilder: (BuildContext context, int index) { return ListTile( leading: Icon(Icons.people), title: Text("联系人${index+1}"), subtitle: Text("联系人电话${index+1}"), ); }, separatorBuilder: (BuildContext context, int index) { return index % 2 == 0 ? redColor : blueColor; }, itemCount: 100 ); } }