Flutter入坑分享

本文只適合初次接觸 Flutter 的開發者。 原文連接: blog.myweb.kim/flutter/Flu…javascript

簡介

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

  • 跨平臺前端

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

  • 高性能linux

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

    Skia 延伸...webpack

    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安裝包小的主要緣由。ios

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

    並且在渲染層,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變量一旦賦值,類型便會肯定,則不能再改變其類型,如:

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

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

  2. dynamicObject

    DynamicObjectvar功能類似,都會在賦值時自動進行類型推斷,不一樣在於,賦值後能夠改變其類型,如:

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

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

  3. finalconst

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

    //能夠省略String這個類型聲明
    final str = "hi world";
    //final str = "hi world"; 
    const str1 = "hi world";
    //const String str1 = "hi world";
    複製代碼

函數

Dart是一種真正的面向對象的語言,因此即便是函數也是對象,而且有一個類型Function。這意味着函數能夠賦值給變量或做爲參數傳遞給其餘函數,這是函數式編程的典型特徵。

  1. 函數聲明

    bool isNoble(int atomicNumber) {
      return _nobleGases[atomicNumber] != null;
    }
    複製代碼

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

    typedef bool CALLBACK();
    
    //不指定返回類型,此時默認爲dynamic,不是bool
    isNoble(int atomicNumber) {
      return _nobleGases[atomicNumber] != null;
    }
    
    void test(CALLBACK cb){
       print(cb()); 
    }
    //報錯,isNoble不是bool類型
    test(isNoble);
    複製代碼
  2. 對於只包含一個表達式的函數,可使用簡寫語法

    bool isNoble (int atomicNumber )=> _nobleGases [ atomicNumber ] != null ;   
    複製代碼
  3. 函數做爲變量

    var say= (str){
      print(str);
    };
    say("hi world");
    複製代碼
  4. 函數做爲參數傳遞

    void execute(var callback){
        callback();
    }
    execute(()=>print("xxx"))
    複製代碼
  5. 可選的位置參數

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

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

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

    say('Bob', 'Howdy'); //結果是: Bob says Howdy
    複製代碼

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

    say('Bob', 'Howdy', 'smoke signal'); //結果是:Bob says Howdy with a smoke signal
    複製代碼
  6. 可選的命名參數

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

    //設置[bold]和[hidden]標誌
    void enableFlags({bool bold, bool hidden}) {
        // ... 
    }
    複製代碼

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

    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 底層架構的一個大概示意圖:

官方PPT截圖

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行:

    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

    也就是安裝新的依賴。

  2. 替換Demo代碼

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

    將lib/main.dart 中的全部代碼刪除,替換成下面的代碼:

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

    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]);
          }
        );
      }
    }
    
    複製代碼
  3. 選擇調試 -> 啓動調試 而後選擇 ios emulator , 等待啓動便可。(這個是macOS上的操做,windows只能選擇Android的模擬器,當前全部的前提是你的 Flutter 環境確保搭建成功了。)

    運行成功後以下圖所示:

    flutter-ios

官方學習資料連接

Flutter 中文網

Flutter 實戰

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

相關文章
相關標籤/搜索