Flutter开发实战初级(一)ListView详解
Flutter开发实战初级(一)ListView详解
- 本篇博客主要以一个demo的形式讲解ListView的使用
源码下载点击这里:flutter_listview_demo
先来看一下效果图,下面是运行在iphone11上面的:
ListView 知识点
在Flutter中,用ListView来显示列表项,支持垂直和水平方向展示,通过一个属性我们就可以控制其方向
1.水平的列表
2.垂直的列表
3.数据量非常大的列表
4.内置的ListTile(挺好用的)
ListView Demo
- demo 下载地址:flutter_listviewdemo
-
运行效果:
1. 新建car.dart 保存模型信息
- 定义一个Car
class Car {
const Car({
this.name,
this.imageUrl,
});
final String name;
final String imageUrl;
}
- 定义一个数组保存Car对象
//模型数组
final List datas = [
Car(
name: '保时捷918 Spyder',
imageUrl:
'https://upload-images.jianshu.io/upload_images/2990730-7d8be6ebc4c7c95b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
),
Car(
name: '兰博基尼Aventador',
imageUrl:
'https://upload-images.jianshu.io/upload_images/2990730-e3bfd824f30afaac?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
),
Car(
name: '法拉利Enzo',
imageUrl:
'https://upload-images.jianshu.io/upload_images/2990730-a1d64cf5da2d9d99?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
),
Car(
name: 'Zenvo ST1',
imageUrl:
'https://upload-images.jianshu.io/upload_images/2990730-bf883b46690f93ce?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
),
Car(
name: '迈凯伦F1',
imageUrl:
'https://upload-images.jianshu.io/upload_images/2990730-5a7b5550a19b8342?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
),
Car(
name: '萨林S7',
imageUrl:
'https://upload-images.jianshu.io/upload_images/2990730-2e128d18144ad5b8?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
),
Car(
name: '科尼赛克CCR',
imageUrl:
'https://upload-images.jianshu.io/upload_images/2990730-01ced8f6f95219ec?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
),
Car(
name: '布加迪Chiron',
imageUrl:
'https://upload-images.jianshu.io/upload_images/2990730-7fc8359eb61adac0?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
),
Car(
name: '轩尼诗Venom GT',
imageUrl:
'https://upload-images.jianshu.io/upload_images/2990730-d332bf510d61bbc2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
),
Car(
name: '西贝尔Tuatara',
imageUrl:
'https://upload-images.jianshu.io/upload_images/2990730-3dd9a70b25ae6bc9?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
)
];
2. 新建carlistview.dart 用来展示列表数据
- 定义Listview 展示数据
@override
Widget build(BuildContext context) {
// TODO: implement build
return ListView.builder(
//控制方向 默认是垂直的
// scrollDirection: Axis.horizontal, //控制水平方向显示
/* children: [
_getContainer('Maps', Icons.map),
_getContainer('phone', Icons.phone),
_getContainer('Maps', Icons.map),
], */
itemCount: datas.length, //告诉ListView总共有多少个cell
itemBuilder: _cellForRow //使用_cellForRow回调返回每个cell
);
}
- 定义一个回调函数,返回每个cell
Widget _cellForRow(BuildContext context, int index) {
return Container(
color: Colors.white,
margin: EdgeInsets.all(10),
child: Column(
children: [
Image.network(
datas[index].imageUrl
),
SizedBox(
height: 10,
),
Text(
datas[index].name,
style: TextStyle(
fontWeight: FontWeight.w800,
fontSize: 18.0,
fontStyle: FontStyle.values[1]
),
),
Container(height: 20,),
],
), //每人一辆跑车
);
}
3. main.dart 调用ListView
import 'package:flutter/material.dart';
import 'model/carlistview.dart';
//如果只有一行代码,可以是 => 代替 {}
void main() => runApp(KYLApp());
class KYLApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Home(),
theme: ThemeData(
primaryColor: Colors.yellow
),
);
}
}
class Home extends StatelessWidget{
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
backgroundColor: Colors.grey[100],
appBar: AppBar(
title: Text('kongyulu first app'),
),
body: ListViewDemo(),
);
}
}
4. 知识点讲解
4.1 Widget
4.1.1 Widget基本概念
4.1.2 Widget之间的交互
4.1.3 Widget点击事件,手势
我们处理手势可以使用GestureDetector组件,它是可以添加手势的一个widget,观察它的源码:
class GestureDetector extends StatelessWidget {
GestureDetector({
Key key,
this.child,
this.onTapDown,
this.onTapUp,
this.onTap,
this.onTapCancel,
this.onDoubleTap,
this.onLongPress,
this.onLongPressUp,
this.onVerticalDragDown,
this.onVerticalDragStart,
this.onVerticalDragUpdate,
this.onVerticalDragEnd,
this.onVerticalDragCancel,
this.onHorizontalDragDown,
this.onHorizontalDragStart,
this.onHorizontalDragUpdate,
this.onHorizontalDragEnd,
this.onHorizontalDragCancel,
this.onPanDown,
this.onPanStart,
this.onPanUpdate,
this.onPanEnd,
this.onPanCancel,
this.onScaleStart,
this.onScaleUpdate,
this.onScaleEnd,
this.behavior,
this.excludeFromSemantics = false
})
可以看到GestureDetector的本质就是一个普通的widget,它拥有很多的手势onTapDown(点下),onTapUp(抬起),onTap(点击)...等,同时也拥有child属性,我们可以利用child绘制界面,利用手势处理点击事件。
4.1.4 Widget 深入探索
首先我们需要明白,Widget 是什么?这里有一个 “总所周知” 的答就是:Widget并不真正的渲染对象 。是的,事实上在 Flutter 中渲染是经历了从 Widget 到 Element 再到 RenderObject 的过程。
-
我们都知道 Widget 是不可变的,那么 Widget 是如何在不可变中去构建画面的?上面我们知道,Widget 是需要转化为 Element 去渲染的,而从下图注释可以看到,事实上 Widget 只是 Element 的一个配置描述 ,告诉 Element 这个实例如何去渲染。
那么 Widget 和 Element 之间是怎样的对应关系呢?从上图注释也可知: Widget 和 Element 之间是一对多的关系 。实际上渲染树是由 Element 实例的节点构成的树,而作为配置文件的 Widget 可能被复用到树的多个部分,对应产生多个 Element 对象。
-
那么RenderObject 又是什么?它和上述两个的关系是什么?从源码注释写着 An object in the render tree 可以看出到 RenderObject 才是实际的渲染对象,而通过 Element 源码我们可以看出:Element 持有 RenderObject 和 Widget。
再结合下图,可以大致总结出三者的关系是:配置文件 Widget 生成了 Element,而后创建 RenderObject 关联到 Element 的内部 renderObject 对象上,最后Flutter 通过 RenderObject 数据来布局和绘制。 理论上你也可以认为 RenderObject 是最终给 Flutter 的渲染数据,它保存了大小和位置等信息,Flutter 通过它去绘制出画面。
- 说到 RenderObject ,就不得不说 RenderBox :A render object in a 2D Cartesian coordinate system,从源码注释可以看出,它是在继承 RenderObject 基础的布局和绘制功能上,实现了“笛卡尔坐标系”:以 Top、Left 为基点,通过宽高两个轴实现布局和嵌套的。
RenderBox 避免了直接使用 RenderObject 的麻烦场景,其中 RenderBox 的布局和计算大小是在 performLayout() 和 performResize() 这两个方法中去处理,很多时候我们更多的是选择继承 RenderBox 去实现自定义。
- 综合上述情况,我们知道:
- Widget只是显示的数据配置,所以相对而言是轻量级的存在,而 Flutter 中对 Widget 的也做了一定的优化,所以每次改变状态导致的 Widget 重构并不会有太大的问题。
- RenderObject 就不同了,RenderObject 涉及到布局、计算、绘制等流程,要是每次都全部重新创建开销就比较大了。
- 所以针对是否每次都需要创建出新的 Element 和 RenderObject 对象,Widget 都做了对应的判断以便于复用,比如:在 newWidget 与oldWidget 的 runtimeType 和 key 相等时会选择使用 newWidget 去更新已经存在的 Element 对象,不然就选择重新创建新的 Element。
由此可知:Widget 重新创建,Element 树和 RenderObject 树并不会完全重新创建。
-
看到这,说个题外话:那一般我们可以怎么获取布局的大小和位置呢?
首先这里需要用到我们前文中提过的 GlobalKey ,通过 key 去获取到控件对象的 BuildContext,而我们也知道 BuildContext 的实现其实是 Element,而Element持有 RenderObject 。So,我们知道的 RenderObject ,实际上获取到的就是 RenderBox ,那么通过 RenderBox 我们就只大小和位置了。
showSizes() {
RenderBox renderBoxRed = fileListKey.currentContext.findRenderObject();
print(renderBoxRed.size);
}
showPositions() {
RenderBox renderBoxRed = fileListKey.currentContext.findRenderObject();
print(renderBoxRed.localToGlobal(Offset.zero));
}
4.2 StatelessWidget和StatefulWidget
通俗点讲就是:
stateful组件就是和用户交互后会有状态变化,例如滚动条Slider。
stateless组件就是交互后没有状态变化,例如显示的一个文本Text。
4.2.1 基本概念和用法
- StatefulWidget
具有可变状态( state)的Widget(窗口小部件).
例如系统提供的 Checkbox, Radio, Slider, InkWell, Form, and TextField 都是 stateful widgets, 他们都是 StatefulWidget的子类。
状态( state) 是可以在构建Widget时同步读取时 和 在Widget的生命周期期间可能改变的信息
Widget实现者的责任就是 在状态改变时通过 State.setState. 立即通知状态
当您描述的用户界面部分不依赖于对象本身中的配置信息和其中构件被夸大的BuildContext时,无状态小部件很有用。对于可以动态改变的组合,例如由于具有内部时钟驱动状态,或取决于某些系统状态,请考虑使用StatefulWidget。
StatefulWidget实例本身是不可变的,并将其可变状态存储在由createState方法创建的独立状态对象中 ,或者存储在该状态订阅的对象中,例如Stream或ChangeNotifier对象,其引用存储在StatefulWidget的最终字段中本身。
该框架只要调用一个StatefulWidget就 调用createState,这意味着如果该小部件已经插入到多个位置的树中,那么多个State对象可能与同一个StatefulWidget关联。同样,如果StatefulWidget从树中移除,后来在树再次插入时,框架将调用createState再创建一个新的国家目标,简化的生命周期状态的对象。
- StatelessWidget
不需要可变状态的小部件。
无状态小部件是一个小部件,它通过构建一系列其他小部件来更加具体地描述用户界面,从而描述用户界面的一部分。构建过程以递归方式继续进行,直到用户界面的描述完全具体(例如,完全由RenderObjectWidget组成,它描述具体的RenderObject)。
当您描述的用户界面部分不依赖于对象本身中的配置信息和其中构件被夸大的BuildContext时,无状态小部件很有用。对于可以动态改变的组合,例如由于具有内部时钟驱动状态,或取决于某些系统状态,请考虑使用StatefulWidget。
无状态小部件的构建方法通常只在以下三种情况下调用:第一次将小部件插入树中,第一次在小部件的父级更改其配置时以及第二次使用InheritedWidget时,它依赖于更改。
如果一个小部件的父节点会定期更改小部件的配置,或者如果它依赖于频繁更改的继承小部件,那么优化构建方法的性能以保持流畅的渲染性能非常重要。
有几种技术可以用来最小化重建无状态小部件的影响:
最小化构建方法及其创建的任何小部件传递创建的节点数量。例如,可以考虑只使用一个Align或一个 CustomSingleChildLayout,而不是精心安排Row s,Column s,Padding s和SizedBox es来定位一个单独的孩子。您可以考虑使用单个CustomPaint小部件,而不是使用多个Container的复杂分层和装饰 s来绘制恰当的图形效果。
const尽可能使用小部件,并为小部件提供const构造函数,以便小部件的用户也可以这样做。
考虑将无状态小部件重构为有状态的小部件,以便它可以使用StatefulWidget中描述的一些技术,例如缓存子树的公共部分,并在更改树结构时使用GlobalKey。
如果由于使用了InheritedWidget,小部件可能会经常重建 ,请考虑将无状态小部件重构为多个小部件,并将更改后的树部分推送到树叶。例如,不是构建一个具有四个小部件的树,最内部的小部件取决于主题,而是考虑将构建最内部小部件的构建函数的部分分解到其自己的小部件中,以便只有最内部的小部件当主题改变时需要重建。
4.2.2 源码分析
Flutter的Widget有StatelessWidget和StatefulWidget两个子类(当然还有其他子类,此处暂且不谈),二者的的使用方式大致模板代码如下:
//StatelessWidget的使用模板代码
class StatelessWidgetDemo extends StatelessWidget{
@override
Widget build(BuildContext context) {
return null;///返回创建的页面
}
}
//StatefulWidget的使用方式模板代码
class StatefulWidgetDemo extends StatefulWidget{
@override
State createState() {
//创建state对象
return _State();
}
}
class _State extends State{
//创建页面
@override
Widget build(BuildContext context) {
return null;
}
}
这是典型的模板设计模式的应用,我们只需要依葫芦画瓢就可以创建所需的UI页
阅读上面的代码,可以跑出一下问题:
1) build方法需要一个BuildContext参数,那么这个BuildContext是什么?
2)build方法是模板方法,那么什么时候调用的呢?
带着这两个问题,后面简单的梳理下Widget的结构,之所以说是简单的梳理,因为难得我也不会,还没研究到。
StatelessWidget和StatefulWidget都继承于Widget,其定义如下:
abstract class Widget extends DiagnosticableTree {
const Widget({ this.key });
final Key key;
@protected
Element createElement();
}
Widget继承于DiagnosticableTree,且提供了一个createElement抽象方法返回了一个Element对象,该对象查看源码可知其继承解构是Element extends DiagnosticableTree implements BuildContext.所以其Widget 和Element的整体解构可以用如下图表示:
先来看看StatelessWidget的具体实现:
abstract class StatelessWidget extends Widget {
@override
StatelessElement createElement() => StatelessElement(this);
@protected
Widget build(BuildContext context);
}
StatelessWidget实现了createElement方法返回了一个StatelessElement对象,且提供了一个build方法,注意build方法的参数是BuildContext,那么这个BuildContext是不是就是StatelessElement这个对象了呢?预知答案如何先看看build是在那儿调用的,在StatelessElement这个类里可以找到答案,其源码如下:
class StatelessElement extends ComponentElement {
//在element中调用了widget.build方法,并将自己传入了进去
//所以BuildContext就是StatelessElement
@override
Widget build() => widget.build(this);
}
通过其源码可以知道StatelessElement继承了ComponentElement,且重写了build方法,其调用了widget的build方法。这个build就是StatelessWidget对象(或者其子对象),并且可以确定StatelessWidget的build方法的参数就是StatelessElement这个对象。
所以可以断定想要知道StatelessWidget的build(BuildContext)方法什么时候调用,就需要知道StatelessElement的build()什么时候调用。在StatelessElement的父类ComponentElement的perfromReBuild方法可以得到解答:
@override
void performRebuild() {
//省略了部分代码
Widget built = build();
//省略部分代码
}
所以概述下来就是StatelessWidget通过build(BuildContext)方法构建Widget是通过StatelessElement的build()方法来完成的。想要调用build(BuildContext)必定先通过createElement方法创建一个StatelessElement对象。那么有一个此处就有一个问题了:Widget的createElement方法是神马时候调用的呢?
上面粗略的分了StatelessWidget,下来再来简略的看下StatefullWidget这个类。
abstract class StatefulWidget extends Widget {
@override
StatefulElement createElement() => StatefulElement(this);
@protected
State createState();
}
StatefulWidget的createElement方法返回了SatefulElement,且提供了一个createState()方法,大胆猜测一下createState就是在StatefulElement里面调用的,果不其然,证据如下:
StatefulElement 的构造器:
StatefulElement(StatefulWidget widget)
///调用了createState方法
: _state = widget.createState(), super(widget) {
}
StatefulWidget需要通过createState方法创建一个State,State也提供了build(BuildContext)方法。另外查看StatefulElement的可以该类也实现了ComponentElement的build方法:
@override
Widget build() => state.build(this);
分析到这儿StatelessWidget ,StatefulWidget和Element的关系可以用如下图来表示:
其构建关系的流程图可以用如下来表示:
build(BuildContext)方法就需要先调用具体子类的createElement方法创建对应的ComponentElement对象,而后重写Component的build方法。performRebuild方法又是什么时机调用的的呢?performRebuild方法在ComponentElment的mount方法和rebuild方法()方法里面都有调用,而ComponentElement的mount方法又是Flutter形成渲染树的入口:
//mount方法形成了解析Widget,构建渲染树
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_firstBuild();
}
void _firstBuild() {
//rebuild方法内部调用了performRebuild方法。
rebuild();
}