Flutter開發(fā)實戰(zhàn)初級(一)ListView詳解
- 本篇博客主要以一個demo的形式講解ListView的使用
源碼下載點擊這里:flutter_listview_demo
先來看一下效果圖,下面是運行在iphone11上面的:

ListView 知識點
在Flutter中,用ListView來顯示列表項,支持垂直和水平方向展示,通過一個屬性我們就可以控制其方向
1.水平的列表
2.垂直的列表
3.數(shù)據(jù)量非常大的列表
4.內(nèi)置的ListTile(挺好用的)
ListView Demo
1.demo 下載地址:flutter_listviewdemo
2.運行效果:

一. 新建car.dart 保存模型信息
1.定義一個Car
class Car {
const Car({
this.name,
this.imageUrl,
});
final String name;
final String imageUrl;
}
2.定義一個數(shù)組保存Car對象
//模型數(shù)組
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 用來展示列表數(shù)據(jù)
1.定義Listview 展示數(shù)據(jù)
@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回調(diào)返回每個cell
);
}
2.定義一個回調(diào)函數(shù),返回每個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 調(diào)用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函數(shù)使用了(=>)符號, 這是Dart中單行函數(shù)或方法的簡寫。
該應(yīng)用程序繼承了 StatelessWidget,這將會使應(yīng)用本身也成為一個widget。 在Flutter中,大多數(shù)東西都是widget,包括對齊(alignment)、填充(padding)和布局(layout)
Scaffold 是 Material library 中提供的一個widget, 它提供了默認的導(dǎo)航欄、標(biāo)題和包含主屏幕widget樹的body屬性。widget樹可以很復(fù)雜。
widget的主要工作是提供一個build()方法來描述如何根據(jù)其他較低級別的widget來顯示自己。
使用外部包
1.1 Widget
1.1.1 Widget基本概念
Stateless widgets 是不可變的, 這意味著它們的屬性不能改變 - 所有的值都是最終的.
Stateful widgets 持有的狀態(tài)可能在widget生命周期中發(fā)生變化. 實現(xiàn)一個 stateful widget 至少需要兩個類:
一個 StatefulWidget類。
一個 State類。 StatefulWidget類本身是不變的,但是 State類在widget生命周期中始終存在.
- Stateful(有狀態(tài)) 和 stateless(無狀態(tài)) widgets
有些widgets是有狀態(tài)的, 有些是無狀態(tài)的
如果用戶與widget交互,widget會發(fā)生變化,那么它就是有狀態(tài)的.
widget的狀態(tài)(state)是一些可以更改的值, 如一個slider滑動條的當(dāng)前值或checkbox是否被選中.
widget的狀態(tài)保存在一個State對象中, 它和widget的布局顯示分離。
當(dāng)widget狀態(tài)改變時, State 對象調(diào)用setState(), 告訴框架去重繪widget.
stateless widget 沒有內(nèi)部狀態(tài). Icon、 IconButton, 和Text 都是無狀態(tài)widget, 他們都是 StatelessWidget的子類。
stateful widget 是動態(tài)的. 用戶可以和其交互 (例如輸入一個表單、 或者移動一個slider滑塊),或者可以隨時間改變 (也許是數(shù)據(jù)改變導(dǎo)致的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的本質(zhì)就是一個普通的widget,它擁有很多的手勢onTapDown(點下),onTapUp(抬起),onTap(點擊)…等,同時也擁有child屬性,我們可以利用child繪制界面,利用手勢處理點擊事件。
1.1.4 Widget 深入探索
1.首先我們需要明白,Widget 是什么?這里有一個 “總所周知” 的答就是:Widget并不真正的渲染對象 。是的,事實上在 Flutter 中渲染是經(jīng)歷了從 Widget 到 Element 再到 RenderObject 的過程。
2.我們都知道 Widget 是不可變的,那么 Widget 是如何在不可變中去構(gòu)建畫面的?上面我們知道,Widget 是需要轉(zhuǎn)化為 Element 去渲染的,而從下圖注釋可以看到,事實上 Widget 只是 Element 的一個配置描述 ,告訴 Element 這個實例如何去渲染。

那么 Widget 和 Element 之間是怎樣的對應(yīng)關(guān)系呢?從上圖注釋也可知: Widget 和 Element 之間是一對多的關(guān)系 。實際上渲染樹是由 Element 實例的節(jié)點構(gòu)成的樹,而作為配置文件的 Widget 可能被復(fù)用到樹的多個部分,對應(yīng)產(chǎn)生多個 Element 對象。
3.那么RenderObject 又是什么?它和上述兩個的關(guān)系是什么?從源碼注釋寫著 An object in the render tree 可以看出到 RenderObject 才是實際的渲染對象,而通過 Element 源碼我們可以看出:Element 持有 RenderObject 和 Widget。

再結(jié)合下圖,可以大致總結(jié)出三者的關(guān)系是:配置文件 Widget 生成了 Element,而后創(chuàng)建 RenderObject 關(guān)聯(lián)到 Element 的內(nèi)部 renderObject 對象上,最后Flutter 通過 RenderObject 數(shù)據(jù)來布局和繪制。 理論上你也可以認為 RenderObject 是最終給 Flutter 的渲染數(shù)據(jù),它保存了大小和位置等信息,F(xiàn)lutter 通過它去繪制出畫面。

4.說到 RenderObject ,就不得不說 RenderBox :A render object in a 2D Cartesian coordinate system,從源碼注釋可以看出,它是在繼承 RenderObject 基礎(chǔ)的布局和繪制功能上,實現(xiàn)了“笛卡爾坐標(biāo)系”:以 Top、Left 為基點,通過寬高兩個軸實現(xiàn)布局和嵌套的。
RenderBox 避免了直接使用 RenderObject 的麻煩場景,其中 RenderBox 的布局和計算大小是在 performLayout() 和 performResize() 這兩個方法中去處理,很多時候我們更多的是選擇繼承 RenderBox 去實現(xiàn)自定義。
5.綜合上述情況,我們知道:
- Widget只是顯示的數(shù)據(jù)配置,所以相對而言是輕量級的存在,而 Flutter 中對 Widget 的也做了一定的優(yōu)化,所以每次改變狀態(tài)導(dǎo)致的 Widget 重構(gòu)并不會有太大的問題。
- RenderObject 就不同了,RenderObject 涉及到布局、計算、繪制等流程,要是每次都全部重新創(chuàng)建開銷就比較大了。
6.所以針對是否每次都需要創(chuàng)建出新的 Element 和 RenderObject 對象,Widget 都做了對應(yīng)的判斷以便于復(fù)用,比如:在 newWidget 與oldWidget 的 runtimeType 和 key 相等時會選擇使用 newWidget 去更新已經(jīng)存在的 Element 對象,不然就選擇重新創(chuàng)建新的 Element。
由此可知:Widget 重新創(chuàng)建,Element 樹和 RenderObject 樹并不會完全重新創(chuàng)建。
7.看到這,說個題外話:那一般我們可以怎么獲取布局的大小和位置呢?
首先這里需要用到我們前文中提過的 GlobalKey ,通過 key 去獲取到控件對象的 BuildContext,而我們也知道 BuildContext 的實現(xiàn)其實是 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組件就是和用戶交互后會有狀態(tài)變化,例如滾動條Slider。
stateless組件就是交互后沒有狀態(tài)變化,例如顯示的一個文本Text。
1.2.1 基本概念和用法
- StatefulWidget
具有可變狀態(tài)( state)的Widget(窗口小部件).
例如系統(tǒng)提供的 Checkbox, Radio, Slider, InkWell, Form, and TextField 都是 stateful widgets, 他們都是 StatefulWidget的子類。
狀態(tài)( state) 是可以在構(gòu)建Widget時同步讀取時 和 在Widget的生命周期期間可能改變的信息
Widget實現(xiàn)者的責(zé)任就是 在狀態(tài)改變時通過 State.setState. 立即通知狀態(tài)
當(dāng)您描述的用戶界面部分不依賴于對象本身中的配置信息和其中構(gòu)件被夸大的BuildContext時,無狀態(tài)小部件很有用。對于可以動態(tài)改變的組合,例如由于具有內(nèi)部時鐘驅(qū)動狀態(tài),或取決于某些系統(tǒng)狀態(tài),請考慮使用StatefulWidget。
StatefulWidget實例本身是不可變的,并將其可變狀態(tài)存儲在由createState方法創(chuàng)建的獨立狀態(tài)對象中 ,或者存儲在該狀態(tài)訂閱的對象中,例如Stream或ChangeNotifier對象,其引用存儲在StatefulWidget的最終字段中本身。
該框架只要調(diào)用一個StatefulWidget就 調(diào)用createState,這意味著如果該小部件已經(jīng)插入到多個位置的樹中,那么多個State對象可能與同一個StatefulWidget關(guān)聯(lián)。同樣,如果StatefulWidget從樹中移除,后來在樹再次插入時,框架將調(diào)用createState再創(chuàng)建一個新的國家目標(biāo),簡化的生命周期狀態(tài)的對象。
- StatelessWidget
不需要可變狀態(tài)的小部件。
無狀態(tài)小部件是一個小部件,它通過構(gòu)建一系列其他小部件來更加具體地描述用戶界面,從而描述用戶界面的一部分。構(gòu)建過程以遞歸方式繼續(xù)進行,直到用戶界面的描述完全具體(例如,完全由RenderObjectWidget組成,它描述具體的RenderObject)。
當(dāng)您描述的用戶界面部分不依賴于對象本身中的配置信息和其中構(gòu)件被夸大的BuildContext時,無狀態(tài)小部件很有用。對于可以動態(tài)改變的組合,例如由于具有內(nèi)部時鐘驅(qū)動狀態(tài),或取決于某些系統(tǒng)狀態(tài),請考慮使用StatefulWidget。
無狀態(tài)小部件的構(gòu)建方法通常只在以下三種情況下調(diào)用:第一次將小部件插入樹中,第一次在小部件的父級更改其配置時以及第二次使用InheritedWidget時,它依賴于更改。
如果一個小部件的父節(jié)點會定期更改小部件的配置,或者如果它依賴于頻繁更改的繼承小部件,那么優(yōu)化構(gòu)建方法的性能以保持流暢的渲染性能非常重要。
有幾種技術(shù)可以用來最小化重建無狀態(tài)小部件的影響:
最小化構(gòu)建方法及其創(chuàng)建的任何小部件傳遞創(chuàng)建的節(jié)點數(shù)量。例如,可以考慮只使用一個Align或一個 CustomSingleChildLayout,而不是精心安排Row s,Column s,Padding s和SizedBox es來定位一個單獨的孩子。您可以考慮使用單個CustomPaint小部件,而不是使用多個Container的復(fù)雜分層和裝飾 s來繪制恰當(dāng)?shù)膱D形效果。
const盡可能使用小部件,并為小部件提供const構(gòu)造函數(shù),以便小部件的用戶也可以這樣做。
考慮將無狀態(tài)小部件重構(gòu)為有狀態(tài)的小部件,以便它可以使用StatefulWidget中描述的一些技術(shù),例如緩存子樹的公共部分,并在更改樹結(jié)構(gòu)時使用GlobalKey。
如果由于使用了InheritedWidget,小部件可能會經(jīng)常重建 ,請考慮將無狀態(tài)小部件重構(gòu)為多個小部件,并將更改后的樹部分推送到樹葉。例如,不是構(gòu)建一個具有四個小部件的樹,最內(nèi)部的小部件取決于主題,而是考慮將構(gòu)建最內(nèi)部小部件的構(gòu)建函數(shù)的部分分解到其自己的小部件中,以便只有最內(nèi)部的小部件當(dāng)主題改變時需要重建。
1.2.2 源碼分析
Flutter的Widget有StatelessWidget和StatefulWidget兩個子類(當(dāng)然還有其他子類,此處暫且不談),二者的的使用方式大致模板代碼如下:
//StatelessWidget的使用模板代碼
class StatelessWidgetDemo extends StatelessWidget{
@override
Widget build(BuildContext context) {
return null;///返回創(chuàng)建的頁面
}
}
//StatefulWidget的使用方式模板代碼
class StatefulWidgetDemo extends StatefulWidget{
@override
State<StatefulWidget> createState() {
//創(chuàng)建state對象
return _State();
}
}
class _State extends State<StatefulWidgetDemo>{
//創(chuàng)建頁面
@override
Widget build(BuildContext context) {
return null;
}
}
這是典型的模板設(shè)計模式的應(yīng)用,我們只需要依葫蘆畫瓢就可以創(chuàng)建所需的UI頁
閱讀上面的代碼,可以跑出一下問題:
1) build方法需要一個BuildContext參數(shù),那么這個BuildContext是什么?
2)build方法是模板方法,那么什么時候調(diào)用的呢?
帶著這兩個問題,后面簡單的梳理下Widget的結(jié)構(gòu),之所以說是簡單的梳理,因為難得我也不會,還沒研究到。
StatelessWidget和StatefulWidget都繼承于Widget,其定義如下:
abstract class Widget extends DiagnosticableTree {
const Widget({ this.key });
final Key key;
@protected
Element createElement();
}
Widget繼承于DiagnosticableTree,且提供了一個createElement抽象方法返回了一個Element對象,該對象查看源碼可知其繼承解構(gòu)是Element extends DiagnosticableTree implements BuildContext.所以其Widget 和Element的整體解構(gòu)可以用如下圖表示:

先來看看StatelessWidget的具體實現(xiàn):
abstract class StatelessWidget extends Widget {
@override
StatelessElement createElement() => StatelessElement(this);
@protected
Widget build(BuildContext context);
}
StatelessWidget實現(xiàn)了createElement方法返回了一個StatelessElement對象,且提供了一個build方法,注意build方法的參數(shù)是BuildContext,那么這個BuildContext是不是就是StatelessElement這個對象了呢?預(yù)知答案如何先看看build是在那兒調(diào)用的,在StatelessElement這個類里可以找到答案,其源碼如下:
class StatelessElement extends ComponentElement {
//在element中調(diào)用了widget.build方法,并將自己傳入了進去
//所以BuildContext就是StatelessElement
@override
Widget build() => widget.build(this);
}
通過其源碼可以知道StatelessElement繼承了ComponentElement,且重寫了build方法,其調(diào)用了widget的build方法。這個build就是StatelessWidget對象(或者其子對象),并且可以確定StatelessWidget的build方法的參數(shù)就是StatelessElement這個對象。
所以可以斷定想要知道StatelessWidget的build(BuildContext)方法什么時候調(diào)用,就需要知道StatelessElement的build()什么時候調(diào)用。在StatelessElement的父類ComponentElement的perfromReBuild方法可以得到解答:
@override
void performRebuild() {
//省略了部分代碼
Widget built = build();
//省略部分代碼
}
所以概述下來就是StatelessWidget通過build(BuildContext)方法構(gòu)建Widget是通過StatelessElement的build()方法來完成的。想要調(diào)用build(BuildContext)必定先通過createElement方法創(chuàng)建一個StatelessElement對象。那么有一個此處就有一個問題了:Widget的createElement方法是神馬時候調(diào)用的呢?
上面粗略的分了StatelessWidget,下來再來簡略的看下StatefullWidget這個類。
abstract class StatefulWidget extends Widget {
@override
StatefulElement createElement() => StatefulElement(this);
@protected
State createState();
}
StatefulWidget的createElement方法返回了SatefulElement,且提供了一個createState()方法,大膽猜測一下createState就是在StatefulElement里面調(diào)用的,果不其然,證據(jù)如下:
StatefulElement 的構(gòu)造器:
StatefulElement(StatefulWidget widget)
///調(diào)用了createState方法
: _state = widget.createState(), super(widget) {
}
StatefulWidget需要通過createState方法創(chuàng)建一個State,State也提供了build(BuildContext)方法。另外查看StatefulElement的可以該類也實現(xiàn)了ComponentElement的build方法:
@override
Widget build() => state.build(this);
分析到這兒StatelessWidget ,StatefulWidget和Element的關(guān)系可以用如下圖來表示:

其構(gòu)建關(guān)系的流程圖可以用如下來表示:

build(BuildContext)方法就需要先調(diào)用具體子類的createElement方法創(chuàng)建對應(yīng)的ComponentElement對象,而后重寫Component的build方法。performRebuild方法又是什么時機調(diào)用的的呢?performRebuild方法在ComponentElment的mount方法和rebuild方法()方法里面都有調(diào)用,而ComponentElement的mount方法又是Flutter形成渲染樹的入口:
//mount方法形成了解析Widget,構(gòu)建渲染樹
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_firstBuild();
}
void _firstBuild() {
//rebuild方法內(nèi)部調(diào)用了performRebuild方法。
rebuild();
}
收錄|原文地址