flutter 入坑分享

簡介

Flutter 是 Google 推出並開源的移動端開發框架(基於「Dart」語言)。使用 Flutter 開發的APP可以同時運行在 IOS 與 Android 平臺上。並且 Flutter 默認帶有 Material 風格 與 Cupertino 風格的主題包(前者Android,後者IOS),可以快速開發一個IOS 風格或者 Android 風格的…Demo…

  • 跨平臺

Flutter 不使用 WebView 也不使用操作系統的原生控件,而是自己有用一個 高性能 的渲染引擎,可以非常高效的進行組件繪製UI渲染。這樣 Flutter 可以保證在 IOS 與 Android 上的UI表現一致性 ,開發者無需過多關注平臺差異性上的問題。對於初創公司來說,前期節約開發成本就是最好的融資。。。

  • 高性能

React Native (以下簡稱RN)的跨平臺不同的是,RN是會將JS編寫的對應組件轉換爲原生組件去渲染,而 Flutter 是基於最底層 Skia 的圖形庫去渲染(我覺得有點類似於 DOM 中的 canvas , 從平臺上得到一個畫布,自己在畫布上去渲染),所有的渲染都有 Skia 來完成。

>Skia 延伸…
  >
  >Flutter使用Skia作爲其2D渲染引擎,Skia是Google的一個2D圖形處理函數庫,包含字型、座標轉換,以及點陣圖都有高效能且簡潔的表現,Skia是跨平臺的,並提供了非常友好的API,目前Google Chrome瀏覽器和Android均採用Skia作爲其繪圖引擎,值得一提的是,由於Android系統已經內置了Skia,所以Flutter在打包APK(Android應用安裝包)時,不需要再將Skia打入APK中,但iOS系統並未內置Skia,所以構建iPA時,也必須將Skia一起打包,這也是爲什麼Flutter APP的Android安裝包比iOS安裝包小的主要原因。

正是因爲基於自己的渲染機制,不需要與原生平臺之間頻繁通信,才體現出來他的高效率、高性能。Flutter 的佈局、渲染都是 Dart 直接控制,在一些交互中,比如滑動的時候它的高性能就會體現出來。而RN在這方面的渲染則是與原生平臺進行通信,不斷的進行信息同步,這部分的開銷放到手機上還是很大的。

而且在渲染層,Flutter 底層也有一個類似虛擬DOM的組件,在UI進行變化後,會進行diff算法。

  • 開發高效率

Flutter 在開發的時候有一個特點,熱重載。 就像在webpack 與 瀏覽器,在編輯器中保存後,界面立馬就能看到變化。Flutter 也是這樣,當將 APP 在虛擬容器中或者真機設備中調試時,保存後,APP會立刻響應。節省了大量時間。

Dart 初步瞭解

因爲 Flutter 是基於 Dart 語言開發的,所以我們多多少少也要了解下 Dart 這玩意怎麼寫,他的語法與結構是個怎樣的。雖然官網的 Demo 有提到說:「如果您熟悉面向對象和基本編程概念(如變量、循環和條件控制),則可以完成本教程,您無需要了解Dart或擁有移動開發的經驗。」emmmm… 純屬扯淡…

如果不瞭解 Dart,那也僅限於看 Demo 是怎麼寫的…

Dart 出自Google。是一種面向對象編程的強類型語言,語法有點像 Java 與 JavaScript 的集合體。

官方學習資料

以下是使用 Flutter 需要掌握的 Dart 基礎語法:

(以下內容摘抄來至 官網文檔 , 沒必要細看,可快速的過一遍,只做瞭解。)

變量聲明

  1. var

類似於JavaScript中的var,它可以接收任何類型的變量,但最大的不同是Dart中var變量一旦賦值,類型便會確定,則不能再改變其類型,如:

dart    var t;    t="hi world";    // 下面代碼在dart中會報錯,應爲變量t的類型已經確定爲String,    // 類型一旦確定後則不能再更改其類型。    t=1000;

上面的代碼在JavaScript是沒有問題的,前端開發者需要注意一下,之所以有此差異是因爲Dart本身是一個強類型語言,任何變量都是有確定類型的,在Dart中,當用var聲明一個變量後,Dart在編譯時會根據第一次賦值數據的類型來推斷其類型,編譯結束後其類型就已經被確定,而JavaScript是純粹的弱類型腳本語言,var只是變量的聲明方式而已。

  1. dynamicObject

DynamicObjectvar功能相似,都會在賦值時自動進行類型推斷,不同在於,賦值後可以改變其類型,如:

dart    dynamic t;    t="hi world";    //下面代碼沒有問題    t=1000;

Object 是dart所有對象的根基類,也就是說所有類型都是Object的子類,所以任何類型的數據都可以賦值給Object聲明的對象,所以表現效果和dynamic相似。

  1. finalconst

如果您從未打算更改一個變量,那麼使用 finalconst,不是var,也不是一個類型。 一個 final 變量只能被設置一次,兩者區別在於:const 變量是一個編譯時常量,final變量在第一次使用時被初始化。被final或者const修飾的變量,變量類型可以省略,如:

dart    //可以省略String這個類型聲明    final str = "hi world";    //final str = "hi world";     const str1 = "hi world";    //const String str1 = "hi world";

函數

Dart是一種真正的面向對象的語言,所以即使是函數也是對象,並且有一個類型Function。這意味着函數可以賦值給變量或作爲參數傳遞給其他函數,這是函數式編程的典型特徵。

  1. 函數聲明

dart    bool isNoble(int atomicNumber) {      return _nobleGases[atomicNumber] != null;    }

dart函數聲明如果沒有顯示申明返回值類型時會默認當做dynamic處理,注意,函數返回值沒有類型推斷:

dart    typedef bool CALLBACK();        //不指定返回類型,此時默認爲dynamic,不是bool    isNoble(int atomicNumber) {      return _nobleGases[atomicNumber] != null;    }        void test(CALLBACK cb){       print(cb());     }    //報錯,isNoble不是bool類型    test(isNoble);

  1. 對於只包含一個表達式的函數,可以使用簡寫語法

dart    bool isNoble (int atomicNumber )=> _nobleGases [ atomicNumber ] != null ;

  1. 函數作爲變量

dart    var say= (str){      print(str);    };    say("hi world");

  1. 函數作爲參數傳遞

dart    void execute(var callback){        callback();    }    execute(()=>print("xxx"))

  1. 可選的位置參數

包裝一組函數參數,用[]標記爲可選的位置參數:

dart    String say(String from, String msg, [String device]) {      var result = '$from says $msg';      if (device != null) {        result = '$result with a $device';      }      return result;    }

下面是一個不帶可選參數調用這個函數的例子:

dart    say('Bob', 'Howdy'); //結果是: Bob says Howdy

下面是用第三個參數調用這個函數的例子:

dart    say('Bob', 'Howdy', 'smoke signal'); //結果是:Bob says Howdy with a smoke signal

  1. 可選的命名參數

定義函數時,使用{param1, param2, …},用於指定命名參數。例如:

dart    //設置[bold]和[hidden]標誌    void enableFlags({bool bold, bool hidden}) {        // ...     }

調用函數時,可以使用指定命名參數。例如:paramName: value

dart    enableFlags(bold: true, hidden: false);

可選命名參數在Flutter中使用非常多。

異步支持

Dart類庫有非常多的返回Future或者Stream對象的函數。 這些函數被稱爲異步函數:它們只會在設置好一些需要消耗一定時間的操作之後返回,比如像 IO操作。而不是等到這個操作完成。

asyncawait關鍵詞支持了異步編程,運行您寫出和同步代碼很像的異步代碼。

  1. Future

Future與JavaScript中的Promise非常相似,表示一個異步操作的最終完成(或失敗)及其結果值的表示。簡單來說,它就是用於處理異步操作的,異步處理成功了就執行成功的操作,異步處理失敗了就捕獲錯誤或者停止後續操作。一個Future只會對應一個結果,要麼成功,要麼失敗。

由於本身功能較多,這裏我們只介紹其常用的API及特性。還有,請記住,Future 的所有API的返回值仍然是一個Future對象,所以可以很方便的進行鏈式調用。

  1. Future.then

爲了方便示例,在本例中我們使用Future.delayed 創建了一個延時任務(實際場景會是一個真正的耗時任務,比如一次網絡請求),即2秒後返回結果字符串"hi world!",然後我們在then中接收異步結果並打印結果,代碼如下:

Future.delayed(new Duration(seconds: 2),(){
   return "hi world!";
}).then((data){
   print(data);
});
  1. Future.catchError

如果異步任務發生錯誤,我們可以在catchError中捕獲錯誤,我們將上面示例改爲:

Future.delayed(new Duration(seconds: 2),(){
   //return "hi world!";
   throw AssertionError("Error");  
}).then((data){
   //執行成功會走到這裏  
   print("success");
}).catchError((e){
   //執行失敗會走到這裏  
   print(e);
});

在本示例中,我們在異步任務中拋出了一個異常,then的回調函數將不會被執行,取而代之的是 catchError回調函數將被調用;但是,並不是只有 catchError回調才能捕獲錯誤,then方法還有一個可選參數onError,我們也可以它來捕獲異常:

Future.delayed(new Duration(seconds: 2), () {
    //return "hi world!";
    throw AssertionError("Error");
}).then((data) {
    print("success");
}, onError: (e) {
    print(e);
});
  1. Future.whenComplete

有些時候,我們會遇到無論異步任務執行成功或失敗都需要做一些事的場景,比如在網絡請求前彈出加載對話框,在請求結束後關閉對話框。這種場景,有兩種方法,第一種是分別在thencatch中關閉一下對話框,第二種就是使用FuturewhenComplete回調,我們將上面示例改一下:

Future.delayed(new Duration(seconds: 2),(){
   //return "hi world!";
   throw AssertionError("Error");
}).then((data){
   //執行成功會走到這裏 
   print(data);
}).catchError((e){
   //執行失敗會走到這裏   
   print(e);
}).whenComplete((){
   //無論成功或失敗都會走到這裏
});
  1. Future.wait

有些時候,我們需要等待多個異步任務都執行結束後才進行一些操作,比如我們有一個界面,需要先分別從兩個網絡接口獲取數據,獲取成功後,我們需要將兩個接口數據進行特定的處理後再顯示到UI界面上,應該怎麼做?答案是Future.wait,它接受一個Future數組參數,只有數組中所有Future都執行成功後,纔會觸發then的成功回調,只要有一個Future執行失敗,就會觸發錯誤回調。下面,我們通過模擬Future.delayed 來模擬兩個數據獲取的異步任務,等兩個異步任務都執行成功時,將兩個異步任務的結果拼接打印出來,代碼如下:

Future.wait([
  // 2秒後返回結果  
  Future.delayed(new Duration(seconds: 2), () {
    return "hello";
  }),
  // 4秒後返回結果  
  Future.delayed(new Duration(seconds: 4), () {
    return " world";
  })
]).then((results){
  print(results[0]+results[1]);
}).catchError((e){
  print(e);
});

執行上面代碼,4秒後你會在控制檯中看到“hello world”。

Async/await

Dart中的async/await 和JavaScript中的async/await功能和用法是一模一樣的,如果你已經瞭解JavaScript中的async/await的用法,可以直接跳過本節。

  1. 回調地獄(Callback hell)

如果代碼中有大量異步邏輯,並且出現大量異步任務依賴其它異步任務的結果時,必然會出現Future.then回調中套回調情況。舉個例子,比如現在有個需求場景是用戶先登錄,登錄成功後會獲得用戶Id,然後通過用戶Id,再去請求用戶個人信息,獲取到用戶個人信息後,爲了使用方便,我們需要將其緩存在本地文件系統,代碼如下:

//先分別定義各個異步任務
Future<String> login(String userName, String pwd){
    ...
    //用戶登錄
};
Future<String> getUserInfo(String id){
    ...
    //獲取用戶信息 
};
Future saveUserInfo(String userInfo){
    ...
    // 保存用戶信息 
}; 

接下來,執行整個任務流:

login("alice","******").then((id){
 //登錄成功後通過,id獲取用戶信息    
 getUserInfo(id).then((userInfo){
    //獲取用戶信息後保存 
    saveUserInfo(userInfo).then((){
       //保存用戶信息,接下來執行其它操作
        ...
    });
  });
})

可以感受一下,如果業務邏輯中有大量異步依賴的情況,將會出現上面這種在回調裏面套回調的情況,過多的嵌套會導致的代碼可讀性下降以及出錯率提高,並且非常難維護,這個問題被形象的稱爲回調地獄(Callback hell)。回調地獄問題在之前JavaScript中非常突出,也是JavaScript被吐槽最多的點,但隨着ECMAScript6和ECMAScript7標準發佈後,這個問題得到了非常好的解決,而解決回調地獄的兩大神器正是ECMAScript6引入了Promise,以及ECMAScript7中引入的async/await。 而在Dart中幾乎是完全平移了JavaScript中的這兩者:Future相當於Promise,而async/await連名字都沒改。接下來我們看看通過Futureasync/await如何消除上面示例中的嵌套問題。

  1. 使用Future消除callback hell
login("alice","******").then((id){
      return getUserInfo(id);
}).then((userInfo){
    return saveUserInfo(userInfo);
}).then((e){
   //執行接下來的操作 
}).catchError((e){
  //錯誤處理  
  print(e);
});

正如上文所述, Future 的所有API的返回值仍然是一個Future對象,所以可以很方便的進行鏈式調用” ,如果在then中返回的是一個Future的話,該future會執行,執行結束後會觸發後面的then回調,這樣依次向下,就避免了層層嵌套。

  1. 使用async/await消除callback hell

通過Future回調中再返回Future的方式雖然能避免層層嵌套,但是還是有一層回調,有沒有一種方式能夠讓我們可以像寫同步代碼那樣來執行異步任務而不使用回調的方式?答案是肯定的,這就要使用async/await了,下面我們先直接看代碼,然後再解釋,代碼如下:

task() async {
   try{
    String id = await login("alice","******");
    String userInfo = await getUserInfo(id);
    await saveUserInfo(userInfo);
    //執行接下來的操作   
   } catch(e){
    //錯誤處理   
    print(e);   
   }  
}
  • async用來表示函數是異步的,定義的函數會返回一個Future對象,可以使用then方法添加回調函數。
  • await 後面是一個Future,表示等待該異步任務完成,異步完成後纔會往下走;await必須出現在 async 函數內部。

可以看到,我們通過async/await將一個異步流用同步的代碼表示出來了。

其實,無論是在JavaScript還是Dart中,async/await都只是一個語法糖,編譯器或解釋器最終都會將其轉化爲一個Promise(Future)的調用鏈。

Stream

Stream 也是用於接收異步事件數據,和Future 不同的是,它可以接收多個異步操作的結果(成功或失敗)。 也就是說,在執行異步任務時,可以通過多次觸發成功或失敗事件而傳遞結果數據或錯誤異常。 Stream 常用於會多次讀取數據的異步任務場景,如網絡內容下載、文件讀寫等。舉個例子:

Stream.fromFutures([
  // 1秒後返回結果
  Future.delayed(new Duration(seconds: 1), () {
    return "hello 1";
  }),
  // 拋出一個異常
  Future.delayed(new Duration(seconds: 2),(){
    throw AssertionError("Error");
  }),
  // 3秒後返回結果
  Future.delayed(new Duration(seconds: 3), () {
    return "hello 3";
  })
]).listen((data){
   print(data);
}, onError: (e){
   print(e.message);
},onDone: (){

});

上面的代碼依次會輸出:

I/flutter (17666): hello 1
I/flutter (17666): Error
I/flutter (17666): hello 3

代碼很簡單,就不贅述了。

思考題:既然Stream可以接收多次事件,那能不能用Stream來實現一個訂閱者模式的事件總線?

總結

通過上面介紹,相信你對Dart應該有了一個初步的印象,由於筆者平時也使用Java和JavaScript,下面筆者根據自己的經驗,結合Java和JavaScript,談一下自己的看法。

之所以將Dart與Java和JavaScript對比,是因爲,這兩者分別是強類型語言和弱類型語言的典型代表,並且Dart 語法中很多地方也都借鑑了Java和JavaScript。

Dart vs Java

客觀的來講,Dart在語法層面確實比Java更有表現力;在VM層面,Dart VM在內存回收和吞吐量都進行了反覆的優化,但具體的性能對比,筆者沒有找到相關測試數據,但在筆者看來,只要Dart語言能流行,VM的性能就不用擔心,畢竟Google在go(沒用vm但有GC)、javascript(v8)、dalvik(android上的java vm)上已經有了很多技術積澱。值得注意的是Dart在Flutter中已經可以將GC做到10ms以內,所以Dart和Java相比,決勝因素並不會是在性能方面。而在語法層面,Dart要比java更有表現力,最重要的是Dart對函數式編程支持要遠強於Java(目前只停留在lamda表達式),而Dart目前真正的不足是生態,但筆者相信,隨着Futter的逐漸火熱,會回過頭來反推Dart生態加速發展,對於Dart來說,現在需要的是時間。

Dart vs JavaScript

JavaScript的弱類型一直被抓短,所以TypeScript、Coffeescript甚至是Facebook的flow(雖然並不能算JavaScript的一個超集,但也通過標註和打包工具提供了靜態類型檢查)纔有市場。就筆者使用過的腳本語言中(筆者曾使用過Python、PHP),JavaScript無疑是動態化支持最好的腳本語言,比如在JavaScript中,可以給任何對象在任何時候動態擴展屬性,對於精通JavaScript的高手來說,這無疑是一把利劍。但是,任何事物都有兩面性,JavaScript的強大的動態化特性也是把雙刃劍,你可經常聽到另一個聲音,認爲JavaScript的這種動態性糟糕透了,太過靈活反而導致代碼很難預期,無法限制不被期望的修改。畢竟有些人總是對自己或別人寫的代碼不放心,他們希望能夠讓代碼變得可控,並期望有一套靜態類型檢查系統來幫助自己減少錯誤。正因如此,在Flutter中,Dart幾乎放棄了腳本語言動態化的特性,如不支持反射、也不支持動態創建函數等。並且Dart在2.0強制開啓了類型檢查(Strong Mode),原先的檢查模式(checked mode)和可選類型(optional type)將淡出,所以在類型安全這個層面來說,Dart和TypeScript、Coffeescript是差不多的,所以單從這一點來看,Dart並不具備什麼明顯優勢,但綜合起來看,dart既能進行服務端腳本、APP開發、web開發,這就有優勢了!

官方PPT宣傳截圖

Flutter 底層架構的一個大概示意圖:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-t2x3nf5W-1579247993920)(…/images/flutter.png)]

Material 和 Cupertino 是 Flutter 官方提供的兩個不同的 UI 風格組件庫(前者Android,後者IOS)。

在 Flutter 中,一切皆是 Widget 。 一個按鈕是 Widget,一段文字也是 Widget,一個圖片也是 Widget,一個路由導航 也是 Widget。所以前期接觸 Flutter 可以先學習這兩個UI庫如何使用即可。(個人見解)

基礎組件庫

Material 組件庫

Cupertino 組件庫

搭建開發環境

搭建過程很簡單,下載 SDK 包,然後配置下環境變量就ok了。

編輯器推薦

VScode,輕巧、簡潔。

配置好 Flutter環境,只需要在安裝一個 Flutter 插件就好了。

官方配置教程

第一個Demo

在 VScode 中安裝好插件後,按下shift + command + p 輸入 flutter ,選擇 New Project

第一次創建時可能需要選擇 Flutter SDK 的位置。

下面的Demo是官網上的給出的代碼,整理出來的一個完整的。

  1. 先在 pubspec.yaml 中添加一個依賴: english_words 它是 Dart 語言編寫的一個隨機生成英文單詞的工具包。

> pubspec.yaml 是 Flutter 配置文件,可以理解爲 npm 中的 package.json

找到文件的第21行:

yaml    dependencies:      flutter:        sdk: flutter          # The following adds the Cupertino Icons font to your application.      # Use with the CupertinoIcons class for iOS style icons.      cupertino_icons: ^0.1.2            # 在這裏添加 版本號遵循 語義化(Semantic Versioning)      english_words: ^3.1.5        dev_dependencies:      flutter_test:        sdk: flutter

> Flutter 有一個官方的包管理平臺,pub.dartlang.org 類似於npm

添加完成後,在控制檯輸入flutter packages get 或者在編輯器中右鍵點擊 pubspes.yaml 選擇 Get Packages

也就是安裝新的依賴。

  1. 替換Demo代碼

這個Demo是一個隨機生成英文名字的程序,有一個可以無限滾動的列表,可以讓用戶對喜歡的名字進行紅心標記蒐藏,然後點擊右上角,可以查看已收藏的名字(路由跳轉來實現的)。

lib/main.dart 中的所有代碼刪除,替換成下面的代碼:

> 下面的代碼是將官網Demo中的代碼整理好的,可以先不去管它什麼樣的結果或者具體每句代碼什麼意思,先將Demo在模擬器中跑起來再說。

dart    import 'package:flutter/material.dart';    import 'package:english_words/english_words.dart';        // 程序入口    void main() => runApp(new MyApp());        class MyApp extends StatelessWidget {      @override      Widget build(BuildContext context) {        return new MaterialApp(          title: 'Startup Name Generator',          home: new RandomWords(),          theme: new ThemeData(            primaryColor: Colors.white,          ),        );      }    }        class RandomWords extends StatefulWidget {      @override      createState() => new RandomWordsState();    }        class RandomWordsState extends State<RandomWords> {      final _suggestions = <WordPair>[];          final _saved = new Set<WordPair>();          final _biggerFont = const TextStyle(fontSize: 18.0);      @override      Widget build(BuildContext context) {        return new Scaffold (          appBar: new AppBar(            title: new Text('Startup Name Generator'),            actions: <Widget>[              new IconButton(icon: new Icon(Icons.list), onPressed: _pushSaved),            ],          ),          body: _buildSuggestions(),        );      }          void _pushSaved() {        Navigator.of(context).push(          new MaterialPageRoute(            builder: (context) {              final tiles = _saved.map(                (pair) {                  return new ListTile(                    title: new Text(                      pair.asPascalCase,                      style: _biggerFont,                    ),                  );                },              );              final divided = ListTile                .divideTiles(                  context: context,                  tiles: tiles,                )                .toList();                    return new Scaffold(                  appBar: new AppBar(                    title: new Text('Saved Suggestions'),                  ),                  body: new ListView(children: divided),                );            },          )        );      }          Widget _buildRow(WordPair pair) {        final alreadySaved = _saved.contains(pair);        return new ListTile(          title: new Text(            pair.asPascalCase,            style: _biggerFont,          ),          trailing: new Icon(            alreadySaved ? Icons.favorite : Icons.favorite_border,            color: alreadySaved ? Colors.red : null,          ),          onTap: () {            setState(() {              if (alreadySaved) {                _saved.remove(pair);              } else {                _saved.add(pair);              }            });          },        );      }          Widget _buildSuggestions() {        return new ListView.builder(          padding: const EdgeInsets.all(16.0),          // 對於每個建議的單詞對都會調用一次itemBuilder,然後將單詞對添加到ListTile行中          // 在偶數行,該函數會爲單詞對添加一個ListTile row.          // 在奇數行,該行書湖添加一個分割線widget,來分隔相鄰的詞對。          // 注意,在小屏幕上,分割線看起來可能比較吃力。          itemBuilder: (context, i) {            // 在每一列之前,添加一個1像素高的分隔線widget            if (i.isOdd) return new Divider();                // 語法 "i ~/ 2" 表示i除以2,但返回值是整形(向下取整),比如i爲:1, 2, 3, 4, 5            // 時,結果爲0, 1, 1, 2, 2, 這可以計算出ListView中減去分隔線後的實際單詞對數量            final index = i ~/ 2;            // 如果是建議列表中最後一個單詞對            if (index >= _suggestions.length) {              // ...接着再生成10個單詞對,然後添加到建議列表              _suggestions.addAll(generateWordPairs().take(10));            }            return _buildRow(_suggestions[index]);          }        );      }    }

  1. 選擇調試 -> 啓動調試 然後選擇 ios emulator , 等待啓動即可。(這個是macOS上的操作,windows只能選擇Android的模擬器,當前所有的前提是你的 Flutter 環境確保搭建成功了。)

運行成功後如下圖所示:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-E1SjEesz-1579247993927)(…/images/flutter-ios.png)]

官方學習資料鏈接

Flutter 中文網

Flutter 實戰

以上,致那顆騷動的心……

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章