前言一:接下来一段时间我会陆续更新一些列Flutter文字教程vue
更新进度: 每周至少两篇;ios
更新地点: 首发于公众号,次日更新于掘金、思否、开发者头条等地方;算法
更多交流: 能够添加个人微信 372623326,关注个人微博:coderwhyjson
但愿你们能够 帮忙转发,点击在看,给我更多的创做动力。安全
前言二:列表是移动端常用的一种视图展现方式,在Flutter中提供了ListView和GridView。服务器
为了可能展现出更好的效果,我这里提供了一段Json数据,因此咱们能够先学习一下Json解析。微信
在开发中,咱们常常会使用本地JSON或者从服务器请求数据后回去到JSON,拿到JSON后一般会将JSON转成Model对象来进行后续的操做,由于这样操做更加的方便,也更加的安全。数据结构
因此学习JSON的相关操做以及读取JSON后如何转成Model对象对于Flutter开发也很是重要。app
JSON也属于一种资源,因此在使用以前须要先进行相关的配置less
咱们以前在学习使用Image组件时,用到了本地图片,本地图片必须在pubspec.yaml
中进行配置:
JSON资源读取
若是咱们但愿读取JSON资源,可使用package:flutter/services.dart
包中的rootBundle
。
在rootBundle
中有一个loadString
方法,能够去加载JSON资源
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"];
}
}
复制代码
上面咱们给出了解析的一个个步骤,下面咱们给出完整的代码逻辑
这里我单首创建了一个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 = new List();
for (Map<String, dynamic> map in jsonResult) {
anchors.add(Anchor.withMap(map));
}
return anchors;
}
复制代码
移动端数据量比较大时,咱们都是经过列表来进行展现的,好比商品数据、聊天列表、通讯录、朋友圈等。
在Android中,咱们可使用ListView或RecyclerView来实现,在iOS中,咱们能够经过UITableView来实现。
在Flutter中,咱们也有对应的列表Widget,就是ListView。
ListView能够沿一个方向(垂直或水平方向,默认是垂直方向)来排列其全部子Widget。
一种最简单的使用方式是直接将全部须要排列的子Widget放在ListView的children属性中便可。
咱们来看一下直接使用ListView的代码演练:
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),
)
],
);
}
}
复制代码
在开发中,咱们常常见到一种列表,有一个图标或图片(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),
)
],
);
}
}
复制代码
咱们能够经过设置 scrollDirection
参数来控制视图的滚动方向。
咱们经过下面的代码实现一个水平滚动的内容:
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),
],
);
}
}
复制代码
经过构造函数中的children传入全部的子Widget有一个问题:默认会建立出全部的子Widget。
可是对于用户来讲,一次性构建出全部的Widget并不会有什么差别,可是对于咱们的程序来讲会产生性能问题,并且会增长首屏的渲染时间。
咱们能够ListView.build来构建子Widget,提供性能。
ListView.build适用于子Widget比较多的场景,该构造函数将建立子Widget交给了一个抽象的方法,交给ListView进行管理,ListView会在真正须要的时候去建立子Widget,而不是一开始就所有初始化好。
该方法有两个重要参数:
咱们仍是经过一个简单的案例来认识它:
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"));
}
);
}
}
复制代码
在以前,咱们搞了一个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)
],
),
);
},
);
}
}
复制代码
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
);
}
}
复制代码
GridView用于展现多列的展现,在开发中也很是常见,好比直播App中的主播列表、电商中的商品列表等等。
在Flutter中咱们可使用GridView来实现,使用方式和ListView也比较类似。
咱们先学习GridView构造函数的使用方法
一种使用GridView的方式就是使用构造函数来建立,和ListView对比有一个特殊的参数:gridDelegate
gridDelegate
用于控制交叉轴的item数量或者宽度,须要传入的类型是SliverGridDelegate,可是它是一个抽象类,因此咱们须要传入它的子类:
SliverGridDelegateWithFixedCrossAxisCount
SliverGridDelegateWithFixedCrossAxisCount({
@required double crossAxisCount, // 交叉轴的item个数
double mainAxisSpacing = 0.0, // 主轴的间距
double crossAxisSpacing = 0.0, // 交叉轴的间距
double childAspectRatio = 1.0, // 子Widget的宽高比
})
复制代码
代码演练:
class MyGridCountDemo extends StatelessWidget {
List<Widget> getGridWidgets() {
return List.generate(100, (index) {
return Container(
color: Colors.purple,
alignment: Alignment(0, 0),
child: Text("item$index", style: TextStyle(fontSize: 20, color: Colors.white)),
);
});
}
@override
Widget build(BuildContext context) {
return GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 1.0
),
children: getGridWidgets(),
);
}
}
复制代码
SliverGridDelegateWithMaxCrossAxisExtent
SliverGridDelegateWithMaxCrossAxisExtent({
double maxCrossAxisExtent, // 交叉轴的item宽度
double mainAxisSpacing = 0.0, // 主轴的间距
double crossAxisSpacing = 0.0, // 交叉轴的间距
double childAspectRatio = 1.0, // 子Widget的宽高比
})
复制代码
代码演练:
class MyGridExtentDemo extends StatelessWidget {
List<Widget> getGridWidgets() {
return List.generate(100, (index) {
return Container(
color: Colors.purple,
alignment: Alignment(0, 0),
child: Text("item$index", style: TextStyle(fontSize: 20, color: Colors.white)),
);
});
}
@override
Widget build(BuildContext context) {
return GridView(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 150,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 1.0
),
children: getGridWidgets(),
);
}
}
复制代码
前面两种方式也能够不设置delegate
能够分别使用:GridView.count构造函数
和GridView.extent
构造函数实现相同的效果,这里再也不赘述。
和ListView同样,使用构造函数会一次性建立全部的子Widget,会带来性能问题,因此咱们可使用GridView.build
来交给GridView本身管理须要建立的子Widget。
咱们直接使用以前的数据来进行代码演练:
class _GridViewBuildDemoState extends State<GridViewBuildDemo> {
List<Anchor> anchors = [];
@override
void initState() {
getAnchors().then((anchors) {
setState(() {
this.anchors = anchors;
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: GridView.builder(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 1.2
),
itemCount: anchors.length,
itemBuilder: (BuildContext context, int index) {
return Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Image.network(anchors[index].imageUrl),
SizedBox(height: 5),
Text(anchors[index].nickname, style: TextStyle(fontSize: 16),),
Text(anchors[index].roomName, maxLines: 1, overflow: TextOverflow.ellipsis,)
],
),
);
}
),
);
}
}
复制代码
咱们考虑一个这样的布局:一个滑动的视图中包括一个标题视图(HeaderView),一个列表视图(ListView),一个网格视图(GridView)。
咱们怎么可让它们作到统一的滑动效果呢?使用前面的滚动是很难作到的。
Flutter中有一个能够完成这样滚动效果的Widget:CustomScrollView,能够统一管理多个滚动视图。
在CustomScrollView中,每个独立的,可滚动的Widget被称之为Sliver。
补充:Sliver能够翻译成裂片、薄片,你能够将每个独立的滚动视图当作一个小裂片。
由于咱们须要把不少的Sliver放在一个CustomScrollView中,因此CustomScrollView有一个slivers属性,里面让咱们放对应的一些Sliver:
咱们简单演示一下:SliverGrid+SliverPadding+SliverSafeArea的组合
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: <Widget>[
SliverSafeArea(
sliver: SliverPadding(
padding: EdgeInsets.all(8),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
alignment: Alignment(0, 0),
color: Colors.orange,
child: Text("item$index"),
);
},
childCount: 20
),
),
),
)
],
);
}
}
复制代码
这里我使用官方的示例程序,将SliverAppBar+SliverGrid+SliverFixedExtentList作出以下界面:
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return showCustomScrollView();
}
Widget showCustomScrollView() {
return new CustomScrollView(
slivers: <Widget>[
const SliverAppBar(
expandedHeight: 250.0,
flexibleSpace: FlexibleSpaceBar(
title: Text('Coderwhy Demo'),
background: Image(
image: NetworkImage(
"https://tva1.sinaimg.cn/large/006y8mN6gy1g72j6nk1d4j30u00k0n0j.jpg",
),
fit: BoxFit.cover,
),
),
),
new SliverGrid(
gridDelegate: new SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200.0,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 4.0,
),
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
return new Container(
alignment: Alignment.center,
color: Colors.teal[100 * (index % 9)],
child: new Text('grid item $index'),
);
},
childCount: 10,
),
),
SliverFixedExtentList(
itemExtent: 50.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return new Container(
alignment: Alignment.center,
color: Colors.lightBlue[100 * (index % 9)],
child: new Text('list item $index'),
);
},
childCount: 20
),
),
],
);
}
}
复制代码
对于滚动的视图,咱们常常须要监听它的一些滚动事件,在监听到的时候去作对应的一些事情。
好比视图滚动到底部时,咱们可能但愿作上拉加载更多;
好比滚动到必定位置时显示一个回到顶部的按钮,点击回到顶部的按钮,回到顶部;
好比监听滚动何时开始,何时结束;
在Flutter中监听滚动相关的内容由两部分组成:ScrollController和ScrollNotification。
在Flutter中,Widget并非最终渲染到屏幕上的元素(真正渲染的是RenderObject),所以一般这种监听事件以及相关的信息并不能直接从Widget中获取,而是必须经过对应的Widget的Controller来实现。
ListView、GridView的组件控制器是ScrollController,咱们能够经过它来获取视图的滚动信息,而且能够调用里面的方法来更新视图的滚动位置。
另外,一般状况下,咱们会根据滚动的位置来改变一些Widget的状态信息,因此ScrollController一般会和StatefulWidget一块儿来使用,而且会在其中控制它的初始化、监听、销毁等事件。
咱们来作一个案例,当滚动到1000位置的时候,显示一个回到顶部的按钮:
jumpTo(double offset)
、animateTo(double offset,...)
:这两个方法用于跳转到指定的位置,它们不一样之处在于,后者在跳转时会执行一个动画,而前者不会。class MyHomePage extends StatefulWidget {
@override
State<StatefulWidget> createState() => MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
ScrollController _controller;
bool _isShowTop = false;
@override
void initState() {
// 初始化ScrollController
_controller = ScrollController();
// 监听滚动
_controller.addListener(() {
var tempSsShowTop = _controller.offset >= 1000;
if (tempSsShowTop != _isShowTop) {
setState(() {
_isShowTop = tempSsShowTop;
});
}
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("ListView展现"),
),
body: ListView.builder(
itemCount: 100,
itemExtent: 60,
controller: _controller,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("item$index"));
}
),
floatingActionButton: !_isShowTop ? null : FloatingActionButton(
child: Icon(Icons.arrow_upward),
onPressed: () {
_controller.animateTo(0, duration: Duration(milliseconds: 1000), curve: Curves.ease);
},
),
);
}
}
复制代码
若是咱们但愿监听何时开始滚动,何时结束滚动,这个时候咱们能够经过NotificationListener
。
true
时,则冒泡终止,事件中止向上传播,若是不返回或者返回值为false
时,则冒泡继续。案例: 列表滚动, 而且在中间显示滚动进度
class MyHomeNotificationDemo extends StatefulWidget {
@override
State<StatefulWidget> createState() => MyHomeNotificationDemoState();
}
class MyHomeNotificationDemoState extends State<MyHomeNotificationDemo> {
int _progress = 0;
@override
Widget build(BuildContext context) {
return NotificationListener(
onNotification: (ScrollNotification notification) {
// 1.判断监听事件的类型
if (notification is ScrollStartNotification) {
print("开始滚动.....");
} else if (notification is ScrollUpdateNotification) {
// 当前滚动的位置和总长度
final currentPixel = notification.metrics.pixels;
final totalPixel = notification.metrics.maxScrollExtent;
double progress = currentPixel / totalPixel;
setState(() {
_progress = (progress * 100).toInt();
});
print("正在滚动:${notification.metrics.pixels} - ${notification.metrics.maxScrollExtent}");
} else if (notification is ScrollEndNotification) {
print("结束滚动....");
}
return false;
},
child: Stack(
alignment: Alignment(.9, .9),
children: <Widget>[
ListView.builder(
itemCount: 100,
itemExtent: 60,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("item$index"));
}
),
CircleAvatar(
radius: 30,
child: Text("$_progress%"),
backgroundColor: Colors.black54,
)
],
),
);
}
}
复制代码
备注:全部内容首发于公众号,以后除了Flutter也会更新其余技术文章,TypeScript、React、Node、uniapp、mpvue、数据结构与算法等等,也会更新一些本身的学习心得等,欢迎你们关注