Flutter快速上車之Widget

閒魚技術發表於2018-09-03

作者:閒魚技術-意境

Flutter作為一種全新的響應式,跨平臺,高效能的移動開發框架。從開源以來,已經得到越來越多開發者的喜愛。閒魚是最早一批與谷歌展開合作,並在重要的商品詳情頁中使用上線的公司。一路走來,積累了大量的開發經驗。雖然越來越多的技術大牛在flutter世界中弄得風聲水起,但是肯定有很多的flutter小白希望能快速上手,享受flutter程式設計的樂趣。本文就是面向剛剛踏上futter的同學,從Flutter體系中最基本的一個概念widget入手學習Flutter。希望能助力每一位初學者。

可能大家要問的第一個問題是為什麼從Widget開始?

Flutter快速上車之Widget

從flutter的架構圖中不難看出widget是整個檢視描述的基礎。Flutter 的核心設計思想便是

Everything’s a Widget
複製程式碼

即一切即Widget。在flutter的世界裡,包括views,view controllers,layouts等在內的概念都建立在Widget之上。widget是flutter功能的抽象描述。所以掌握Flutter的基礎就是學會使用widget開始。

本文會從大家熟悉的UI繪製視角來介紹flutter元件和佈局的基礎知識。首先羅列了UI開發中最為常用,最為基礎的元件。下面逐一進行介紹。

Flutter快速上車之Widget

1 元件篇

1.1 Text

Text幾乎是UI開發中最為重要的元件之一了,UI上面文字的展示基本上都要靠Text元件來完成。Flutter提供了原生的Text元件。Text的配置屬性是很豐富的,屬性主要分為兩個部分一個是對齊&顯示控制相關的在Text類的屬性中,另一類是樣式相關的屬性使用單獨的類TextStyle進行控制。跟native控制元件相比(以android為例),Text的元件基本上提供了同等的能力,並且提供了更加豐富的樣式裝飾能力。詳細的屬性可以參考官方文件flutter text.

1.1.1 實踐Coding

設定文字&文字大小&顏色&行數限制&文字對齊

const Text(  "hello flutter!",
            textAlign: TextAlign.center,
            maxLines: 1,
            overflow: TextOverflow.ellipsis, // 溢位顯示。。。
            style: TextStyle(fontSize: 30.0,// 文字大小
               color: Colors.yellow),// 文字顏色
          ),
複製程式碼

效果如下:

Flutter快速上車之Widget

1.2 Image

圖片也是UI部分開發最為重要的元件之一。在能看圖隨看文字的年代,圖片是頁面展示的重中之重!Flutter同樣原生提供了Image元件。下面重點介紹一下幾個重點:

1.2.1 縮放

怎樣設定圖片顯示的縮放方式呢? Flutter中的圖片縮放是fit欄位來控制的。這是對最終圖片展示效果影響很大的一個引數,也是容易出錯的點。下面逐個分析一下flutter Image元件提供的縮放方式。

縮放屬性值在BoxFit列舉中

下面列出的圖片是flutter官方對各種縮放做的圖片示例。基本上都表述很清楚了,就整理出來供大家查閱。

屬性 縮放效果
fill
Flutter快速上車之Widget
contain
Flutter快速上車之Widget
cover
Flutter快速上車之Widget
fitWidth
Flutter快速上車之Widget
fitHeight
Flutter快速上車之Widget
none
Flutter快速上車之Widget
scaleDown
Flutter快速上車之Widget

1.2.2 圖片獲取

怎樣從各種來源載入圖片? 預設的Image元件不能直接顯示圖片,他需要一個ImageProvider來提供具體的圖片資源的(即Image中的image欄位需要賦值)。咋一看這確實非常麻煩,但是實際上ImageProvider並不需要完全重新自己實現。在Image類中目前提供了一下幾個實現好的ImageProvider,基本能滿足常見的需求。

ImageProvider 用途
Image.asset 從asset資原始檔中獲取圖片
Image.network 從網路獲取圖片
Image.file 從本地file檔案中獲取圖片
Image.memory 從記憶體中獲取圖片

Image同樣支援GIF圖片

網路請求Image是大家最常見的操作。這裡重點說明兩個點:

  • 快取

ImageCache是ImageProvider預設使用的圖片快取。ImageCache使用的是LRU的演算法。預設可以儲存1000張圖片。如果覺得快取太大,可以通過設定ImageCache的maximumSize屬性來控制快取圖片的數量。也可以通過設定maximumSizeBytes來控制快取的大小(預設快取大小10MB)。

  • CDN優化

如果想要使用cdn優化,可以通過url增加字尾的方式實現。預設實現中沒有這個點,但是考慮到cdn優化的可觀收益,建議大家利用好這個優化。

1.2.3 FadeInImage

在實際開發中,考慮到圖片載入速度可能不能達到預期。所以希望能增加漸入效果&增加placeHolder的功能。Flutter同樣提供的這樣的元件——FadeInImage。

FadeInImage也提供了從多種渠道載入圖片的能力。這塊跟上面所說差異不大。這裡不再贅述。

1.2.4 實踐Coding

  • 從網路獲取圖片保持圖片比例並儘可能大的放入
new Image.network(
            'https://gw.alicdn.com/tfs/TB1CgtkJeuSBuNjy1XcXXcYjFXa-906-520.png',
            fit: BoxFit.contain,
            width: 150.0,
            height: 100.0,
          ),
複製程式碼
  • 效果如下:

Flutter快速上車之Widget

1.3 Container

Flutter的設計思想就是完全的widget化。這也就是說連最基本的padding,Center都是widget。設想一下如果每次寫view,連padding,Center都要自己包一個元件是一種怎樣的體驗?作為一個工程師,別給只給我談思想,實際操作的操作效率也同樣非常重要。flutter 官方也意識到了這個問題,他們從實際編寫效率的角提供了一個友好高效的封裝,這就是Container!首先沒有任何疑問,Container 本身也是一個widget。但是他卻提供了對基礎widget的封裝,提高了UI基礎裝飾能力的表達效率。Container類似於android中的ViewGroup。

相信大部分的屬性大家都會感覺非常親切,結合程式碼註釋都比較容易理解,這裡就不再贅述。其中需要重點解釋一下的是:Decoration和BoxConstraints。

1.3.1 裝飾

Decoration是對Container進行裝飾的描述。其概念類似與android中的shape。一般實際場景中會使用他的子類BoxDecoration。BoxDecoration提供了對背景色,邊框,圓角,陰影和漸變等功能的定製能力。 需要注意幾個點:

  • BoxDecoration的image屬性相當於設定的是背景圖。但是image會繪製在color 和gradient之上。
  • image是需要一個DecorationImage類的實現。DecorationImage的屬性和Image元件比較類似,可以複用Image元件中的ImageProvider。

1.3.2 大小

BoxConstraints其實是對Container元件大小的描述。BoxConstraints屬性比較簡單。如果不太清楚可以研究一下盒子模型。這裡有個點需要重點說明一下:

  • 如何表達儘可能大這樣的意思?(類似於android中的match_parent)Flutter中可以使用double.infinity來做出類似的表達。

1.3.3 實踐Coding

  • 設定邊框&padding&margin&圓角&背景圖
new Container(
         alignment: Alignment.center,
         padding: const EdgeInsets.all(15.0),
         margin: const EdgeInsets.all(15.0),
         decoration: new BoxDecoration(
           border: new Border.all(
             color: Colors.red,
           ),
           image: const DecorationImage(
             image: const NetworkImage(
               'https://gw.alicdn.com/tfs/TB1CgtkJeuSBuNjy1XcXXcYjFXa-906-520.png',
             ),
             fit: BoxFit.contain,
           ),
           //borderRadius: const BorderRadius.all(const Radius.circular(6.0)),
           borderRadius: const BorderRadius.only(
             topLeft: const Radius.circular(3.0),
             topRight: const Radius.circular(6.0),
             bottomLeft: const Radius.circular(9.0),
             bottomRight: const Radius.circular(0.0),
           ),
         ),
         child: Text(''),
       ),
複製程式碼
  • 效果如下:

Flutter快速上車之Widget

1.4 手勢操作

手勢操作是最常見的UI互動操作。在Flutter中手勢識別也是一個widget!這點對新人來說又是一個新鮮的地方。通常來說可以通過GestureDetector類來完成點選事件的處理。使用時只需要將GestureDetector包裹在目標widget外面,再實現對應事件的函式即可。從點選到長按,從縮放到拖動,這個類基本上都有相應的實現。具體可以參見元件文件。

2. 佈局

頁面佈局應該是UI編寫最為根本的知識,其主要的描述的是父子元件子子元件之間的位置關係。首先我們理解一下官方文件的邏輯:

Flutter快速上車之Widget

將佈局分為單孩子和多孩子是Flutter佈局的一大特色。這點對native研發同學來說會比較新鮮。單孩子元件主要繼承自SingleChildRenderObjectWidget。這些元件能提供豐富的裝飾能力(例如container),也能提供部分特定的佈局能力(例如center)。多孩子元件繼承自MultiChildRenderObjectWidget,能提供更加豐富的佈局能力(Flex,Stack,flow),但幾乎沒有裝飾的能力。下面介紹幾個重點佈局:

2.1 Flex

Flutter在佈局上也提供了完整的Flex佈局能力。但是在Flutter官方文件中Layout Widgets,是看不到任何Flex的影子的。映入眼簾的卻是Row,Column,這些是什麼鬼?其實不難發現類似Row,Column 這樣的元件,他們的基類都是Flex。Row和Column差別是設定了不同的flex-direction。而之所這麼設計,是因為Flutter的widget從開始設計之初就考慮到UI佈局語義保持的重要性。這塊應該部分借鑑了前端的經驗,極力避免一個div搞定全部頁面的尷尬(當然flutter也可以使用Flex來做同樣的事情,但是並不建議這麼做)。 Flutter使用的Flex模型基本上跟傳統的Css類似。這塊前端同學可以快速上手。但是Flex對於客戶端同學來說是一種全新的佈局方式。Flex的基礎知識可以參看flex佈局基礎。由於篇幅有限這裡不展開敘述。這裡只重點強調一個點: 如下圖flex佈局概念如下:

Flutter快速上車之Widget
flex通過direction設定了flex的主軸方向即main axis。和主軸垂直的方向叫做cross axis。flex佈局中對子佈局的控制是從main axis 和cross axis兩個方向上進行的。例如居中有main axis居中和cross axis居中。兩者都居中才是容器的完全居中。這點是客戶端同學可能會容易弄混的地方。重點關注一下。

2.1.1 實踐Coding

ok,看完這些知識,我們實際需求角度實際操作幾個case來熟悉一下Flex。

  • 居中
new Flex(direction: Axis.horizontal,
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: <Widget>[
              new Container(
                  width: 40.0,
                  height: 60.0,
                  color: Colors.pink,
                  child: const Center(
                    child: const Text("left"),
                  )),
              new Container(
                  width: 80.0,
                  height: 60.0,
                  color: Colors.grey,
                  child: const Center(
                    child: const Text("middle"),
                  )),
              new Container(
                  width: 60.0,
                  height: 60.0,
                  color: Colors.yellow,
                  child: const Center(
                    child: const Text("right"),
                  )),
              ],
            ),
複製程式碼
  • 效果如下:

Flutter快速上車之Widget

  • weight left:right=2:1 通過設定Flexible的flex值大小完成比例設定
new Flex(
         direction: Axis.horizontal,
         mainAxisAlignment: MainAxisAlignment.spaceEvenly,
         crossAxisAlignment: CrossAxisAlignment.center,
         children: <Widget>[
           new Flexible(
             flex: 2,
             fit: FlexFit.loose,
             child: new Container(
               color: Colors.blue,
               height: 60.0,
               alignment: Alignment.center,
               child: const Text('left!',
                   textAlign: TextAlign.center,
                   style: TextStyle(color: Colors.black),
                   textDirection: TextDirection.ltr),
             ),
           ),
           new Flexible(
             flex: 1,
             fit: FlexFit.loose,
             child: new Container(
               color: Colors.red,
               height: 60.0,
               alignment: Alignment.center,
               child: const Text('right',
                   textAlign: TextAlign.center,
                   style: TextStyle(color: Colors.black),
                   textDirection: TextDirection.ltr),
             ),
           ),
         ],
       )
複製程式碼
  • 效果如下:

Flutter快速上車之Widget

2.2 stack

在實際開發中,還是需要在一些Widgets的上面再覆蓋上新的Widgets。這時候就需要層式佈局了。這種佈局在Native上,以android為例,類似於relativeLayout 或者FrameLayout。在Flutter中使用的是Stack。

實際使用中Stack中的子Widgets分為兩種:

  • positioned
    • 是包裹在元件Positioned中的元件
    • 可以通過Positioned屬性靈活定位
  • non-positioned
    • 沒有包裹在Positioned元件中
    • 需要通過父Widget Stack 的屬性來控制佈局

對於non-positioned children, 我們通過控制Stack的alignment屬性來控制對齊方式。Positioned的佈局方式類似於H5&weex中的position佈局中的absolute佈局方式。通過設定距離父元件上下左右的距離,Positioned物件能在Stack佈局中更加靈活的控制view的展現方式。

2.2.1 實踐Coding

  • 層疊佈局
new Container(
            color: Colors.yellow,
            height: 150.0,
            width: 500.0,
            child: new Stack(children: <Widget>[
              new Container(
                color: Colors.blueAccent,
                height: 50.0,
                width: 100.0,
                alignment: Alignment.center,
                child: Text('unPositioned'),
              ),
              new Positioned(
                  left: 40.0,
                  top: 80.0,
                  child: new Container(
                    color: Colors.pink,
                    height: 50.0,
                    width: 95.0,
                    alignment: Alignment.center,
                    child: Text('Positioned'),
                  )),
            ]))
複製程式碼
  • 效果如下:

Flutter快速上車之Widget

3. Visibility

當你看完Flutter Widge文件的時候,我們突然發現一個略顯尷尬的問題:元件是否顯示怎麼控制?貌似所有的元件中都沒有這個屬性!這不坑了,咋辦?

目前看方法無非如下幾個:

3.1 刪除法

核心將該真實widget或者widget樹從renderTree中移除。

具體到實踐級別主要分為兩個:

  • 單個元件‘隱藏’自己。在build方法中返回一個空的Container.
@override
Widget build(BuildContext context) {
  return isVisible
      ? Widget //真的Widget
      : new Container(); //空Widget 僅僅佔位 並不顯示
}
複製程式碼
  • 多個child

在父容器的children欄位的list中,刪除掉對應的cell。

3.2 Offstage

Offstage 是一個widget。Offstage的offstage屬性設定為true,那麼Offstage以及他的child都將不會被繪製到介面上。 sample code如下:

@override
Widget build(BuildContext context) {
  return new Offstage(
          offstage: !isVisible,
          child:child);
}
複製程式碼

3.3 透明度

設定widget的透明度,使之不可見。但是這樣的方法是副作用的。因為這個對應的widget樹是已經經過了完整的layout&paint過程,成本高。同時設定透明度本身也要耗費一定的計算資源,造成了二次浪費。需要注意的是即便變透明瞭,佔據的位置還在。大家慎重選擇使用。 sample code如下:

@override
Widget build(BuildContext context) {
  return new AnimatedOpacity(
        duration: Duration(milliseconds: 10),
        opacity: isVisible ? 1.0 : 0.0,
          child:child);
}
複製程式碼

visibility的控制還是比較麻煩的。這是Flutter設計上不符合正常習慣的一個點,需要大家重點關注。

4 生命週期

4.1 state 生命週期

widget是immutable的,發生變化的時候需要重建,所以談不上狀態。StatefulWidget 中的狀態保持其實是通過State類來實現的。State擁有一套自己的生命週期,下面做一個簡單的介紹。

名稱 狀態
initState 插入渲染樹時呼叫,只呼叫一次
didChangeDependencies state依賴的物件發生變化時呼叫
didUpdateWidget 元件狀態改變時候呼叫,可能會呼叫多次
build 構建Widget時呼叫
deactivate 當移除渲染樹的時候呼叫
dispose 元件即將銷燬時呼叫

生命週期狀態圖如下:

Flutter快速上車之Widget

幾個注意點

  • didChangeDependencies有兩種情況會被呼叫。

    • 建立時候在initState 之後被呼叫
    • 在依賴的InheritedWidget發生變化的時候會被呼叫
  • 正常的退出流程中會執行deactivate然後執行dispose。但是也會出現deactivate以後不執行dispose,直接加入樹中的另一個節點的情況。

  • 這裡的狀態改變包括兩種可能:1.通過setState內容改變 2.父節點的state狀態改變,導致孩子節點的同步變化。

4.2 App生命週期

需要指出的是如果想要知道App的生命週期,那麼需要通過WidgetsBindingObserver的didChangeAppLifecycleState 來獲取。通過該介面可以獲取是生命週期在AppLifecycleState類中。常用狀態包含如下幾個:

名稱 狀態
resumed 可見並能相應使用者的輸入
inactive 處在並不活動狀態,無法處理使用者相應
paused 不可見並不能相應使用者的輸入,但是在後臺繼續活動中

一個實際場景中的例子:

在不考慮suspending的情況下:從後臺切入前臺生命週期變化如下:

  • AppLifecycleState.inactive->AppLifecycleState.resumed;

從前臺壓後臺生命週期變化如下:

  • AppLifecycleState.inactive->AppLifecycleState.paused;

5 初學者的困惑

5.1 為什麼使用dart語言?

Dart語言對大部分開發者而言是很陌生的一種語言。google為啥會選擇如此'冷門'的語言來開發flutter?主要原因如下:

    1. dart具有jit&Aot雙重編譯執行方式。這樣就能利用JIt進行開發階段的hot reload開發,提升研發效率。同時在最終release版本中使用aot將dart程式碼直接變成目標平臺的指令集程式碼。簡單高效,最大限度保障了效能。
    1. dart針對flutter中頻繁建立銷燬Widget的場景做了專門的gc優化。通過分代無鎖垃圾回收器,將gc對效能的影響降至最低。
    1. dart語言在語法上面是類java的,易學易用。

5.2 為什麼widget都是immutable?

個人認為是兩個主要的點:

  • 提高渲染效率 flutte在頁面渲染上面的核心思想是simple is fast!將widget設計成immutable,所以在資料變化時,flutter選擇重建widget樹的方式進行資料更新。採用這樣方式的好處是框架不需要關心資料影響的範圍,簡單高效。缺點就是對GC會造成壓力。
  • 元件描述的複用 既然widget都是不可變的。那widget可以以較低成本進行復用。在一個真實的渲染樹中可能存在同一個widget渲染樹中不同節點的情況。

5.3 widget是view麼?

可能剛開始接觸flutter的同學最疑惑的一個問題就是widget和view的關係了。那麼簡單分析一下: widget是對頁面UI的一種描述。他功能類有點似於android中的xml,或者web中的html。widget在渲染的時候會轉化成element。Element相比於widget增加了上下文的資訊。element是對應widget,在渲染樹的例項化節點。由於widget是immutable的,所以同一個widget可以同時描述多個渲染樹中的節點。但是Element是描述固定在渲染書中的某一個特定位置的點。簡單點說widget作為一種描述是可以複用的,但是element卻跟需要繪製的節點一一對應。那element是最終渲染的view麼?抱歉,還不是。element繪製時會轉化成rendObject。RendObject才是真正經過layout和paint並繪製在螢幕上的物件。在flutter中有三套渲染相關的tree,分別是:widget tree, element tree & rendObject tree。三者的渲染流程如下:

Flutter快速上車之Widget

那可能有人會問,為什麼需要增增加中間這層的Element tree?

Flutter快速上車之Widget
flutter是響應式的框架。在某一時刻頁面的佈局,可能受不同的輸入源的影響。Element這層實際上做了對某一時刻事件的彙總,在將真正需要修改的部分同步到真實的rendObject tree上。這麼做有兩個好處:

  • 1.不需要直接操作UI,改為通過資料驅動檢視。程式碼表達可以更加精煉。
  • 2.最大層度降低對最終真實檢視(rendObject tree)的修改,提高頁面渲染效率。

5.4 StatelessWidget 和 StatefulWidget的區別

StatelessWidget是狀態不可變的widget。初始狀態設定以後就不可再變化。如果需要變化需要重新建立。StatefulWidget可以儲存自己的狀態。那問題是既然widget都是immutable的,怎麼儲存狀態?其實Flutter是通過引入了State來儲存狀態。當State的狀態改變時,能重新構建本節點以及孩子的Widget樹來進行UI變化。注意:如果需要主動改變State的狀態,需要通過setState()方法進行觸發,單純改變資料是不會引發UI改變的。

6.聯絡我們

本文詳細解釋了基礎元件的用法,也解答了一些初學者的疑惑。希望能給剛踏上flutter學習之路的人一些幫助。如果對文字的內容有疑問或指正,歡迎告知我們。

閒魚技術團隊是一隻短小精悍的工程技術團隊。我們不僅關注於業務問題的有效解決,同時我們在推動打破技術棧分工限制(android/iOS/Html5/Server 程式設計模型和語言的統一)、計算機視覺技術在移動終端上的前沿實踐工作。作為閒魚技術團隊的軟體工程師,您有機會去展示您所有的才能和勇氣,在整個產品的演進和使用者問題解決中證明技術發展是改變生活方式的動力。 簡歷投遞:guicai.gxy@alibaba-inc.com

7.引用:

相關文章