Flutter開發(fā)實戰(zhàn)初級(一)ListView詳解
Flutter開發(fā)實戰(zhàn)初級(一)ListView詳解
- 本篇博客主要以一個demo的形式講解ListView的使用
源碼下載點擊這里:flutter_listview_demo
先來看一下效果圖,下面是運行在iphone11上面的:
ListView 知識點
在Flutter中,用ListView來顯示列表項,支持垂直和水平方向展示,通過一個屬性我們就可以控制其方向
1.水平的列表
2.垂直的列表
3.數(shù)據(jù)量非常大的列表
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;
}
- 定義一個數(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',
)
];
2. 新建carlistview.dart 用來展示列表數(shù)據(jù)
- 定義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回調返回每個cell
);
}
- 定義一個回調函數(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,),
],
), //每人一輛跑車
);
}
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 中渲染是經(jīng)歷了從 Widget 到 Element 再到 RenderObject 的過程。
-
我們都知道 Widget 是不可變的,那么 Widget 是如何在不可變中去構建畫面的?上面我們知道,Widget 是需要轉化為 Element 去渲染的,而從下圖注釋可以看到,事實上 Widget 只是 Element 的一個配置描述 ,告訴 Element 這個實例如何去渲染。
在這里插入圖片描述
那么 Widget 和 Element 之間是怎樣的對應關系呢?從上圖注釋也可知: Widget 和 Element 之間是一對多的關系 。實際上渲染樹是由 Element 實例的節(jié)點構成的樹,而作為配置文件的 Widget 可能被復用到樹的多個部分,對應產(chǎn)生多個 Element 對象。
-
那么RenderObject 又是什么?它和上述兩個的關系是什么?從源碼注釋寫著 An object in the render tree 可以看出到 RenderObject 才是實際的渲染對象,而通過 Element 源碼我們可以看出:Element 持有 RenderObject 和 Widget。
在這里插入圖片描述
再結合下圖,可以大致總結出三者的關系是:配置文件 Widget 生成了 Element,而后創(chuàng)建 RenderObject 關聯(lián)到 Element 的內部 renderObject 對象上,最后Flutter 通過 RenderObject 數(shù)據(jù)來布局和繪制。 理論上你也可以認為 RenderObject 是最終給 Flutter 的渲染數(shù)據(jù),它保存了大小和位置等信息,F(xiàn)lutter 通過它去繪制出畫面。
- 說到 RenderObject ,就不得不說 RenderBox :A render object in a 2D Cartesian coordinate system,從源碼注釋可以看出,它是在繼承 RenderObject 基礎的布局和繪制功能上,實現(xiàn)了“笛卡爾坐標系”:以 Top、Left 為基點,通過寬高兩個軸實現(xiàn)布局和嵌套的。
RenderBox 避免了直接使用 RenderObject 的麻煩場景,其中 RenderBox 的布局和計算大小是在 performLayout() 和 performResize() 這兩個方法中去處理,很多時候我們更多的是選擇繼承 RenderBox 去實現(xiàn)自定義。
- 綜合上述情況,我們知道:
- Widget只是顯示的數(shù)據(jù)配置,所以相對而言是輕量級的存在,而 Flutter 中對 Widget 的也做了一定的優(yōu)化,所以每次改變狀態(tài)導致的 Widget 重構并不會有太大的問題。
- RenderObject 就不同了,RenderObject 涉及到布局、計算、繪制等流程,要是每次都全部重新創(chuàng)建開銷就比較大了。
- 所以針對是否每次都需要創(chuàng)建出新的 Element 和 RenderObject 對象,Widget 都做了對應的判斷以便于復用,比如:在 newWidget 與oldWidget 的 runtimeType 和 key 相等時會選擇使用 newWidget 去更新已經(jīng)存在的 Element 對象,不然就選擇重新創(chuàng)建新的 Element。
由此可知:Widget 重新創(chuàng)建,Element 樹和 RenderObject 樹并不會完全重新創(chuàng)建。
-
看到這,說個題外話:那一般我們可以怎么獲取布局的大小和位置呢?
首先這里需要用到我們前文中提過的 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));
}
4.2 StatelessWidget和StatefulWidget
通俗點講就是:
stateful組件就是和用戶交互后會有狀態(tài)變化,例如滾動條Slider。
stateless組件就是交互后沒有狀態(tài)變化,例如顯示的一個文本Text。
4.2.1 基本概念和用法
- StatefulWidget
具有可變狀態(tài)( state)的Widget(窗口小部件).
例如系統(tǒng)提供的 Checkbox, Radio, Slider, InkWell, Form, and TextField 都是 stateful widgets, 他們都是 StatefulWidget的子類。
狀態(tài)( state) 是可以在構建Widget時同步讀取時 和 在Widget的生命周期期間可能改變的信息
Widget實現(xiàn)者的責任就是 在狀態(tài)改變時通過 State.setState. 立即通知狀態(tài)
當您描述的用戶界面部分不依賴于對象本身中的配置信息和其中構件被夸大的BuildContext時,無狀態(tài)小部件很有用。對于可以動態(tài)改變的組合,例如由于具有內部時鐘驅動狀態(tài),或取決于某些系統(tǒng)狀態(tài),請考慮使用StatefulWidget。
StatefulWidget實例本身是不可變的,并將其可變狀態(tài)存儲在由createState方法創(chuàng)建的獨立狀態(tài)對象中 ,或者存儲在該狀態(tài)訂閱的對象中,例如Stream或ChangeNotifier對象,其引用存儲在StatefulWidget的最終字段中本身。
該框架只要調用一個StatefulWidget就 調用createState,這意味著如果該小部件已經(jīng)插入到多個位置的樹中,那么多個State對象可能與同一個StatefulWidget關聯(lián)。同樣,如果StatefulWidget從樹中移除,后來在樹再次插入時,框架將調用createState再創(chuàng)建一個新的國家目標,簡化的生命周期狀態(tài)的對象。
- StatelessWidget
不需要可變狀態(tài)的小部件。
無狀態(tài)小部件是一個小部件,它通過構建一系列其他小部件來更加具體地描述用戶界面,從而描述用戶界面的一部分。構建過程以遞歸方式繼續(xù)進行,直到用戶界面的描述完全具體(例如,完全由RenderObjectWidget組成,它描述具體的RenderObject)。
當您描述的用戶界面部分不依賴于對象本身中的配置信息和其中構件被夸大的BuildContext時,無狀態(tài)小部件很有用。對于可以動態(tài)改變的組合,例如由于具有內部時鐘驅動狀態(tài),或取決于某些系統(tǒng)狀態(tài),請考慮使用StatefulWidget。
無狀態(tài)小部件的構建方法通常只在以下三種情況下調用:第一次將小部件插入樹中,第一次在小部件的父級更改其配置時以及第二次使用InheritedWidget時,它依賴于更改。
如果一個小部件的父節(jié)點會定期更改小部件的配置,或者如果它依賴于頻繁更改的繼承小部件,那么優(yōu)化構建方法的性能以保持流暢的渲染性能非常重要。
有幾種技術可以用來最小化重建無狀態(tài)小部件的影響:
最小化構建方法及其創(chuàng)建的任何小部件傳遞創(chuàng)建的節(jié)點數(shù)量。例如,可以考慮只使用一個Align或一個 CustomSingleChildLayout,而不是精心安排Row s,Column s,Padding s和SizedBox es來定位一個單獨的孩子。您可以考慮使用單個CustomPaint小部件,而不是使用多個Container的復雜分層和裝飾 s來繪制恰當?shù)膱D形效果。
const盡可能使用小部件,并為小部件提供const構造函數(shù),以便小部件的用戶也可以這樣做。
考慮將無狀態(tài)小部件重構為有狀態(tài)的小部件,以便它可以使用StatefulWidget中描述的一些技術,例如緩存子樹的公共部分,并在更改樹結構時使用GlobalKey。
如果由于使用了InheritedWidget,小部件可能會經(jīng)常重建 ,請考慮將無狀態(tài)小部件重構為多個小部件,并將更改后的樹部分推送到樹葉。例如,不是構建一個具有四個小部件的樹,最內部的小部件取決于主題,而是考慮將構建最內部小部件的構建函數(shù)的部分分解到其自己的小部件中,以便只有最內部的小部件當主題改變時需要重建。
4.2.2 源碼分析
Flutter的Widget有StatelessWidget和StatefulWidget兩個子類(當然還有其他子類,此處暫且不談),二者的的使用方式大致模板代碼如下:
//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;
}
}
這是典型的模板設計模式的應用,我們只需要依葫蘆畫瓢就可以創(chuàng)建所需的UI頁
閱讀上面的代碼,可以跑出一下問題:
1) build方法需要一個BuildContext參數(shù),那么這個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的具體實現(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這個對象了呢?預知答案如何先看看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方法的參數(shù)就是StatelessElement這個對象。
所以可以斷定想要知道StatelessWidget的build(BuildContext)方法什么時候調用,就需要知道StatelessElement的build()什么時候調用。在StatelessElement的父類ComponentElement的perfromReBuild方法可以得到解答:
@override
void performRebuild() {
//省略了部分代碼
Widget built = build();
//省略部分代碼
}
所以概述下來就是StatelessWidget通過build(BuildContext)方法構建Widget是通過StatelessElement的build()方法來完成的。想要調用build(BuildContext)必定先通過createElement方法創(chuàng)建一個StatelessElement對象。那么有一個此處就有一個問題了:Widget的createElement方法是神馬時候調用的呢?
上面粗略的分了StatelessWidget,下來再來簡略的看下StatefullWidget這個類。
abstract class StatefulWidget extends Widget {
@override
StatefulElement createElement() => StatefulElement(this);
@protected
State createState();
}
StatefulWidget的createElement方法返回了SatefulElement,且提供了一個createState()方法,大膽猜測一下createState就是在StatefulElement里面調用的,果不其然,證據(jù)如下:
StatefulElement 的構造器:
StatefulElement(StatefulWidget widget)
///調用了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的關系可以用如下圖來表示:
其構建關系的流程圖可以用如下來表示:
build(BuildContext)方法就需要先調用具體子類的createElement方法創(chuàng)建對應的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();
}