Flutter從入門到能寄幾玩兒

  • 2019 年 10 月 10 日
  • 筆記

國慶後面兩天在家學習整理了一波flutter,基本把能擼過能看到的程式碼都過了一遍,此文篇幅較長,建議保存(star)再看。傳送門: Nealyang personal blog

前言

畢竟前端出生,找(qi)到(shi)了(bing)感(mei)覺(ru)後(men),其實就是一個UI框架,只不過他的引擎基於C++,底層基於Skia渲染,DartVM虛擬機以及Text and so on…

2018年6月21日Google發布Flutter首個release預覽版,作為Google baba大力推出的一種全新的響應式,跨平台,高性能的移動開發框架,勢必會火一波~沒別的,就是因為Google baba,當然,從目前看來也的確越來越火了。 Questions tagged [flutter]

img

本文我們從介紹flutter基本概念到梳理常用Widget到常用app demos編寫到~放棄~,希望可以幫助每一個像我一樣的初學者。有誤地方還望大神不吝賜教~

img

國際慣例,吹一波先~

直接移步Flutter官宣ppt

關於Dart

作為Flutter入門文章,Dart必然少不了,當然,作為Flutter入門篇,Dart預發基礎必然不會過多介紹。

Dart入門傳送門:Dart or Dart2,或者從Dart中文網中學習也不錯其實.

這裡我們說說為啥是Dart。

許多語言科學家認為,一個人說的自然語言會影響他們的思維方式。早起Flutter團隊評估了十多種語言最終選擇了Dart,因為它符合他們構建用戶介面的方式

  • Dart 是AOT 編譯的,編譯成快速可預測的本地程式碼,使Flutter幾乎都可以使用Dart編寫,這不僅使Flutter變的更快,而且幾乎所有的東西都可以訂製
  • Dart也可以JIT編譯,開發周期異常快,工作流顛覆常規,也使得Flutter可以實現非常Diao的有狀態熱重載(別扯別的,人家是出生自帶哇)
  • Dart可以更輕鬆地創建以60fps運行的流暢動畫和轉場。Dart可以在沒有鎖的情況下進行對象分配和垃圾回收。就像JavaScript一樣,Dart避免了搶佔式調度和共享記憶體(因而也不需要鎖)。由於Flutter應用程式被編譯為本地程式碼,因此它們不需要在領域之間建立緩慢的橋樑(例如,JavaScript到本地程式碼)。它的啟動速度也快得多
  • Dart使Flutter不需要單獨的聲明式布局語言,如JSX或XML,或單獨的可視化介面構建器,因為Dart的聲明式編程布局易於閱讀和可視化。所有的布局使用一種語言,聚集在一處,Flutter很容易提供高級工具,使布局更簡單
  • Dart對於IOS、Android、Web FE來說,都還比較友好。

具體選擇Dart的原因,以及向了解Dart的,移步為什麼Flutter會選擇 Dart

關於Flutter

剛開始接觸flutter心中難免會有疑惑,不是已經有RN、Weex等各種跨平台移動開發 了,flutter優勢在哪呢?看我從網上盜的圖!

img

圖片來源:簡書 作者:AWeiLoveAndroidAWeiLoveAndroid

Everything is Widget

有一種說法認為函數式語言和命令式語言的不同在於命令式語言是給電腦下達指令而函數式語言是向電腦描述邏輯。這種思路在Flutter UI中得到了體現。Flutter不提倡去操作UI,它當然也基本不會提供操作View的API,比如我們常見的類似TextView.setText(),Button.setOnClick()這種是不會有的。對介面的描述是可以數據化的(類似XML,JSON等),而對介面的操作是很難數據化的,這很重要,響應式需要方便可持續的將數據映射成介面。

在Flutter中用Widget來描述介面,Widget只是View的「配置資訊」,編寫的時候利用Dart語言一些聲明式特性來得到類似結構化標記語言的可讀性。Widget根據布局形成一個層次結構。每個widget嵌入其中,並繼承其父項的屬性。沒有單獨的「應用程式」對象,相反,根widget扮演著這個角色。在Flutter中,一切皆為Widget,甚至包括css樣式。

<div class="greybox">      Lorem ipsum  </div>    .greybox {        background-color: #e0e0e0; /* grey 300 */        width: 320px;        height: 240px;        font: 900 24px Georgia;      }  

在flutter中我們編寫為

var container = new Container( // grey box    child: new Text(      "Lorem ipsum",      style: new TextStyle(        fontSize: 24.0        fontWeight: FontWeight.w900,        fontFamily: "Georgia",      ),    ),    width: 320.0,    height: 240.0,    color: Colors.grey[300],  );  

可以看到我們css樣式中的font定義的樣式,在flutter中,需要new TextStyle,TextStyle就是一個Widget,並且樣式必須作用與Container中的child:text上,不存在web中樣式的繼承。

剛開始接觸的同學就類比於react中扯的,一切皆為組件吧,其實widget是對頁面UI的一種描述。他功能類有點似於android中的xml,react中的jsx。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。三者的渲染流程如下:

img

有沒有一種 jsx -> virtual Dom -> real dom滴感覺呢~

img

咳咳,後面會介紹基礎常用的Widget配合一些demo,大家可能對這個體會就會更加清晰一些。

組合大於繼承

Flutter中很多借鑒了react的思想,甚至包括後面會說到的state。

Widget本身通常由許多更小的、單一的小小widget組成,甚至小到它單一下來並沒有什麼作用的感覺,這些Widget几几組合形成一個強大的自定義的大大Widget。

比如一個Container,對於Web FE來說可能就是個div,而他就是由很多的widget組成,這些widget負責布局、繪製、定位、大小等。我們可以使用各種姿勢來組合他們而不是繼承他們。類層次結構很淺且很寬,可以最大限度的增加可能組合的數量

img

框架結構

img

上面的圖片是Flutter分層框架結構圖,對大部分開發者而言,最常用的是Widgets層,螢幕上可見與不可見的元素都由Widgets層實現,這些元素被稱為Widget。在Widgets層在上層,有兩個現成的Widget庫,Material庫即Material Design的Widget庫,Material Design是Google I/O 2014發布的設計語言,目前成為統一Android Mobile、Android Table、Desktop Chrome等平台的設計語言規範。Cupertino庫則是一個模仿iOS設計風格的Widget庫。

底層是Flutter Engine虛擬機,在這一層次中需要了解一下的是Skia,Skia是Google研發的包括圖形、文本、影像、動畫等多方面的圖形引擎,不僅用於Google Chrome瀏覽器,Android系統也採用Skia作為繪圖處理引擎。

GPU渲染:

img

state生命周期:

img

作為初學者看上面的圖有點雲里霧裡的,且先做到心裡有數~

Flutter走馬觀花

關於Flutter環境問題這裡不再贅述 此後~大量程式碼來襲

基礎Widget之material版Hello world

國際慣例,hello world

import 'package:flutter/material.dart';    class MyAppBar extends StatelessWidget{    MyAppBar({this.title});//    final Widget title;      @override    Widget build(BuildContext context){      return new Container(        height: 56.0,        padding: const EdgeInsets.symmetric(horizontal:8.0),        decoration: new BoxDecoration(          color:Colors.blue[400]        ),        child: Row(          children: <Widget>[            new IconButton(              icon:new Icon(Icons.menu),              tooltip:'Navigation menu',              onPressed: (){                print('點擊Menu');              },            ),            new Expanded(              child:new Center(                child:title              )            ),            new IconButton(              icon:Icon(Icons.search),              tooltip:'Search',              onPressed: (){                print('點擊搜索按鈕');              },            )          ],        ),      );    }  }    class MyScaffold extends StatelessWidget{    @override    Widget build(BuildContext context){      return Material(        child: new Column(          children:<Widget>[            new MyAppBar(              title:new Text(                'Hello World',                style:Theme.of(context).primaryTextTheme.title               ),            ),            new Expanded(              child:new Center(                child:Text('Hello World!!!')              )            )          ]        ),      );    }  }    void main(){    runApp(      new MaterialApp(        title:'My app',        home:new MyScaffold()      )    );  }  

img

程式碼地址:https://github.com/Nealyang/flutter

這個UI的確有些對不起人了,上面的title被擋住了。且先不去適配,後面我們使用Material提供的Scaffold即可

第一個例子,重點說下程式碼(用過的Widget記住):

  • 一切都是Widget,且Widget前面的new可有可無。
  • 類MyAppBar和MyScaffold中使用了Container、Row、Column、Text、IconButton、Icon、BoxDecoration、Center、Expanded等常用Widget
  • Container一個擁有繪製、定位、調整大小的 widget。類似於div,我們可以用它來創建矩形視圖,container 可以裝飾為一個BoxDecoration, 如 background、一個邊框、或者一個陰影。Container 也可以具有邊距(margins)、填充(padding)和應用於其大小的約束(constraints)。另外, Container可以使用矩陣在三維空間中對其進行變換。
  • Row、Column其實就是flex布局中的flex-direction
  • Expanded它會填充尚未被其他子項佔用的的剩餘可用空間。Expanded可以擁有多個children。然後使用flex參數來確定他們佔用剩餘空間的比例。更多細節可以參看:flutter控制項Flexible和 Expanded的區別
  • 先定義了一個MyAppBar的類,構造函數中接受一個Widget的title,其實我們也可以接受String title然後在類中自己去new Title(title)
  • runApp函數接受給定的Widget並使用其作為widget根。
  • widget的主要工作是實現一個build函數,用以構建自身。一個widget通常由一些較低級別widget組成。Flutter框架將依次構建這些widget,直到構建到最底層的子widget時,這些最低層的widget通常為RenderObject,它會計算並描述widget的幾何形狀。

基本交互之material版Hello world

import 'package:flutter/material.dart';    void main() => runApp(new MyApp());    class MyApp extends StatelessWidget {    // app的根Widget    @override    Widget build(BuildContext context) {      return new MaterialApp(        title: 'Flutter Demo',        theme: new ThemeData(          // 這是設置的app主題          // 運行後你可以看到app有一個藍色的toobar,並且在不退出app的情況下修改程式碼會熱更新          primarySwatch: Colors.blue,        ),        home: new MyHomePage(title: 'Flutter Demo Home Page'),      );    }  }    class MyHomePage extends StatefulWidget {    MyHomePage({Key key, this.title}) : super(key: key);    // 這是應用中一個基類,繼承自StateFulWidget,意味著這個類擁有一個state對象,該對象里的一些欄位會影響app的UI  // 這個類是state的一些配置項。通過構造函數來獲取值,這個值一般在State中消費,並且使用final關鍵字。其實類似於react中的defaultProps      final String title;      @override    _MyHomePageState createState() => new _MyHomePageState();  }    class _MyHomePageState extends State<MyHomePage> {    int _counter = 0;      void _incrementCounter() {      setState(() {        // setState方法告訴Flutter,這個State中有些值發生了變化,以便及時將新值更新到UI上,        // 如果我不通過setState更改_count欄位,那麼Flutter並不會調用build匿名函數去更新介面        _counter++;      });    }      @override    Widget build(BuildContext context) {      // build方法會在每次setState的時候重新運行,例如上面的_incrementCounter方法被調用      //Flutter已經被優化了重新構建的方法,所以你只會去更新需要去更新的部分,不必去單獨更新裡面的一些更細小的widget,類似於React中diff      return new Scaffold(        appBar: new AppBar(          // 這裡我們使用從App.build方法中初始化MyHomePage時候傳入的title值來設置我們的title          title: new Text(widget.title),        ),        body: new Center(          // Center是一個布局Widget,他只有一個child(區分row or cloumn等是children),並且會將child的widget居中顯示          child: new Column(            // Column也是一個布局widget,他可以有多個子widget            // Column 有很多的屬性去控制他的大小以及子widget的位置,這裡我們使用mainAxisAlignment來讓children在垂直線上居中,            // 這裡的主軸就是垂直的,因為Column就是垂直方向的,這裡可以大概想像為display:flex,flex-directions:column,align-item,justifyContent。。。            mainAxisAlignment: MainAxisAlignment.center,            children: <Widget>[              new Text(                'Hello World!',                style:TextStyle(                  fontSize:24.0,                  color: Colors.redAccent,                  decorationStyle:TextDecorationStyle.dotted,                  fontWeight: FontWeight.bold,                  fontStyle: FontStyle.italic,                  decoration: TextDecoration.underline                )              ),              new Text(                'You have pushed the button this many times:',              ),              new Text(                '$_counter',                style: Theme.of(context).textTheme.display1,              ),            ],          ),        ),        floatingActionButton: new FloatingActionButton(          onPressed: _incrementCounter,          tooltip: 'Increment',          child: new Icon(Icons.add),        ),//最後這個逗號有利於格式化程式碼      );    }  }  

img

注釋上基本已經加了,這裡重點說下,StatefulWidget和StatelessWidget.

  • Stateless widgets 是不可變的,這意味著它們的屬性不能改變——所有的值都是 final
  • Stateful widgets 持有的狀態可能在 widget 生命周期中發生變化,實現一個 stateful widget 至少需要兩個類:1)一個 StatefulWidget 類;2)一個 State 類,StatefulWidget 類本身是不變的,但是 State 類在 widget 生命周期中始終存在
  • 如果需要變化需要重新創建。StatefulWidget可以保存自己的狀態。那問題是既然widget都是immutable的,怎麼保存狀態?其實Flutter是通過引入了State來保存狀態。當State的狀態改變時,能重新構建本節點以及孩子的Widget樹來進行UI變化。注意:如果需要主動改變State的狀態,需要通過setState()方法進行觸發,單純改變數據是不會引發UI改變的。

還有關於key的部分這裡就不做介紹了,其實就類似與react中key的概念,便於diff,提高效率的。 具體可以查看 Key

到這裡,我們看到了Flutter的一些基本用法,Widget的套用、樣式的編寫、事件的註冊,如果再學習下一些路由、請求、快取是不是就可以自己開發APP了呢

img

OK,強化下編寫介面,咱再來些demo吧~

布局Widget

img

自己寫的後,發現跟官網實現方式不同,程式碼地址

具體實現可以參照官網教程

這裡不再贅述,下面我們說下對於布局的理解和感受以及常用布局widget。

從一個前端的角度來說,說到畫介面,可能還是對布局這塊比較敏感

img

img

)

當然,這裡我們還是說下目前常用的flex布局,基本拿到頁面從大到小拆分後就是如上圖。

所以Widget布局其實也就是Row和Column用的最多,然後由於Flutter一切皆為組件的理念,可能會需要用到別的類css布局的Widget,譬如:Container。其實咱就理解為塊元素吧!

下面簡單演示下一些常用的Widget,這裡就不在贅述Row和Column了。傳送門:布局Widget

Container

可以添加padding、margin、border、background color、通常用於裝飾其他Widget

img

程式碼鏈接 Nealyang/flutter

class MyHomePage extends StatelessWidget{    @override    Widget build(BuildContext context){      Container cell (String imgSrc){        return new Container(          decoration: new BoxDecoration(            border:Border.all(width:6.0,color:Colors.black38),            borderRadius: BorderRadius.all(const Radius.circular(8.0))          ),          child: Image.asset(            'images/$imgSrc',            width: 180.0,            height: 180.0,            fit: BoxFit.cover,          ),        );      }        return Container(        padding: const EdgeInsets.all(10.0),        color: Colors.grey,        child: new Column(          mainAxisSize: MainAxisSize.min,          children:<Widget>[            new Container(              margin: const EdgeInsets.only(bottom:10.0),              child: new Row(                mainAxisAlignment: MainAxisAlignment.spaceAround,                children:<Widget>[                  cell('1.jpg'),                  cell('2.jpg')                ]              ),            ),            new Container(              margin: const EdgeInsets.only(bottom:10.0),              child: new Row(                mainAxisAlignment: MainAxisAlignment.spaceAround,                children:<Widget>[                  cell('3.jpg'),                  cell('4.jpg')                ]              ),            ),          ]        ),      );    }  }  

該布局中每個影像使用一個Container來添加一個圓形的灰色邊框和邊距。然後使用容器將列背景顏色更改為淺灰色。

GridView

可滾動的網格布局,理解為display:grid

GridView提供兩個預製list,當GridView檢測到內容太長時,會自動滾動。如果需要構建自定義grid,可是使用GridView.countGridView.extent來指定允許設置的列數以及指定項最大像素寬度。

img

程式碼鏈接 Nealyang/flutter

List<Container> _buildGridTileList(int count) {      return new List<Container>.generate(        count,        (int index) =>            new Container(child: new Image.asset('images/${index+1}.jpg')));  }    Widget buildGrid() {    return new GridView.extent(        maxCrossAxisExtent: 150.0,        padding: const EdgeInsets.all(4.0),        mainAxisSpacing: 4.0,        crossAxisSpacing: 4.0,        children: _buildGridTileList(10));  }    class MyHomePage extends StatelessWidget{    @override    Widget build(BuildContext context){      return  new Center(          child: buildGrid(),        );    }  }  

如上是指定maxCrossAxisExtent,我們可以直接去指定列數,例如官網的程式碼實例:

new GridView.count(    primary: false,    padding: const EdgeInsets.all(20.0),    crossAxisSpacing: 10.0,    crossAxisCount: 3,    children: <Widget>[      const Text('He'd have you all unravel at the'),      const Text('Heed not the rabble'),      const Text('Sound of screams but the'),      const Text('Who scream'),      const Text('Revolution is coming...'),      const Text('Revolution, they...'),    ],  )  

通過crossAxisCount直接指定列數。

Stack

層疊布局,position為absolute的感jio~

使用Stack來組織需要重疊的widget。widget可以完全或部分重疊底部widget。子列表中的第一個widget是base widget; 隨後的子widget被覆蓋在基礎widget的頂部。Stack的內容不能滾動。有點類似於weex中的設置了absolute的感覺。底部組件永遠在上面組件的上面。

ListView

可滾動的長列表,可以水平或者垂直。

Card

Material風格組件,卡片,AntD啥的組件庫經常會出現的那種組件。

在flutter中,Card具有圓角和陰影,更改Card的elevation屬性可以控制陰影效果。

ListTile

Material風格組件,我理解為常用的列表Item的樣式,最多三行文字,可選的行前、行尾的圖標

img

程式碼鏈接 Nealyang/flutter

總結

從目前我個人淺薄的Flutter技能來說,最大的困難可能是找不到合適的Widget去實現想要的布局或者效果,甚至包括css樣式作用於那個Widget,譬如Opacity是一個widget而不是一個css樣式~

所以對於Flutter,我們還是要多折騰,多些demo,類似網上很多仿xxxApp等~

對於畫介面,更多的還可以參看下官網教程:Flutter for Web開發者

一切才剛剛開始

Flutter一切基於Widget,搞定widget就好比,搞定英語單詞一樣,單詞、片語都賊6了還怕英語?

別急別急,借用張晟哥的圖來給大家消消火氣~

widgets

所以說,Flutter有一個龐大的組件體系,需要花費非常多的時間去梳理。

!更重要的是:多實踐

本來最後一章是自己寫的一個demo的講解~

可惜時間評估不準確,漏評估了假期惰性。。。考慮篇幅,後面補上仿XXX的Demo吧~~

img

參考鏈接 && 好文推薦

  • Flutter的原理及美團的實踐
  • Flutter從入門到進階
  • Flutter快速上車之Widget
  • 深入了解Flutter介面開發
  • flutter控制項Flexible和 Expanded的區別
  • 常用Widget
  • 閑魚專家詳解:Flutter React編程範式實踐
  • Flutter 布局詳解
  • 在Flutter中構建布局
  • Flutter中文網

Demo 推薦

  • 基於Flutter的俄羅斯方塊小遊戲
  • 基於Google Flutter的開源中國客戶端
  • 實時聊天APP
  • 超完整的Flutter項目 維護個人Github
  • 山寨掘金
  • 開眼影片