一. JSON读取和解析

开发中,经常会从服务器请求数据,拿到JSON数据后通常会将JSON转成Model对象来进行后续的操作,这样操作更加的方便,也更加的安全。

1.1. JSON资源配置

JSON也属于一种资源,在使用之前需要先进行相关的配置
使用Image组件访问本地图片,本地图片必须在pubspec.yaml中进行配置

1.2. JSON读取解析

JSON资源读取
如果希望读取JSON资源,可以使用package:flutter/services.dart包中的rootBundle。
在rootBundle中有一个loadString方法,可以去加载JSON资源

  • 注意,这个操作是一个异步的;
    1. Future<String> loadString(String key, { bool cache = true }) async {
    2. ...省略具体代码,可以自行查看源码
    3. }
    伪代码:
    1. import 'package:flutter/services.dart' show rootBundle;
    2. // 打印读取的结果是一个字符串
    3. rootBundle.loadString("assets/yz.json").then((value) => print(value));
    JSON字符串转化
    拿到JSON字符串后,需要将其转成成我们熟悉的List和Map类型,可以通过dart:convert包中的json.decode方法将其进行转化:
    1. // 1.读取json文件
    2. String jsonString = await rootBundle.loadString("assets/yz.json");
    3. // 2.转成List或Map类型
    4. final jsonResult = json.decode(jsonString);
    对象Model定义
    将JSON转成了List和Map类型后,就可以将List中的一个个Map转成Model对象,定义自己的Model
    1. class Anchor {
    2. String nickname;
    3. String roomName;
    4. String imageUrl;
    5. Anchor({
    6. this.nickname,
    7. this.roomName,
    8. this.imageUrl
    9. });
    10. Anchor.withMap(Map<String, dynamic> parsedMap) {
    11. this.nickname = parsedMap["nickname"];
    12. this.roomName = parsedMap["roomName"];
    13. this.imageUrl = parsedMap["roomSrc"];
    14. }
    15. }

    1.3. JSON解析代码

    1. import 'package:flutter/services.dart' show rootBundle;
    2. import 'dart:convert';
    3. import 'dart:async';
    4. class Anchor {
    5. String nickname;
    6. String roomName;
    7. String imageUrl;
    8. Anchor({
    9. this.nickname,
    10. this.roomName,
    11. this.imageUrl
    12. });
    13. Anchor.withMap(Map<String, dynamic> parsedMap) {
    14. this.nickname = parsedMap["nickname"];
    15. this.roomName = parsedMap["roomName"];
    16. this.imageUrl = parsedMap["roomSrc"];
    17. }
    18. }
    19. Future<List<Anchor>> getAnchors() async {
    20. // 1.读取json文件
    21. String jsonString = await rootBundle.loadString("assets/yz.json");
    22. // 2.转成List或Map类型
    23. final jsonResult = json.decode(jsonString);
    24. // 3.遍历List,并且转成Anchor对象放到另一个List中
    25. List<Anchor> anchors = new List();
    26. for (Map<String, dynamic> map in jsonResult) {
    27. anchors.add(Anchor.withMap(map));
    28. }
    29. return anchors;
    30. }

    二. ListView组件

    移动端数据量比较大时,都是通过列表来进行展示的,比如商品数据、聊天列表、通信录、朋友圈等; Flutter中,也有对应的列表Widget,就是ListView;

2.1. ListView基础

2.1.1 ListView基本使用

ListView可以沿一个方向(垂直或水平方向,默认是垂直方向)来排列其所有子Widget。
一种最简单的使用方式是直接将所有需要排列的子Widget放在ListView的children属性中即可。

  • 为了让文字之间有一些间距,我使用了Padding Widget

    1. class MyHomeBody extends StatelessWidget {
    2. final TextStyle textStyle = TextStyle(fontSize: 20, color: Colors.redAccent);
    3. @override
    4. Widget build(BuildContext context) {
    5. return ListView(
    6. children: <Widget>[
    7. Padding(
    8. padding: const EdgeInsets.all(8.0),
    9. child: Text("人的一切痛苦,本质上都是对自己无能的愤怒。", style: textStyle),
    10. ),
    11. Padding(
    12. padding: const EdgeInsets.all(8.0),
    13. child: Text("人活在世界上,不可以有偏差;而且多少要费点劲儿,才能把自己保持到理性的轨道上。", style: textStyle),
    14. ),
    15. Padding(
    16. padding: const EdgeInsets.all(8.0),
    17. child: Text("我活在世上,无非想要明白些道理,遇见些有趣的事。", style: textStyle),
    18. )
    19. ],
    20. );
    21. }
    22. }

    2.2.2. ListTile的使用

    在开发中,经常见到一种列表,有一个图标或图片(Icon),有一个标题(Title),有一个子标题(Subtitle),还有尾部一个图标(Icon)。
    可以使用ListTile来实现:

    1. class MyHomeBody extends StatelessWidget {
    2. @override
    3. Widget build(BuildContext context) {
    4. return ListView(
    5. children: <Widget>[
    6. ListTile(
    7. leading: Icon(Icons.people, size: 36,),
    8. title: Text("联系人"),
    9. subtitle: Text("联系人信息"),
    10. trailing: Icon(Icons.arrow_forward_ios),
    11. ),
    12. ListTile(
    13. leading: Icon(Icons.email, size: 36,),
    14. title: Text("邮箱"),
    15. subtitle: Text("邮箱地址信息"),
    16. trailing: Icon(Icons.arrow_forward_ios),
    17. ),
    18. ListTile(
    19. leading: Icon(Icons.message, size: 36,),
    20. title: Text("消息"),
    21. subtitle: Text("消息详情信息"),
    22. trailing: Icon(Icons.arrow_forward_ios),
    23. ),
    24. ListTile(
    25. leading: Icon(Icons.map, size: 36,),
    26. title: Text("地址"),
    27. subtitle: Text("地址详情信息"),
    28. trailing: Icon(Icons.arrow_forward_ios),
    29. )
    30. ],
    31. );
    32. }
    33. }

    2.2.3. 垂直方向滚动

    可以设置 scrollDirection 参数来控制视图的滚动方向。

  • 注意,需要给Container设置width,否则它是没有宽度的,就不能正常显示

  • 或者也可以给ListView设置一个itemExtent,该属性会设置滚动方向上每个item所占据的宽度
    1. class MyHomeBody extends StatelessWidget {
    2. @override
    3. Widget build(BuildContext context) {
    4. return ListView(
    5. scrollDirection: Axis.horizontal,
    6. itemExtent: 200,
    7. children: <Widget>[
    8. Container(color: Colors.red, width: 200),
    9. Container(color: Colors.green, width: 200),
    10. Container(color: Colors.blue, width: 200),
    11. Container(color: Colors.purple, width: 200),
    12. Container(color: Colors.orange, width: 200),
    13. ],
    14. );
    15. }
    16. }

    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为无限列表。

    1. class MyHomeBody extends StatelessWidget {
    2. @override
    3. Widget build(BuildContext context) {
    4. return ListView.builder(
    5. itemCount: 100,
    6. itemExtent: 80,
    7. itemBuilder: (BuildContext context, int index) {
    8. return ListTile(title: Text("标题$index"), subtitle: Text("详情内容$index"));
    9. }
    10. );
    11. }
    12. }

    2.2.2. ListView.build动态数据

    假设一个json数据,现在动态的来通过JSON数据展示一个列表。
    思考:这个时候是否依然可以使用StatelessWidget?
    不可以,因为当前的数据是异步加载的,刚开始界面并不会展示数据(没有数据),后面从JSON中加载出来数据(有数据)后,再次展示加载的数据。

  • 这里是有状态的变化的,从无数据,到有数据的变化

  • 这个时候,需要使用StatefulWidget来管理组件
    1. import 'model/anchor.dart';
    2. ...省略中间代码
    3. class MyHomeBody extends StatefulWidget {
    4. @override
    5. State<StatefulWidget> createState() {
    6. return MyHomeBodyState();
    7. }
    8. }
    9. class MyHomeBodyState extends State<MyHomeBody> {
    10. List<Anchor> anchors = [];
    11. // 在初始化状态的方法中加载数据
    12. @override
    13. void initState() {
    14. getAnchors().then((anchors) {
    15. setState(() {
    16. this.anchors = anchors;
    17. });
    18. });
    19. super.initState();
    20. }
    21. @override
    22. Widget build(BuildContext context) {
    23. return ListView.builder(
    24. itemBuilder: (BuildContext context, int index) {
    25. return Padding(
    26. padding: EdgeInsets.all(8),
    27. child: Column(
    28. crossAxisAlignment: CrossAxisAlignment.start,
    29. children: <Widget>[
    30. Image.network(
    31. anchors[index].imageUrl,
    32. fit: BoxFit.fitWidth,
    33. width: MediaQuery.of(context).size.width,
    34. ),
    35. SizedBox(height: 8),
    36. Text(anchors[index].nickname, style: TextStyle(fontSize: 20),),
    37. SizedBox(height: 5),
    38. Text(anchors[index].roomName)
    39. ],
    40. ),
    41. );
    42. },
    43. );
    44. }
    45. }

    2.2.3. ListView.separated

    ListView.separated可以生成列表项之间的分割器,它除了比ListView.builder多了一个separatorBuilder参数,该参数是一个分割器生成器。
    奇数行添加一条蓝色下划线,偶数行添加一条红色下划线:
    1. class MySeparatedDemo extends StatelessWidget {
    2. Divider blueColor = Divider(color: Colors.blue);
    3. Divider redColor = Divider(color: Colors.red);
    4. @override
    5. Widget build(BuildContext context) {
    6. return ListView.separated(
    7. itemBuilder: (BuildContext context, int index) {
    8. return ListTile(
    9. leading: Icon(Icons.people),
    10. title: Text("联系人${index+1}"),
    11. subtitle: Text("联系人电话${index+1}"),
    12. );
    13. },
    14. separatorBuilder: (BuildContext context, int index) {
    15. return index % 2 == 0 ? redColor : blueColor;
    16. },
    17. itemCount: 100
    18. );
    19. }
    20. }

    三. GridView组件

    GridView用于展示多列的展示,在开发中也非常常见,比如直播App中的主播列表、电商中的商品列表等等。 在Flutter中我们可以使用GridView来实现,使用方式和ListView也比较相似。

3.1. GridView构造函数

一种使用GridView的方式就是使用构造函数来创建,和ListView对比有一个特殊的参数:gridDelegate
gridDelegate用于控制交叉轴的item数量或者宽度,需要传入的类型是SliverGridDelegate,但是它是一个抽象类,所以我们需要传入它的子类:
SliverGridDelegateWithFixedCrossAxisCount

  1. SliverGridDelegateWithFixedCrossAxisCount({
  2. @required double crossAxisCount, // 交叉轴的item个数
  3. double mainAxisSpacing = 0.0, // 主轴的间距
  4. double crossAxisSpacing = 0.0, // 交叉轴的间距
  5. double childAspectRatio = 1.0, // 子Widget的宽高比
  6. })

代码演练:

  1. class MyGridCountDemo extends StatelessWidget {
  2. List<Widget> getGridWidgets() {
  3. return List.generate(100, (index) {
  4. return Container(
  5. color: Colors.purple,
  6. alignment: Alignment(0, 0),
  7. child: Text("item$index", style: TextStyle(fontSize: 20, color: Colors.white)),
  8. );
  9. });
  10. }
  11. @override
  12. Widget build(BuildContext context) {
  13. return GridView(
  14. gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
  15. crossAxisCount: 3,
  16. mainAxisSpacing: 10,
  17. crossAxisSpacing: 10,
  18. childAspectRatio: 1.0
  19. ),
  20. children: getGridWidgets(),
  21. );
  22. }
  23. }

SliverGridDelegateWithMaxCrossAxisExtent

  1. SliverGridDelegateWithMaxCrossAxisExtent({
  2. double maxCrossAxisExtent, // 交叉轴的item宽度
  3. double mainAxisSpacing = 0.0, // 主轴的间距
  4. double crossAxisSpacing = 0.0, // 交叉轴的间距
  5. double childAspectRatio = 1.0, // 子Widget的宽高比
  6. })

代码演练:

  1. class MyGridExtentDemo extends StatelessWidget {
  2. List<Widget> getGridWidgets() {
  3. return List.generate(100, (index) {
  4. return Container(
  5. color: Colors.purple,
  6. alignment: Alignment(0, 0),
  7. child: Text("item$index", style: TextStyle(fontSize: 20, color: Colors.white)),
  8. );
  9. });
  10. }
  11. @override
  12. Widget build(BuildContext context) {
  13. return GridView(
  14. gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
  15. maxCrossAxisExtent: 150,
  16. mainAxisSpacing: 10,
  17. crossAxisSpacing: 10,
  18. childAspectRatio: 1.0
  19. ),
  20. children: getGridWidgets(),
  21. );
  22. }
  23. }

前面两种方式也可以不设置delegate
可以分别使用:GridView.count构造函数和GridView.extent构造函数实现相同的效果,这里不再赘述。

3.2. GridView.build

和ListView一样,使用构造函数会一次性创建所有的子Widget,会带来性能问题,所以我们可以使用GridView.build来交给GridView自己管理需要创建的子Widget。
我们直接使用之前的数据来进行代码演练:

  1. class _GridViewBuildDemoState extends State<GridViewBuildDemo> {
  2. List<Anchor> anchors = [];
  3. @override
  4. void initState() {
  5. getAnchors().then((anchors) {
  6. setState(() {
  7. this.anchors = anchors;
  8. });
  9. });
  10. super.initState();
  11. }
  12. @override
  13. Widget build(BuildContext context) {
  14. return Padding(
  15. padding: const EdgeInsets.all(8.0),
  16. child: GridView.builder(
  17. shrinkWrap: true,
  18. physics: ClampingScrollPhysics(),
  19. gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
  20. crossAxisCount: 2,
  21. mainAxisSpacing: 10,
  22. crossAxisSpacing: 10,
  23. childAspectRatio: 1.2
  24. ),
  25. itemCount: anchors.length,
  26. itemBuilder: (BuildContext context, int index) {
  27. return Container(
  28. child: Column(
  29. crossAxisAlignment: CrossAxisAlignment.start,
  30. children: <Widget>[
  31. Image.network(anchors[index].imageUrl),
  32. SizedBox(height: 5),
  33. Text(anchors[index].nickname, style: TextStyle(fontSize: 16),),
  34. Text(anchors[index].roomName, maxLines: 1, overflow: TextOverflow.ellipsis,)
  35. ],
  36. ),
  37. );
  38. }
  39. ),
  40. );
  41. }
  42. }

四. Slivers

有这样的一个布局:一个滑动的视图中包括一个标题视图(HeaderView),一个列表视图(ListView),一个网格视图(GridView)。 怎么可以让它们做到统一的滑动效果呢?使用前面的滚动是很难做到的。 Flutter中有一个可以完成这样滚动效果的Widget:CustomScrollView,可以统一管理多个滚动视图。 在CustomScrollView中,每一个独立的,可滚动的Widget被称之为Sliver。 补充:Sliver可以翻译成裂片、薄片,你可以将每一个独立的滚动视图当做一个小裂片。

4.1. Slivers的基本使用

因为需要把很多的Sliver放在一个CustomScrollView中,所以CustomScrollView有一个slivers属性,里面让我们放对应的一些Sliver:

  • SliverList:类似于我们之前使用过的ListView;
  • SliverFixedExtentList:类似于SliverList只是可以设置滚动的高度;
  • SliverGrid:类似于我们之前使用过的GridView;
  • SliverPadding:设置Sliver的内边距,因为可能要单独给Sliver设置内边距;
  • SliverAppBar:添加一个AppBar,通常用来作为CustomScrollView的HeaderView;
  • SliverSafeArea:设置内容显示在安全区域(比如不让齐刘海挡住我们的内容)

SliverGrid+SliverPadding+SliverSafeArea的组合:

  1. class HomeContent extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. return CustomScrollView(
  5. slivers: <Widget>[
  6. SliverSafeArea(
  7. sliver: SliverPadding(
  8. padding: EdgeInsets.all(8),
  9. sliver: SliverGrid(
  10. gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
  11. crossAxisCount: 2,
  12. crossAxisSpacing: 8,
  13. mainAxisSpacing: 8,
  14. ),
  15. delegate: SliverChildBuilderDelegate(
  16. (BuildContext context, int index) {
  17. return Container(
  18. alignment: Alignment(0, 0),
  19. color: Colors.orange,
  20. child: Text("item$index"),
  21. );
  22. },
  23. childCount: 20
  24. ),
  25. ),
  26. ),
  27. )
  28. ],
  29. );
  30. }
  31. }

4.2. Slivers的组合使用

将SliverAppBar+SliverGrid+SliverFixedExtentList做出如下界面:

  1. class HomeContent extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. return showCustomScrollView();
  5. }
  6. Widget showCustomScrollView() {
  7. return new CustomScrollView(
  8. slivers: <Widget>[
  9. const SliverAppBar(
  10. expandedHeight: 250.0,
  11. flexibleSpace: FlexibleSpaceBar(
  12. title: Text('Coderwhy Demo'),
  13. background: Image(
  14. image: NetworkImage(
  15. "https://tva1.sinaimg.cn/large/006y8mN6gy1g72j6nk1d4j30u00k0n0j.jpg",
  16. ),
  17. fit: BoxFit.cover,
  18. ),
  19. ),
  20. ),
  21. new SliverGrid(
  22. gridDelegate: new SliverGridDelegateWithMaxCrossAxisExtent(
  23. maxCrossAxisExtent: 200.0,
  24. mainAxisSpacing: 10.0,
  25. crossAxisSpacing: 10.0,
  26. childAspectRatio: 4.0,
  27. ),
  28. delegate: new SliverChildBuilderDelegate(
  29. (BuildContext context, int index) {
  30. return new Container(
  31. alignment: Alignment.center,
  32. color: Colors.teal[100 * (index % 9)],
  33. child: new Text('grid item $index'),
  34. );
  35. },
  36. childCount: 10,
  37. ),
  38. ),
  39. SliverFixedExtentList(
  40. itemExtent: 50.0,
  41. delegate: SliverChildBuilderDelegate(
  42. (BuildContext context, int index) {
  43. return new Container(
  44. alignment: Alignment.center,
  45. color: Colors.lightBlue[100 * (index % 9)],
  46. child: new Text('list item $index'),
  47. );
  48. },
  49. childCount: 20
  50. ),
  51. ),
  52. ],
  53. );
  54. }
  55. }

五. 监听滚动事件

对于滚动的视图,经常需要监听它的一些滚动事件,在监听到的时候去做对应的一些事情。 比如视图滚动到底部时,可能希望做上拉加载更多; 比如滚动到一定位置时显示一个回到顶部的按钮,点击回到顶部的按钮,回到顶部; 比如监听滚动什么时候开始,什么时候结束; 在Flutter中监听滚动相关的内容由两部分组成:ScrollController和ScrollNotification。

5.1. ScrollController

在Flutter中,Widget并不是最终渲染到屏幕上的元素(真正渲染的是RenderObject),因此通常这种监听事件以及相关的信息并不能直接从Widget中获取,而是必须通过对应的Widget的Controller来实现

ListView、GridView的组件控制器是ScrollController,我们可以通过它来获取视图的滚动信息,并且可以调用里面的方法来更新视图的滚动位置。
另外,通常情况下,我们会根据滚动的位置来改变一些Widget的状态信息,所以ScrollController通常会和StatefulWidget一起来使用,并且会在其中控制它的初始化、监听、销毁等事件。
我们来做一个案例,当滚动到1000位置的时候,显示一个回到顶部的按钮:

  • jumpTo(double offset)、animateTo(double offset,...):这两个方法用于跳转到指定的位置,它们不同之处在于,后者在跳转时会执行一个动画,而前者不会。
  • ScrollController间接继承自Listenable,我们可以根据ScrollController来监听滚动事件。

    1. class MyHomePage extends StatefulWidget {
    2. @override
    3. State<StatefulWidget> createState() => MyHomePageState();
    4. }
    5. class MyHomePageState extends State<MyHomePage> {
    6. ScrollController _controller;
    7. bool _isShowTop = false;
    8. @override
    9. void initState() {
    10. // 初始化ScrollController
    11. _controller = ScrollController();
    12. // 监听滚动
    13. _controller.addListener(() {
    14. var tempSsShowTop = _controller.offset >= 1000;
    15. if (tempSsShowTop != _isShowTop) {
    16. setState(() {
    17. _isShowTop = tempSsShowTop;
    18. });
    19. }
    20. });
    21. super.initState();
    22. }
    23. @override
    24. Widget build(BuildContext context) {
    25. return Scaffold(
    26. appBar: AppBar(
    27. title: Text("ListView展示"),
    28. ),
    29. body: ListView.builder(
    30. itemCount: 100,
    31. itemExtent: 60,
    32. controller: _controller,
    33. itemBuilder: (BuildContext context, int index) {
    34. return ListTile(title: Text("item$index"));
    35. }
    36. ),
    37. floatingActionButton: !_isShowTop ? null : FloatingActionButton(
    38. child: Icon(Icons.arrow_upward),
    39. onPressed: () {
    40. _controller.animateTo(0, duration: Duration(milliseconds: 1000), curve: Curves.ease);
    41. },
    42. ),
    43. );
    44. }
    45. }

    5.2. NotificationListener

    如果希望监听什么时候开始滚动,什么时候结束滚动,这个时候我们可以通过NotificationListener。

  • NotificationListener是一个Widget,模板参数T是想监听的通知类型,如果省略,则所有类型通知都会被监听,如果指定特定类型,则只有该类型的通知会被监听。

  • NotificationListener需要一个onNotification回调函数,用于实现监听处理逻辑。
  • 该回调可以返回一个布尔值,代表是否阻止该事件继续向上冒泡,如果为true时,则冒泡终止,事件停止向上传播,如果不返回或者返回值为false 时,则冒泡继续。

案例: 列表滚动, 并且在中间显示滚动进度

  1. class MyHomeNotificationDemo extends StatefulWidget {
  2. @override
  3. State<StatefulWidget> createState() => MyHomeNotificationDemoState();
  4. }
  5. class MyHomeNotificationDemoState extends State<MyHomeNotificationDemo> {
  6. int _progress = 0;
  7. @override
  8. Widget build(BuildContext context) {
  9. return NotificationListener(
  10. onNotification: (ScrollNotification notification) {
  11. // 1.判断监听事件的类型
  12. if (notification is ScrollStartNotification) {
  13. print("开始滚动.....");
  14. } else if (notification is ScrollUpdateNotification) {
  15. // 当前滚动的位置和总长度
  16. final currentPixel = notification.metrics.pixels;
  17. final totalPixel = notification.metrics.maxScrollExtent;
  18. double progress = currentPixel / totalPixel;
  19. setState(() {
  20. _progress = (progress * 100).toInt();
  21. });
  22. print("正在滚动:${notification.metrics.pixels} - ${notification.metrics.maxScrollExtent}");
  23. } else if (notification is ScrollEndNotification) {
  24. print("结束滚动....");
  25. }
  26. return false;
  27. },
  28. child: Stack(
  29. alignment: Alignment(.9, .9),
  30. children: <Widget>[
  31. ListView.builder(
  32. itemCount: 100,
  33. itemExtent: 60,
  34. itemBuilder: (BuildContext context, int index) {
  35. return ListTile(title: Text("item$index"));
  36. }
  37. ),
  38. CircleAvatar(
  39. radius: 30,
  40. child: Text("$_progress%"),
  41. backgroundColor: Colors.black54,
  42. )
  43. ],
  44. ),
  45. );
  46. }
  47. }