Flutter开发实战初级(一)ListView详解

Flutter开发实战初级(一)ListView详解

  • 本篇博客主要以一个demo的形式讲解ListView的使用

源码下载点击这里:flutter_listview_demo
先来看一下效果图,下面是运行在iphone11上面的:

image.png

ListView 知识点

在Flutter中,用ListView来显示列表项,支持垂直和水平方向展示,通过一个属性我们就可以控制其方向
1.水平的列表
2.垂直的列表
3.数据量非常大的列表
4.内置的ListTile(挺好用的)

ListView Demo

1.demo 下载地址:flutter_listviewdemo
2.运行效果:


image.png

一. 新建car.dart 保存模型信息

1.定义一个Car

class Car {
  const Car({
    this.name,
    this.imageUrl,
  });

  final String name;
  final String imageUrl;
}

2.定义一个数组保存Car对象

//模型数组
final List<Car> 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',
  )

];

二. 新建carlistview.dart 用来展示列表数据

1.定义Listview 展示数据

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return ListView.builder(
     //控制方向 默认是垂直的
//           scrollDirection: Axis.horizontal, //控制水平方向显示
/*           children: <Widget>[
              _getContainer('Maps', Icons.map),
              _getContainer('phone', Icons.phone),
              _getContainer('Maps', Icons.map),
            ], */

        itemCount: datas.length, //告诉ListView总共有多少个cell
        itemBuilder: _cellForRow //使用_cellForRow回调返回每个cell
        );
  }

2.定义一个回调函数,返回每个cell

Widget _cellForRow(BuildContext context, int index) {
    return Container(
      color: Colors.white,

      margin: EdgeInsets.all(10),
      child: Column(
        children: <Widget>[
          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,),
        ],
      ), //每人一辆跑车
    );
  }

三. 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(),
    );
  }
}

四. 知识点讲解

main函数使用了(=>)符号, 这是Dart中单行函数或方法的简写。

该应用程序继承了 StatelessWidget,这将会使应用本身也成为一个widget。 在Flutter中,大多数东西都是widget,包括对齐(alignment)、填充(padding)和布局(layout)

Scaffold 是 Material library 中提供的一个widget, 它提供了默认的导航栏、标题和包含主屏幕widget树的body属性。widget树可以很复杂。

widget的主要工作是提供一个build()方法来描述如何根据其他较低级别的widget来显示自己。

使用外部包

1.1 Widget

1.1.1 Widget基本概念

Stateless widgets 是不可变的, 这意味着它们的属性不能改变 - 所有的值都是最终的.

Stateful widgets 持有的状态可能在widget生命周期中发生变化. 实现一个 stateful widget 至少需要两个类:

一个 StatefulWidget类。
一个 State类。 StatefulWidget类本身是不变的,但是 State类在widget生命周期中始终存在.

  • Stateful(有状态) 和 stateless(无状态) widgets

有些widgets是有状态的, 有些是无状态的
如果用户与widget交互,widget会发生变化,那么它就是有状态的.
widget的状态(state)是一些可以更改的值, 如一个slider滑动条的当前值或checkbox是否被选中.
widget的状态保存在一个State对象中, 它和widget的布局显示分离。
当widget状态改变时, State 对象调用setState(), 告诉框架去重绘widget.
stateless widget 没有内部状态. Icon、 IconButton, 和Text 都是无状态widget, 他们都是 StatelessWidget的子类。

stateful widget 是动态的. 用户可以和其交互 (例如输入一个表单、 或者移动一个slider滑块),或者可以随时间改变 (也许是数据改变导致的UI更新). Checkbox, Radio, Slider, InkWell, Form, and TextField 都是 stateful widgets, 他们都是 StatefulWidget的子类。

1.1.2 Widget之间的交互
1.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绘制界面,利用手势处理点击事件。

1.1.4 Widget 深入探索

1.首先我们需要明白,Widget 是什么?这里有一个 “总所周知” 的答就是:Widget并不真正的渲染对象 。是的,事实上在 Flutter 中渲染是经历了从 Widget 到 Element 再到 RenderObject 的过程。

2.我们都知道 Widget 是不可变的,那么 Widget 是如何在不可变中去构建画面的?上面我们知道,Widget 是需要转化为 Element 去渲染的,而从下图注释可以看到,事实上 Widget 只是 Element 的一个配置描述 ,告诉 Element 这个实例如何去渲染。


image.png

那么 Widget 和 Element 之间是怎样的对应关系呢?从上图注释也可知: Widget 和 Element 之间是一对多的关系 。实际上渲染树是由 Element 实例的节点构成的树,而作为配置文件的 Widget 可能被复用到树的多个部分,对应产生多个 Element 对象。

3.那么RenderObject 又是什么?它和上述两个的关系是什么?从源码注释写着 An object in the render tree 可以看出到 RenderObject 才是实际的渲染对象,而通过 Element 源码我们可以看出:Element 持有 RenderObject 和 Widget。

image.png

再结合下图,可以大致总结出三者的关系是:配置文件 Widget 生成了 Element,而后创建 RenderObject 关联到 Element 的内部 renderObject 对象上,最后Flutter 通过 RenderObject 数据来布局和绘制。 理论上你也可以认为 RenderObject 是最终给 Flutter 的渲染数据,它保存了大小和位置等信息,Flutter 通过它去绘制出画面。


image.png

4.说到 RenderObject ,就不得不说 RenderBox :A render object in a 2D Cartesian coordinate system,从源码注释可以看出,它是在继承 RenderObject 基础的布局和绘制功能上,实现了“笛卡尔坐标系”:以 Top、Left 为基点,通过宽高两个轴实现布局和嵌套的。

RenderBox 避免了直接使用 RenderObject 的麻烦场景,其中 RenderBox 的布局和计算大小是在 performLayout() 和 performResize() 这两个方法中去处理,很多时候我们更多的是选择继承 RenderBox 去实现自定义。

5.综合上述情况,我们知道:

  • Widget只是显示的数据配置,所以相对而言是轻量级的存在,而 Flutter 中对 Widget 的也做了一定的优化,所以每次改变状态导致的 Widget 重构并不会有太大的问题。
  • RenderObject 就不同了,RenderObject 涉及到布局、计算、绘制等流程,要是每次都全部重新创建开销就比较大了。

6.所以针对是否每次都需要创建出新的 Element 和 RenderObject 对象,Widget 都做了对应的判断以便于复用,比如:在 newWidget 与oldWidget 的 runtimeType 和 key 相等时会选择使用 newWidget 去更新已经存在的 Element 对象,不然就选择重新创建新的 Element。

由此可知:Widget 重新创建,Element 树和 RenderObject 树并不会完全重新创建。

7.看到这,说个题外话:那一般我们可以怎么获取布局的大小和位置呢?

首先这里需要用到我们前文中提过的 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));
  }

1.2 StatelessWidget和StatefulWidget

通俗点讲就是:
stateful组件就是和用户交互后会有状态变化,例如滚动条Slider。
stateless组件就是交互后没有状态变化,例如显示的一个文本Text。
1.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,小部件可能会经常重建 ,请考虑将无状态小部件重构为多个小部件,并将更改后的树部分推送到树叶。例如,不是构建一个具有四个小部件的树,最内部的小部件取决于主题,而是考虑将构建最内部小部件的构建函数的部分分解到其自己的小部件中,以便只有最内部的小部件当主题改变时需要重建。

1.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<StatefulWidget> createState() {
    //创建state对象
    return _State();
  }
}
class _State extends State<StatefulWidgetDemo>{
  //创建页面
  @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的整体解构可以用如下图表示:


image.png

先来看看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的关系可以用如下图来表示:


image.png

其构建关系的流程图可以用如下来表示:


image.png

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();
  }

收录|原文地址

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容