Flutter開發實戰初級(一)ListView詳解

iOS一葉發表於2021-08-16

Flutter開發實戰初級(一)ListView詳解

Flutter開發實戰初級(一)ListView詳解

  • 本篇部落格主要以一個demo的形式講解ListView的使用

原始碼獲取

先來看一下效果圖,下面是執行在iphone11上面的:

iphone11上的ListView效果圖

ListView 知識點

在Flutter中,用ListView來顯示列表項,支援垂直和水平方向展示,通過一個屬性我們就可以控制其方向
1.水平的列表
2.垂直的列表
3.資料量非常大的列表
4.內建的ListTile(挺好用的)

ListView Demo

  1. 執行效果:

在這裡插入圖片描述

demo獲取

1. 新建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',
  )

];
複製程式碼

2. 新建carlistview.dart 用來展示列表資料

  1. 定義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
        );
    複製程式碼

    }

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,),
        ],
      ), //每人一輛跑車
    );
  }
複製程式碼

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. 知識點講解

main函式使用了(=>)符號, 這是Dart中單行函式或方法的簡寫。

該應用程式繼承了 StatelessWidget,這將會使應用本身也成為一個widget。 在Flutter中,大多數東西都是widget,包括對齊(alignment)、填充(padding)和佈局(layout)

Scaffold 是 Material library 中提供的一個widget, 它提供了預設的導航欄、標題和包含主螢幕widget樹的body屬性。widget樹可以很複雜。

widget的主要工作是提供一個build()方法來描述如何根據其他較低階別的widget來顯示自己。

使用外部包

4.1 Widget

4.1.1 Widget基本概念

Stateless widgets 是不可變的, 這意味著它們的屬性不能改變 - 所有的值都是最終的.

Stateful widgets 持有的狀態可能在widget生命週期中發生變化. 實現一個 stateful widget 至少需要兩個類:

  1. 一個 StatefulWidget類。

  2. 一個 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的子類。

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 深入探索
  1. 首先我們需要明白,Widget 是什麼?這裡有一個 “總所周知” 的答就是:Widget並不真正的渲染物件 。是的,事實上在 Flutter 中渲染是經歷了從 Widget 到 Element 再到 RenderObject 的過程。

  2. 我們都知道 Widget 是不可變的,那麼 Widget 是如何在不可變中去構建畫面的?上面我們知道,Widget 是需要轉化為 Element 去渲染的,而從下圖註釋可以看到,事實上 Widget 只是 Element 的一個配置描述 ,告訴 Element 這個例項如何去渲染。

在這裡插入圖片描述

那麼 Widget 和 Element 之間是怎樣的對應關係呢?從上圖註釋也可知: Widget 和 Element 之間是一對多的關係 。實際上渲染樹是由 Element 例項的節點構成的樹,而作為配置檔案的 Widget 可能被複用到樹的多個部分,對應產生多個 Element 物件。

  3.那麼RenderObject 又是什麼?它和上述兩個的關係是什麼?從原始碼註釋寫著 An object in the render tree 可以看出到        RenderObject 才是實際的渲染物件,而通過 Element 原始碼我們可以看出:Element 持有 RenderObject 和 Widget。 

在這裡插入圖片描述

再結合下圖,可以大致總結出三者的關係是:配置檔案 Widget 生成了 Element,而後建立 RenderObject 關聯到 Element 的內部 renderObject 物件上,最後Flutter 通過 RenderObject 資料來佈局和繪製。 理論上你也可以認為 RenderObject 是最終給 Flutter 的渲染資料,它儲存了大小和位置等資訊,Flutter 通過它去繪製出畫面。 

在這裡插入圖片描述

   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 樹並不會完全重新建立。

8.看到這,說個題外話:那一般我們可以怎麼獲取佈局的大小和位置呢?

首先這裡需要用到我們前文中提過的 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<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的整體解構可以用如下圖表示:

在這裡插入圖片描述

先來看看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();
  }
複製程式碼

原始碼獲取

收錄

相關文章