帶你高效入門 Flutter

NanBox發表於2019-07-08

本文面向 Flutter 初學者,旨在用易懂的方式帶大家入門。除了 Flutter 程式碼,還會介紹到語法、原理、特性等基礎知識。相信本文能幫助你學習和理解 Flutter。

跨平臺技術簡介

我們先看一下目前的一些跨平臺方案,從前端渲染的角度來分類的話,大致可以分為以下幾種方案。

WebView 渲染

這種方案就很好理解,現在很多專案都會嵌入 H5 的頁面。就是用 JavaScript 等前端技術進行開發,在客戶端上用 WebView 來進行渲染。微信小程式目前使用的就是這種方案。

微信小程式

它的優點很明顯,使用成熟的前端技術進行開發,學習成本低,開發效率高,並且支援動態釋出程式碼。

但缺點也很明顯,在效能體驗上,和原生還是存在較大差距的。

原生控制元件渲染

既然 WebView 的效能不夠好,於是就有了使用原生控制元件進行渲染的方案。這種方案,同樣也是使用 JavaScript 開發,區別是它最終是呼叫原生控制元件進行渲染的。這種方案的代表是 Facebook 的 React Native。

React Native

由於使用原生控制元件進行渲染,效能體驗也會更接近原生。但也只是更接近,和原生還是有差距的,因為它需要頻繁的進行 JavaScript 和原生之間的通訊,這個通訊效率是比較低的。

另外,由於需要適配各個平臺的控制元件,那就有可能出現,系統控制元件更新了,而框架本身還沒有更新,由此產生了一些問題。換句話說,這種方案是受到原生控制元件限制的。

繪圖引擎渲染

接下來就是主角了。

在前端,如果完全不使用原生控制元件,我們可以通過系統的繪圖 API 繪製出一個使用者介面。從這個角度出發,可以在各個平臺使用一個統一介面的繪圖引擎來進行介面繪製,這個引擎最終呼叫的是系統的 API 繪製的。這樣的話,它的效能可以做到接近原生,並且又不受原生控制元件的限制,在不同平臺上能夠做到 UI 統一。

Flutter 就是這樣的一個開發框架。

Flutter

Flutter 簡介

一個跨平臺 UI 解決方案

Flutter 是由 Google 開發的,一個跨平臺 UI 解決方案。換句話說,它原則上只管 UI 的問題,如果涉及到平臺本身的一些功能,比如呼叫藍芽、攝像頭,一般還是需要原生程式碼去操作。但現在也會有一些第三方庫幫我們解決這些問題。

繪圖引擎 Skia

Skia

Flutter 使用 Skia 作為它的繪圖引擎。Skia 已經被 Google 收購,目前很多 Google 旗下的產品都是用 Skia 繪製的,包括 Android。

Android 內建了 Skia,但 iOS 沒有,所以在打 iOS 安裝包的時候,會把 Skia 一起打進去。這就導致了,用同一份 Flutter 程式碼打包之後,iOS 的包要比 Android 的包大一些。

開發語言 Dart

Dart

Flutter 使用的開發語言,叫 Dart。Dart 也是 Google 自家的,它是一門物件導向的語言,從它身上會看到一些其他開發語言的影子。學習起來難度不大的。

前面講跨平臺方案的時候,可以發現別的方案基本都是用 JavaScript 作為開發語言的,但為什麼 Flutter 不用?就因為 Dart 是谷歌自家的嗎?這個問題先留著,我們後面會提到。

搭建開發環境

這裡部分就簡單點帶過了,具體的搭建流程可以在官網檢視:

flutter-io.cn/docs/get-st…

主要的搭建步驟如下:

下載 Flutter SDK

官網下載地址:

flutter.dev/docs/develo…

由於在國內訪問可能受限,官方為中國開發者搭建了映象:

flutter.dev/community/c…

更新環境變數

解壓後,將 flutter\bin 的全路徑新增到環境變數 PATH 中。

安裝開發工具

理論上,任何文字編輯器都可以用來開發 Flutter 應用,但推薦的開發工具是 Android Studio、IntelliJ 以及 VS Code。因為在這些開發工具上,可以安裝官方的 Flutter 和 Dart 外掛,得到更好的開發體驗。文章裡使用 Android Studio 來演示。

如果你打算開發 iOS 應用,則還需要安裝 Xcode。

安裝外掛

在開發工具的外掛設定中,安裝上面說到的 Flutter 和 Dart 外掛。Flutter 外掛用於支援 Flutter 的執行、除錯、熱過載等功能,而 Dart 外掛則提供了程式碼的輸入校驗、程式碼補全等功能。

Hello World!

萬物始於 Hello World,我們先來建立一個顯示 Hello World 的 Flutter 專案。

在 Android Studio 的歡迎頁面選擇 Start a new Flutter project,或者通過選單欄的 File > New > New Flutter Project,建立一個新的 Flutter 專案。

建立好的專案裡面包含了 android 和 ios 兩個資料夾,它們是標準的 Android 和 iOS 專案。我們的 Flutter 程式碼,存放在 lib 資料夾裡。專案建立好後,會預設帶一個計數器的示例,我們不管它,把 main.dart 的程式碼改成 Hello World:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        home: Scaffold(
            appBar: AppBar(title: Text("Hello")),
            body: Center(child: Text("Hello World!", style: TextStyle(fontSize: 32)))));
  }
}
複製程式碼

啟動一個模擬器,或者連上真機,點選 Run 執行一下,就能看這樣一個介面了:

Hello World

具體程式碼先混個眼熟就好,具體的後面會再講到。

Dart 語法

在寫 Flutter 之前,還要先跟大家簡單介紹一下 Dart 的語法。如果你有 Java 或 JavaScript 的開發經驗,以及物件導向的程式設計思想,學起來是很快的。

我們可以在 test 資料夾下新建一個 dart 檔案,用來寫測試程式碼。

變數宣告

指定型別

和 java 一樣,我們可以這樣來宣告一個整形變數:

int num = 666;
複製程式碼

var

同時,我們也可以像 JavaScript 一樣,用 var 來宣告:

var num = 666;
複製程式碼

但和 JavaScript 不同的是,以下程式碼在 JavaScript 是不會報錯的,但在 Dart 裡會報錯:

var num = 666;

void main() {
  num = "666"; // 報錯
}
複製程式碼

在 Dart 裡用 var 宣告一個變數之後,它會根據第一次賦值,來推斷變數的型別,之後就不能再改變型別了。也就是說,JavaScript 是一種弱型別的語言,而 Dart 是一種強型別的語言。

Object

如果非要上面這樣寫,那也可以。把 var 換成 Object 就不報錯了:

Object num = 666;

void main() {
  num = "666";
}
複製程式碼

和 Java 類似,Object 是所有物件的根基類。但是這樣的話,如果想列印一下 num 的字串長度,是會報錯的:

Object num = 666;

void main() {
  num = "666";
  print(num.length); // 報錯
}
複製程式碼

因為 length 是屬於 String 的,但系統只知道 num 是一個物件,並不知道它是一個 String。

dynamic

如果還是非要這樣寫,那也可以。Dart 有一個特有的關鍵字 dynamic,把 Object 改成 dynamic 就不報錯了:

dynamic num = 666;

void main() {
  num = "666";
  print(num.length);
}
複製程式碼

我們執行一下這個檔案,可以在控制檯看到正確列印出了字串長度。

dynamic 是動態的意思,使用它宣告的話,系統會去找到可用的屬性和函式來呼叫。但這種寫法其實是不太安全的,因為即使你把屬性名拼錯了,編譯器也是不會報錯的,要到執行時才會報錯。

函式

dynamic

在 Dart 裡,函式也是可以不寫返回型別的,不寫的話會被當做 dynamic 來處理。這樣的話,函式的型別就是 return 的型別,如果沒有 return 則是 void 型別。比如可以這樣:

main() {
  print(getNum().length);
}

getNum(){
  return "666";
}
複製程式碼

執行之後是能正確列印出字串長度的。

用於傳參

Dart 裡的函式也是一個物件,所以可以把函式作為引數來傳遞,比如:

main() {
  execute(print666);
}

print666() {
  print("666");
}

execute(Function fun){
  fun();
}
複製程式碼

可選引數

在 Dart 的函式傳參裡,有一個叫可選引數的概念,我們以文字控制元件 Text 為例,在原始碼裡可以看到 Text 的建構函式是這樣的:

const Text(
    this.data, {
    Key key,
    this.style,
    this.strutStyle,
    this.textAlign,
    this.textDirection,
    this.locale,
    this.softWrap,
    this.overflow,
    this.textScaleFactor,
    this.maxLines,
    this.semanticsLabel,
})
複製程式碼

首先,在引數裡有一個 data,它是要顯示的文字內容,是一個必填項。而 data 後面的一堆引數,是用一個大括號括起來的,這些引數就叫做可選引數,意思是這些引數可傳可不傳。

假如我們要顯示一個比較長的文字,又想限制它最多顯示兩行,就可以這樣來建立一個 Text:

new Text("666666666666", maxLines: 2); 
複製程式碼

可選引數,在 Flutter 裡面用的非常多。

非同步

Future

在 Dart 裡使用 Future 來處理非同步任務,比如我們現在延時一秒列印 666,程式碼如下:

void main() {
  Future.delayed(new Duration(seconds: 1), () {
    return "666";
  }).then((data) {
    // 成功後執行
    print(data);
  }).catchError((e) {
    // 失敗後執行
    print("error");
  }).whenComplete(() {
    // 無論成功失敗都會執行
    print("complete");
  });
}
複製程式碼

Future 的語法和 Promise 非常像。任務執行成功會呼叫 then,執行失敗會呼叫 catchError,而無論成功還是失敗,都會呼叫 whenComplete。

async/await

如果你不喜歡上面那種寫法,或者是想把非同步轉成同步,就可以用 async 和 await 這兩個關鍵字來轉換。

我們把上面的程式碼轉換一下,寫一個 getString 方法,返回的型別是 Future,它會延時返回一個字串。在 main 函式後面加上 async 關鍵字,在 getString() 前面加上 await,程式碼如下:

void main() async{
  print(await getString());
}

getString() {
  return Future.delayed(new Duration(seconds: 1), () {
    return "666";
  });
}
複製程式碼

執行之後可以看到,能正常延時一秒後,把字串列印出來。這裡 getString() 返回的型別是 Future,而 await getString() 則是返回了延時之後返回的字串。await 要在 async 的函式裡面才能使用。

async 和 await 其實是一個語法糖,它最終也是轉換成 Future 呼叫鏈的形式執行的。

萬物皆控制元件

接下來回到 Flutter,Flutter 裡最重要的一個概念是 Widget(下面翻譯作控制元件)。

在原生開發裡面,我們可能會在介面上區分,這是一個 View,這是一個 Layout,這是一個 View Controller。但在 Flutter 裡面,它們全都屬於一個統一的模型 Widget。可以說,在 Flutter 介面裡,所有東西都是 Widget。

以前學物件導向的時候,我們都聽過一句話,叫萬物皆物件。我這裡套用一下,在 Flutter 裡,萬物皆控制元件

具體有哪些控制元件,我做了一下簡單的分類。

根控制元件

class MyPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MyWidget();
  }
}
複製程式碼

所有的控制元件都屬於 StatefulWidgetStatelessWidget。它們的區別是,StatefulWidget 擁有狀態 State,而 StatelessWidget 沒有。

StatefulWidget

當一個控制元件是可變的時候,就要使用 StatefulWidget 來構建。StatefulWidget 本身不可變,但它持有的狀態 State 是可變的。

StatelessWidget

當一個控制元件狀態是固定不可變的時候,就可以使用 StatelessWidget。前面我們寫的 Hello World 就是使用 StatelessWidget。

容器控制元件

Center(
    child: MyWidget()
)
複製程式碼

容器類控制元件一般是將某些屬性或配置,作用在它的子控制元件上,比如控制元件所在的寬高、背景、位置等。

常用的容器控制元件有 Container、Center、Padding 等。

佈局控制元件

Column(children: <Widget>[
    MyWidget1(),
    MyWidget2()
])
複製程式碼

佈局控制元件可以類比作原生開發中的 Layout,通常它會擁有一個 children 的屬性,用於接收一個控制元件陣列,對這些控制元件進行特定的排版。

常用的佈局控制元件有 Row、Column、Stack、Flex 等。

基礎控制元件

Text("666")
複製程式碼

基礎控制元件就是常用的文字、按鈕、圖片等控制元件。

常用的基礎控制元件有 Text、TextField、Button、Image 等。

功能控制元件

Navigator.of(context).push(MaterialPageRoute(builder: (context) {
    return NewPage();
}));
複製程式碼

在 Flutter 裡還有一類控制元件,它們不影響 UI 佈局,但帶有一些特定的功能,比如頁面跳轉、事件監聽、定義主題等。我們把這一類控制元件稱作功能控制元件。

常用的功能控制元件有 Navigator、NotificationListener、Theme 等。

計數器

開始寫 Flutter 程式碼了。還記不記得,在 Flutter 專案建立之後,是自帶一個計數器 demo 的,現在我們用自己的程式碼實現一遍。程式碼修改成如下:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: MyPage());
  }
}

class MyPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return MyPageState();
  }
}

class MyPageState extends State<MyPage> {
  int num = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("計數器"),
        ),
        body: Center(
            child: Text(
                num.toString(), 
                style: TextStyle(fontSize: 36)
            )
        ),
        floatingActionButton: FloatingActionButton(
            child: Icon(Icons.add),
            onPressed: () {
              setState(() {
                num++;
              });
            }));
  }
}
複製程式碼

執行之後,就可以看到這樣的介面了:

計數器

按鈕每點選一次,數字就會加一。下面我們來分析一下這段程式碼,看下里面用到的一些 Widget。

StatefulWidget

由於頁面中的數字是跟隨狀態變化的,所以該頁面改用 StatefulWidget。StatefulWidget 並不會直接返回一個 Widget,而是返回狀態 State,在 State 裡再返回 Widget。

Scaffold

Scaffold 是一個標準的 Material Design 頁面,它包含了標題欄、浮動按鈕、側滑選單、底部導航欄等配置。我們這裡用到了標題欄 appBar、頁面內容 body、浮動按鈕 floatingActionButton。

AppBar

AppBar 就是標題欄,通過檢視控制元件的構造方法,我們可以知道它可配置的屬性。

AppBar 的可選引數除了標題 title,還可以配置標題前的內容 leading,右側的操作按鈕 anctions,控制元件垂直高度 elevation 等。我們只傳了 title,其他屬性都用預設值。

Center

Center 是一個容器類控制元件,它的作用就是讓它的子控制元件居中顯示。

FloatingActionButton

熟悉安卓開發的應該對這個控制元件比較熟悉,它就是頁面右下角一個特定樣式的 Button,引數裡面的 onPressed 是一個必填項,要傳一個點選之後的回撥函式。

根據這個例子,下面給大家介紹一下 Flutter 兩個比較重要的特性。

響應式 UI

點選 Button 之後,我們把 num 變數加一,並使用 setState 通知狀態發生了改變,Flutter 會根據新的狀態更新 UI。如果有接觸過小程式開發,setState 就和小程式的 setData 類似。

在 Flutter 裡面我們不需要用 set 方法來更新 UI,可變控制元件是和狀態繫結的,這就是 Flutter 的響應式 UI 程式設計。

熱過載

在 Android Q 和 iOS 13 裡都加入了暗黑模式,我們也換一個暗黑主題來玩一下。MaterialApp 裡有一個 theme 的屬性,我們把它配置一下:

return MaterialApp(
    theme: ThemeData.dark(), 
    home: MyPage()
);
複製程式碼

這次改完之後不點 Run 了,我們點一下閃電圖示 Flutter Hot Reload,就能看到介面發生了變化:

帶你高效入門 Flutter

這就是 Flutter 的熱過載,在修改完程式碼之後,通過熱過載就能馬上在裝置上看到修改結果,可以很大程度上增加開發效率。

下面再給大家介紹幾個 Flutter 裡的常見操作。

頁面跳轉

在 Flutter 裡,使用 Navigator 來管理頁面跳轉,比如要跳轉到一個 NewPage 可以這樣寫:

Navigator.of(context).push(MaterialPageRoute(builder: (context) {
    return NewPage();
}));
複製程式碼

進棧使用 push,出棧則是 pop。

使用 MaterialPageRoute 會模擬出 Android 上頁面跳轉的過場效果。

本地圖片

我們來看看怎麼顯示一張本地圖片。

先在根目錄新建一個存放圖片的資料夾,比如叫 images,把圖片 picture.png 放進去。

找到根目錄下的 pubspec.yaml 檔案,這個便是 Flutter 依賴配置檔案,我們需要在這裡配置一下剛才的圖片:

  assets:
    - images/picture.png
複製程式碼

這樣,我們就能使用 Image 控制元件把這張圖片顯示出來了:

Image.asset("images/picture.png"))
複製程式碼

引入第三方庫

和 node 的 npm 以及 Android 的 jcenter 類似,Flutter 也擁有一個公共倉庫 pub.dev。pub.dev 是 Google 官方的 Dart 倉庫,在上面可以找到我們需要的包和外掛。

Flutter 本身沒有 Toast,我們來接入一個。在 pub.dev 上搜尋後,我決定使用 fluttertoast

帶你高效入門 Flutter

按照說明,在 pubspec.yaml 檔案裡的 dependencies 下配置:

  fluttertoast: ^3.1.0
複製程式碼

點一下 Android Studio 右上角的 Packages get 同步之後就可以使用了:

Fluttertoast.showToast(msg: "666666");
複製程式碼

iOS 樣式

我們上面使用的都是 Material Design 的控制元件,它們都是在 flutter/material.dart 包裡面的。如果要使用 iOS 風格的控制元件,則要用到 flutter/cupertino.dart 包:

import 'package:flutter/cupertino.dart';
複製程式碼

iOS 風格的控制元件,基本都以 Cupertino 開頭。我們把計時器頁面裡的控制元件替換一下:

import 'package:flutter/cupertino.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CupertinoApp(home: MyPage());
  }
}

class MyPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return MyPageState();
  }
}

class MyPageState extends State<MyPage> {
  int num = 0;

  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
        navigationBar: CupertinoNavigationBar(
          middle: Text("計數器"),
        ),
        child: Container(
            width: double.infinity,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: <Widget>[
                Text(num.toString(), style: TextStyle(fontSize: 36)),
                CupertinoButton(
                    color: CupertinoColors.activeBlue,
                    child: Icon(CupertinoIcons.add),
                    onPressed: () {
                      setState(() {
                        num++;
                      });
                    })
              ],
            )));
  }
}
複製程式碼

效果如下:

帶你高效入門 Flutter

編譯方式

程式碼的部分就到這裡了,接下來跟大家聊一下編譯方式,程式語言的編譯方式有兩種。

  • JIT (Just in time) - 邊執行、邊編譯

  • AOT (Ahead of time) - 先編譯,再執行

關於它們孰優孰劣,就要看從哪個角度去對比了。JIT 的話,它的一大特點就是支援動態釋出程式碼,也就是支援熱更新。但要是從效能的角度考慮,AOT 會更好,因為在執行的時候不用再進行編譯的操作的,執行的效率會更高一些。

回到我們一開始的時候留下的問題,為什麼別的跨平臺方案都是用 JavaScript,而 Flutter 要用 Dart 來開發。JavaScript 的編譯方式是 JIT 的,它不支援 AOT。而 Dart 同時支援 JIT 和 AOT。

Flutter 在開發階段使用 JIT,讓我們用上了熱過載,增加了開發效率。在打包時改用 AOT,保證了正式版應用的效能。

熱更新?

最後講一下大家比較關心的一個東西,Flutter 是否支援熱更新?前面說到 Dart 支援 JIT,所以從技術層面它是支援的。但是目前是不支援的,在官方的計劃文件中,可以看到:

帶你高效入門 Flutter

至於原因,官方在這裡進行了說明。總的來說,是由於政策的限制,以及出於對效能和安全性的考慮,暫時不支援了。

結束

到這就結束啦。由於想把 Flutter 基礎在一篇內講完,沒有涉及太多細節,如果要寫 Flutter 程式碼還需要深入學習。但相信理解之後再學,會輕鬆很多。

妥妥的。

相關文章