iT邦幫忙

2024 iThome 鐵人賽

DAY 20
0
Mobile Development

Flutter 開發實戰 - 30 天逃離新手村系列 第 20

Day 20 探索主流第三方插件

  • 分享至 

  • xImage
  •  

上篇我們已經學習了關於套件、插件的基礎。Flutter 擁有豐富的插件,但有時候,問題是我們不知道該從何開始又或者說我們不知道該如何抉擇。後續我們將介紹一些主流的第三方插件以及功能概覽。

我們將從 Firebase 開始。Firebase 是由 Google 雲端的一系列服務組成,可以協助我們建立應用程式。其包含了使用者驗證,資料儲存,推播通知,監控分析等等功能。

進一步學習如何使用 Google Places 來實現關於地圖和位置資訊。很多應用需要在地圖上搜尋和顯示位置,而 Google Places 插件可以協助完成相關功能。

接著,學習如何使用裝置的功能例如相機和相簿功能,最後我們進一步研究一些成熟 app 通常會包含的功能。

大綱如下:

  • Firebase 插件
  • Google Maps 和 Google Places
  • 裝置功能
  • 其他支援功能

Firebase 插件

Firebase 是 Google 提供的服務,支援了許多功能讓我們能專注在我們應用程式的核心業務,其提供的功能如下:

  • Realtime Database 即時資料庫:這是一個雲端 NoSQL 資料庫,使用這個服務可以即時儲存和讀取資料。
  • Cloud Firestore:也是一個 NoSQL 資料庫,但專注於大型應用,和即時資料庫的差異是支援進階查詢。
  • Cloud Function:這是一個提供執行程式函式的服務,可以通過事件觸發執行該函式。而所謂的事件可以由其他 Firebase 的服務產生或使用 SDK 觸發。舉例來說我們可以在當資料庫有資料發生變化時,觸發執行某個函式又或者例如當檔案上傳成功時,觸發事件執行該函式進行轉檔等等。這個函式具體的功能我們可以自行撰寫程式碼。
  • 效能監控:你可以收集並分析應用程式的相關資訊。
  • 驗證:驗證功能讓應用程式可以進行身分驗證,識別用戶同時提供權限控管等。
  • Firebase Cloud Messaging:雲端訊息可以用來交換伺服器和應用程式之間的訊息。主要用於推播通知。
  • AdMob:顯示廣告,通過我們的應用程式來播放廣告盈利。
  • Machine Learning Kit :植入進階 ML 功能的工具。例如:識別文字、檢測人臉、識別地標、掃描條碼

使用 Firebase 其中一個好處便是全部的服務緊密整合,例如資料庫使用驗證來管理存取權限,Cloud Function 可以根據資料庫的變更觸發執行,或者 Cloud Function 使用驗證功能要識別使用者。但缺點也很明顯,就是我們可能會非常依賴該服務。

Firebase 註冊

在開始之前我們需要註冊一個 Firebase 帳號,其服務的功能可由管理介面建立、設定管理。

註冊完成之後,我們可以在控制台建立一個新專案,注意!有些 Firebase 設定是不可變更的,例如專案 ID 和資源的 URL,即時資料庫託管的位置,因此請仔細選擇這些設定。

Flutter 應用程式使用 Firebase

一個 Firebase 專案可以同時支援不同平台的應用程式。在 Firebase 專案頁面,我們可以選擇加入例如支援 iOS,Android,網頁還有 Flutter 等。專案建立完成之後,我們可以根據各平台安裝設定。

Android

對於 Android 應用程式來說,我們需要提供 Android 套件名稱(Package Name)。這個可以在 android/app/build.gradleapplicationId 找到,或者你一開始建立專案時可以使用 flutter create --org com.example my_app

完成之後會生成一個 google-services.json 裡面包含了你的應用程式訪問 Firebase 專案所需的所有資訊。這個檔案應該加入應用程式專案的 android/app 目錄中。然後我們會需要依據 Firebase 官方教學進行一些在 android 目錄原生專案的設定。進階資訊可以參考官方教學

若需要取得偵錯指紋,可以使用 keytool 取得偵錯指紋,執行下面指令並輸入預設密碼 android,更多資訊請參考

$ keytool -list -v -alias androiddebugkey -keystore ~/.android/debug.keystore

iOS

iOS 的流程也很類似,應用程式套件名稱(Bundle Identifier)也是重點。你可以使用 Xcode 開啟 ios/Runner.xcworkspaceGeneral 頁面下找到 Bundle Identifier 欄位。

接著下載產生的 GoogleService-Info.plist 並加入 ios/Runner 目錄。注意,很重要的是你需要使用 Xcode 來執行操作,使用 Xcode 開啟專案,並將檔案拖入。

  1. 先手動將檔案放置到專案目錄 (ios/Runner 目錄) 中,這樣可以確保檔案被正確提交到 Git。
  2. 在 Xcode 中選擇「Create groups」,這樣檔案會被正確引用且不影響專案的實際檔案系統結構。

在 Xcode 中添加檔案時,通常選擇「Create groups」和「Copy items if needed」是比較好的選擇,但如果需要自動同步路徑和 Xcode 中資料內容的情況可以選擇「Create folder reference」。

其他設定請參考官方文件。後續還會根據你的專案 SwiftUI / Swift / Object-C 等情況調整。官方文件會提供你完整且最準確的教學。

FlutterFire 插件

除了上面手動安裝的方式,FlutterFire CLI 是一個專為 Flutter 開發者設計的命令行工具,它可以簡化 Firebase 的整合過程。

Firebase 官方全面支援 Flutter,不管插件的質量,設計和文件都很不錯。由於 Flutter 和 Firebase 都是 Google 的產品, Flutter 和 Firebase 的開發團隊可以相對緊密的進行開發任務。插件適用於大部分情境。

安裝 Firebase 指令

# 安裝 Firebase CLI
$ curl -sL https://firebase.tools | bash

# 登入
$ firebase login

# 列出 Firebase 專案
$ firebase projects:list

# 更新指令
$ curl -sL https://firebase.tools | upgrade=true bash

安裝 FlutterFire 指令

# 安裝 FlutterFire CLI
$ dart pub global activate flutterfire_cli

$ cd [PROJECT]
$ flutterfire configure

執行 flutterfire configure 命令,FlutterFire CLI 會自動檢測我們的 Flutter 專案,並可以選擇要關聯的 Firebase 專案,自動下載必要的設定文件(原本須手動下載並加入原生專案的 Google Service 檔案),在你的 Flutter 專案中生成 firebase_options.dart 文件等操作都會自動完成。

FlutterFire CLI 是一個專門為 Flutter 開發者設計的工具,它簡化了在 Flutter 項目中集成 Firebase 的過程。

當我們執行

$ flutterfire configure

FlutterFire CLI 會自動執行以下操作:

  • 檢查 Flutter 專案並取得相關資訊
  • 提供設定選項,例如建立專案等
  • 下載設定檔案(如 google-services.jsonGoogleService-Info.plist)。
  • FlutterFire CLI 會自動處理 Firebase 初始化所需的大部分設定工作,因此我們不需要手動執行 firebase init 指令。

接著在開始使用任意服務功能之前,我們需要先加入 firebase_core 插件,所有 Firebase 插件都相依這個插件。就如同其名稱,這個插件會負責所有核心任務,例如連線到 Firebase 專案。

$ flutter pub add firebase_core

$ flutterfire configure

要使用 Firebase 的任何插件,我們首先需要初始化 Firebase 物件。Firebase Core 會建立和 Firebase 專案的連線。有不同的方式可以達成這個目標,但最簡單的方式是直接在 main 使用非同步的方式直接初始化

import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(MyApp());
}

如上面所見,現在 main使用了 async 以及 await Firebase.initializeApp()。我們不在意回傳了什麼,只要確保初始化完成。除了在 main 直接調用,還可以例如使用 init 方法,但這裡為了單純我們直接在 main 裡面初始化。

驗證

Firebase 驗證讓我們的應用程式可以通過登入和註冊的流程,安全的存取資源和其他 Firebase 服務。為了盡可能讓使用流程簡單,Firebase 驗證支援了多種驗證選項例如電子郵件加密碼、電話號碼驗證、第三方驗證如 Google,Apple,Twitter 和 Facebook。

一樣我們可以在 pub.dev 找到插件 firebase_auth,此插件由 Firebase 團隊開發。

安裝設定插件除了上面使用指令的方式,我們也可以在 pubspec.yaml 加入:

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.6
  firebase_auth: ^4.19.6
  firebase_core: ^2.31.1

接著存檔,如果你是使用 VSCode 且有安裝相關套件,那麼 VSCode 會自行下載安裝,否則你需要執行 flutter pub get。就這樣 – 你已經在應用程式上實作身份驗證功能了。

要使用這個插件,我們需要先取得 FirebaseAuth 物件實例,你會注意到通過初始化一個物件的模式貫穿整個 Firebase 插件。

FirebaseAuth auth = FirebaseAuth.instance;

這裡要注意的是 FirebaseAuth 採用單一物件模式 singleton。然後我們加入監聽好讓程式知道驗證狀態的改變。

關於資料串流?

資料串流(Stream)是一種非同步的資料傳輸方式,它讓我們的應用程式可以持續接收來自資料源的資料。這種持續的資料傳輸使得我們的應用程式能夠即時地對資料變化做出反應,而不必主動地去查詢資料源。

要使用資料串流,一般我們需要先訂閱(subscribe)或監聽(listen)這個串流。過程類似於註冊一個事件監聽器,我們提供一個回呼函式(callback function),當資料源有新的資料時,這個回呼函式就會被自動調用,我們可以在這個函式中處理新得到的資料。

在 Flutter 的第三方插件,尤其是 Firebase 相關的插件中,資料串流的使用非常普遍。因為資料串流讓插件能夠在資料變化時主動通知我們的應用程式。這個概念與瀏覽器搭配 JavaScript 的(callback pattern)很類似,我們將回呼函式傳遞給資料源(在這裡是插件),當資料發生變化時,資料源調用這個回呼函式,從而通知我們的應用程式執行對應操作。

使用資料串流可以讓我們的應用程式變得更具反應性(reactive)。我們的應用程式不僅可以對使用者的操作做出反應,還可以對各種外部資料的變化做出反應,例如:

  1. 資料庫中的資料更新
  2. 使用者的身分驗證狀態變化
  3. 來自第三方服務的資料更新(如天氣預報)
  4. 設備傳感器的資料變化(如方向變化)

authStateChanges 串流

這裡我們具體要監聽的資料串流是 authStateChanges,如下面的程式碼:

FirebaseAuth auth = FirebaseAuth.instance;

auth.authStateChanges()
  	.listen((User user) {
      if (user == null) {
        // 登出
      } else {
        // 登入
      }
    });

上面範例如我們之前所說,我們須先取得物件實例,然後要求 authStateChanges 串流並且加入監聽事件。後續當狀態發生變化的時候,便會執行回呼函式。

登入

最後,我們需要給使用者可以登入我們的應用程式。要完成這個目的,我們可以顯示電子郵件/密碼欄位給使用者又或者使用第三方登入插件。當我們收到憑證後,最終我們還要將憑證傳入 Firebase。大略的程式如下:

try {
  UserCredential = await auth.signInWithEmailAndPassword(
  	email: "andy.you@uspace.city",
    password: "p@ssw0rd",
  );
} on FirebaseAuthException catch (e) {
  if (e.code == 'user-not-found') {
    // 未查詢到該用戶
  } else if (e.code == 'wrong-password') {
    // 密碼錯誤
  }
}

這裡我們嘗試利用使用者提供的帳密取得憑證。這裡為了單純我們直接固定資料,實務上,我們可以使用 TextFormField 等組件取得資料。通過插件提供的方法 signInWithEmailAndPassword 我們就可以實作電子郵件/密碼方式的登入。

上面我們還處理了兩個常見的錯誤,密碼錯誤或找不到用戶資料。除此之外,Firebase 驗證還可以處理電子郵件驗證,電話號碼註冊,重置密碼等完整的會員機制。舉例來說,登入之後可以檢查電子郵件:

User user = auth.currentUser;
if (!user.emailVerified) {
  await user.sendEmailVerification();
}

我們先取得使用者的物件,然後檢查是否驗證過,若無,則發送驗證信件。

在上面這些簡短的程式碼,我們完成整個驗證機制,其他複雜的後端以至於伺服器等讓 Firebase 為我們處理。

即時資料庫

Firebase Realtime Database 起初是為了聊天應用客戶端設計的資料庫。是 Firebase 最早期的核心部分之一,雖然支援一些特殊功能如離線模式,但感覺官方傾向讓新開發者使用 Firestore ,我們可以從即時資料庫的插件比起其他 Firebase 插件,官方對於 Realtime Database 的支持相對於 Firestore 確實顯得較少。例如,在對 null 安全機制的支持上,Firestore 的進展會更快。反映了 Firebase 團隊對 Firestore 的重視程度。

此插件可以搜尋 firebase_database。即時資料庫本質就是一個 JSON 資料結構的儲存(NoSQL)可以新增、更新、刪除等,這和傳統的關聯式資料庫不同。隨著應用擴展的速度和量體 NoSQL 已廣泛的被使用。

資料庫目前的主流大體諷刺兩種主要類型,基於 SQL 的關聯式資料庫和 NoSQL。在關聯式資料庫中,資料應被正規化,不應該被重複存儲是主要核心重點。

最大的優勢是,如果正確資料只存在一個地方,那麼就不會有不一致的問題。而進階複雜的資料可以通過 SQL 組合查詢。

但是這種方法把資料可確定性的重要性高於存取速度,連接多張 Table 查詢可能會導致查詢速度很慢,這對於需要快速響應的應用並不合適。

另一個缺點是,在雲端要確保資料在多個伺服器上的一致性可能會導致瓶頸。

NoSQL 解決這個問題的方式是放棄資料唯一來源,允許重複,和同步保證一致性來解決。查詢和更新資料非常快速且可擴展,但在另一方面資料庫中不同地方表示同一件事情的資料可能不一致,資料結構和軟體會緊密地耦合。各自的優缺點須自行評估決定。

即時資料庫一個最大的優勢就是可以即時互動,包含可以設定特定的監聽,如此一旦有變動我們的程式會收到通知。

另外可以通過路徑管理 JSON 的特定部分例如

{
  "users": {
    "andyyou": {},
    "calvertyang": {},
  }
}

可以使用路徑 users/andyyou 直接操作。如此我們可以操作特定部分的 JSON 結構,減少 Bug,也減少讀取冗余資料的效能。

即時資料庫最大的缺點則是 Transaction 的支援,雖然有支援 Transaction 的功能,但這個交易的功能非常侷限,也就是你無法在一個交易中對 2 的資料節點做變更。

另一個缺點就是查詢。在搜尋的時候,你的查詢條件只能針對一個欄位,這對你的資料結構設計是極大的限制,迫使你創建並存儲某種可以查詢的合併欄位。

此外,值得注意的是,Realtime Database 有 100,000 名同時連線使用者的限制,而 Firestore 則具有無限的擴展性。你可以通過設置多個 Realtime Database 資料庫來繞過這個限制,但這會增加你的設置複雜性。

設定

如同驗證一樣,插件也需要搭配 firebase_core

dependencies:
  flutter:
    sdk: flutter
  firebase_core: ^2.31.1
  firebase_database: ^10.5.7

安裝好相依插件之後,一貫的我們希望取得資料庫的參考

final _reference = FirebaseDatabase.instance.ref();

這個參考變數,我們將用來存取,更新資料庫的資料。

操作資料

如之前所提到的,資料使用一個大型 JSON 結構儲存。例如:

messages: {
  1: {
    text: 'Hey',
    viewBy: [
      'andy.you',
      'calvert.yang'
    ],
    createAt: '',
  },
  2: {}
}

上面範例結構最上層為 messages 包含了對話訊息資料。在這些訊息物件中還有具體對話內容以及誰檢視過等資料。

假設我們希望註記該對話被刪除。根據我們客戶端的設計可以有很多種作法,其中一種方式是加入刪除的標記

_ref.child('messages/1/deleted').set(true);

上面我們操作資料庫在對應路徑設定 deletedtrue 。即便我們原本沒有這個屬性還是可以這樣操作

messages: {
  1: {
    text: "Hey",
     viewBy: [
      'andy.you',
      'calvert.yang'
    ],
    createAt: '',
    deleted: true,
    …
  },
  2: {…}
}

同樣,你也可以以相同的方式進行更新(僅更改部分數據而不是全部替換)或刪除操作。

安全

即時資料的安全機制是基於我們前面提到的身份驗證。使用起來容量理解和設定。但我們設定一個資料庫的時候,會取得一個設定檔案,後續可以通過這個檔案管理。該檔案也是採用上面提到的 JSON 搭配路徑的模式,例如我只希望發送訊息的用戶才可以刪除,變更資料的時候大概如下:

"messages": {
  "$messageId": {
    "deleted": {
      ".write": "auth.token.email_verified == true && auth.token.email == data.child('sentBy').val()"
    }
  }
}

上面範例中,我們嘗試使用路徑 messages/$messageId/deleted 變更刪除的狀態。其中 $ 前綴表示為動態變數,最後我們使用了 .write 規則定義了在特定條件下用戶是否有權限寫入(或修改)該節點。也就是只有符合寫入條件的用戶才能編輯。

雲端資料庫的安全是非常重要的,因為網路上任何人都可以嘗試存取連線。傳統的作法,網站資料庫和網站服務會放在同一台伺服器,因此外部通常無法直接連線。換句話說資料庫僅允許內網連線。因此資料庫存取受到極大的限制,保護機制(守衛)的角色會落在網頁伺服器身上。而我們的應用程式並沒有伺服器的角色,因此 Firebase 提供了另一種限制存取的方式 - 通過設定檔來限制,因此應該謹慎的管理這些設定。

Firestore

Firestore 資料庫也是 NoSQL 但採用了不同的方式來儲存和檢索資料。和即時資料庫不同,Firestore 把資料儲存在檔案中,比較接近我們電腦上的檔案系統。因此取代大型 JSON 物件的是類似目錄和檔案的架構。

但兩者還是有很多相似之處例如:

  • 資料格式都是 JSON,Firestore 的檔案包含著 JSON 資料,所以同樣的物件資料兩者都可以使用。
  • 兩個資料庫都採用路徑的方式來決定要更新的資料。在 Firestore 中,路徑被分成兩個部分檔案路徑和資料路徑
  • 兩者使用類似的存取控制。Firestore 存取控制支援更多功能,但在使用來自其他文件的資料來控制存取可能有比較多限制。

Firestore 和即時資料庫在計費方式上有所不同。Firestore 的費用是根據你讀取、寫入、刪除的文件數量來計算的。這種計費模式可能不太適合某些特定類型的應用,比如聊天應用。

在聊天應用中,每個文件通常只包含一小部分資料,例如一條聊天訊息。但是,聊天群組中的每個成員都需要讀取這個文件才能看到訊息。這就意味著,雖然文件很小,但讀取的次數卻很多,從而導致較高的 Firestore 使用成本。在這種情況下,使用即時資料庫可能更加適合。

另一方面,Firestore 的資料更新速度比即時資料庫稍慢。雖然 Firestore 的速度已經很快了,通常在幾秒鐘內就能完成更新,但即時資料庫的速度更勝一籌,可以在毫秒級別內完成更新。因此,對於對速度要求非常高的實時應用,比如多人在線遊戲,即時資料庫可能是更好的選擇。

同樣的,插件可以在 pub.dev 搜尋 cloud_firestore

和即時資料庫不同,Firestore 支援更好的查詢功能,可以進行多個欄位的查詢,不過有幾個地方值得注意:

  • 你只能查詢所有文件都有的欄位。例如在即時資料庫的例子中,我們加入了 deleted 欄位,而在 Firestore 中,其他非刪除的資料應該具備 deleted 但值為 nullfalse
  • 若你查詢多個欄位,Firestore 需要幫每個欄位準備索引例如你想搜尋「在特定時間發送的訊息」且「使用者尚未讀取」,那我們需要為這兩個欄位建立索引。後續如果你決定變化查詢包含第三個條件,者需要在建立新的包含三個欄位的索引。通常這個情況問題不大,但如果你的查詢是動態的,確實需要檢查查詢的每個組合是否都有索引

最後,Firestore 具有無限的可擴展性 - 不像即時資料庫那樣對同時在線的使用者數量有所限制。

設定

一樣的,插件須搭配 firebase_core 相依。

dependencies:
  flutter:
    sdk: flutter
  firebase_core: ^2.32.0
  cloud_firestore: ^4.17.5

一旦相依套件安裝完成,我們接著取得資料庫的參考:

FirebaseFirestore _firestore = FirebaseFirestore.instance;

後續就可以使用該參考存取操作資料庫裡的資料。

操作資料

如同前面提到的,資料採用目錄和檔案架構。以同樣即時資料庫的例子,假設我們有一個訊息的資料如下:

text: "哈嘍,朋友",
viewedBy: [
	"andy.you@uspace.city",
	"calvert.yang@uspace.city"
],
deleted: null,
createdAt: 1621835683907,
createdBy: "andy.you@uspace.city"

訊息物件包含了訊息資訊,誰檢視了,建立時間和建立者等。但請注意這裡沒有對應的 messages 包含全部訊息。現在訊息保存在目錄下或在 Firestore 中稱為集合 Collection。同時我們在一開始就加入 deleted 欄位讓我們可以查詢該訊息是否被刪除。

要實作跟即時資料庫相同的更新可以:

DocumentReference ref = FirebaseFirestore.instance
  .doc('messages/1')
  .update({
    deleted: true
  });

如你所見,流程和即時資料庫的操作非常接近。

安全

安全機制也和即時資料庫有些不同,但概念是類似的。要達到限制只有建立者可以刪除訊息,我們的設定如下:

match /messages/{document=**} {
  allow update: if request.auth.token.email == resource.data.createdBy &&
    resource.data.createdBy == request.resource.data.createdBy;
}

上面語法是一個 firestore.rules 文件範例。在這個例子中,我們使用匹配規則來指定適用於 messages 集合中的所有文件。

注意,因為我們是在控制對整個文件的存取,而不是像在即時資料庫中那樣控制特定欄位的存取,所以構建的規則會有些差異。

首先,我們檢查請求的電子郵件地址是否與訊息的createdBy 欄位匹配,以確保只有訊息的發送者才能進行更新。

接著,我們檢查更新中的createdBy 值是否沒有被改變。實際上這是將 createdBy 欄位標記為唯讀,因為它不能被變更。在建立文件時,會有一個對應的規則,確保 createdBy 值只能是寫入資料的使用者的電子郵件。

Analytics 和 Crashlytics

了解應用程式如何被使用對於後續選擇開發功能方向來說非常重要。Firebase Analytics 可以紀錄使用者如何操作,而 Firebase Crashlytics 可以在發生錯誤的時候追蹤一些相關訊息。

兩個插件也都可以在 pub.dev 上搜尋找到 - firebase_analyticsfirebase_crashlytics

兩者也一樣要相依 firebase_core 插件。

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.6
  firebase_core: ^2.32.0
  firebase_analytics: ^10.10.7
  firebase_crashlytics: ^3.5.7

Crashes

要使用 Crashlytics 紀錄一些例外和錯誤可以如下:

await FirebaseCrashlytics.instance.recordError(
	error,
  stackTrace,
  reason: '錯誤原因',
  fatal: true,
);

上面的程式片段,我們使用了 recordError 方法並發送錯誤訊息到 Firebase 伺服器。

不過,有些嚴重非預期的閃退或錯誤很難在程式碼中捕捉。為了在這個情況下回報錯誤,我們需要在 main 方法中加入:

FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError;

雖然這樣還是無法 100% 捕捉到全部錯誤。但還有進階的 Zones 方式來擷取錯誤資訊。

Analytics

和回報錯誤資訊類似,Analytics 也是涉及回報資訊不過是回報其他應用程式的事件。例如:記錄某個使用者開啟了應用程式我們可以:

Analytics.observer.analytics.logAppOpen();

然後會傳送訊息到 Firebase 伺服器,我們就可以分析 app 被開啟的次數。

此外,針對應用程式的瀏覽使用,也可以加到 Navigator 來自動上傳相關分析資料。

在我們的 MaterialApp 組件,定義一個 observer 如下:

static FirebaseAnalytics analytics = FirebaseAnalytics();
static FirebaseAnalyticsObserver observer = FirebaseAnalyticsObserver(analytics: analytics);

然後將 observer 通過 navigatorObservers 建構參數加入 MaterialApp

@override
Widget build(BuildContext context) {
  return MaterialApp(
  	title: "Demo",
    navigatorObservers: <NavigatorObserver>[observer],
  );
}

之後每當使用者切換頁面時都會在 GA 留下記錄。

Cloud Storage

正如我們前面學習的,即時資料庫 Realtime Database 和 Firestore 都是 NoSQL 使用 JSON 資料格式。那如果我們要儲存其他類型的資料例如圖片或檔案呢?這時就可以使用 Cloud Storage。檔案儲存的目錄結構,基本安全控制都類似 Firestore,此外,檔案還可以附加一些 Metadata 以協助存取控制。我們一樣可以在 pub.dev 查詢 firebase_storage 找到插件。

AdMob

當你使用應用程式時看到的廣告,那麼這可能是和 Google AdMob 類似的服務。有時候開發者可能會想要增加一點收入,其中一種方式就是加入廣告。廣告有很不同類型:

  • 橫幅廣告:這是常見矩形的一種廣告,會出現在畫面的局部,並在呈現一段時間後刷新。
  • 插頁廣告:一種全畫面的廣告,通常出現在遊戲的轉場,例如進入下一個關卡之前。
  • 激勵廣告:使用者可以選擇觀看廣告來獲取 app 中的好處,例如遊戲中可以獲得積分或生命值。

Flutter AdMob 插件支援上面所有類型。只要將插件 google_mobile_ads 加入。然後需要從 Google AdMob 註冊一個 UnitId 。除此之外,要顯示廣告還需要一些步驟:

RewardedAd.load(
	adUnitId: "",
  rewardedAdLoadCallback: RewardedAdLoadCallback(
  	onAdFailedToLoad: (LoadAdError error) async {
      print("載入失敗 ${error.message}");
    },
    onAdLoaded: (RewardedAd ad) {
      myRewarded = ad;
    }
  ),
);

上面例子我們使用了激勵廣告並傳入了我們的 adUnitId ,設定 callback 在當廣告載入時執行對應處理。

接著,我們可以設定一些對應使用者進行操作事件的 callback:

myRewarded.fullScreenContentCallback = FullScreenContentCallback(
	onAdShowedFullScreenContent: (RewardedAd ad) {
    print('$ad onAdShowedFullScreenContent');
  },
  onAdDismissedFullScreenContent: (RewardedAd ad) async {
    print('$ad onAdDismissedFullScreenContent');
    await ad.dispose();
  },
  onAdFailedToShowFullScreenContent: (RewardedAd ad, AdError, error) async {
    print('$ad onAdFailedToShowFullScreenContent: $error');
    await ad.dispose();
  }
);

上面程式碼我們設定了當廣告出現、被關閉或顯示失敗對應的 callback。這些步驟非常重要,這樣你的應用程式就能在廣告流程完成後執行對應的步驟。

最後,我們可以顯示廣告:

myRewarded.show(
	onUserEarnedReward: (RewardedAd ad, RewardItem item) {
    //
  }
);

上面程式碼使廣告顯示,然後在使用者完成觀看得到獎勵的時候執行對應 callback。

值得注意的是,向使用者發放任何獎勵都存在風險,因為惡意使用者可以使用程式碼來模擬你的應用程式,並給自己發放大量獎勵。因此,Google 還支持一種 server to server 的模式。Google AdMob 伺服器將通知你的伺服器,使用者獲得了獎勵,進而在伺服器端消除了攻擊的風險。

Cloud Function

這個章節,我們已經見識了各種客戶端的技術,即這些操作是在裝置如手機,平板上執行。但大部分的應用程式還是需要一些伺服器端的支援。主要的原因有:

  • 安全性、信賴的整合程式碼:通常一些機敏資訊如第三方服務的憑證等我們會需要在伺服器上執行以確保安全。若放在客戶端惡意使用者可能會反向編譯取得這些資訊。
  • 權限:有一些操作會需要控管存取權限,例如資料庫的存取我們不會讓客戶端直接存取。
  • 批次處理:例如更新某個排行榜,一些操作在分別的設備上處理是不適合的。

還有很多理由,通常是關於安全和效能的議題,這個時候我們會需要伺服器來協助。

這時,若我們使用 Firebase 服務,那麼我們就可以使用 Cloud Function 來處理需要在伺服器端執行的程式碼。這些函式可以直接操作資料庫等等。也可以直接和 Firebase 驗證一起搭配使用。

Cloud Function 可以使用 JavaScript、TypeScript、Python 來撰寫伺服器端的程式碼。後續就可以使用 cloud_functions 插件直接從你的 Flutter 應用程式調用 Cloud Functions。

final HttpsCallable callable = FirebaseFunctions.instance.httpsCallable(
	'storeReward',
  options: HttpsCallableOptions(timeout: Duration(seconds: 30))
);

try {
  await callable.call({
    "email": email,
    "reward": 30
  });
} catch (error) {
  print(error);
}

上面的範例,第一行定義了我們希望呼叫的 Cloud Function 的名稱 storeReward,還有 timeout 時間(30 秒)。

然後,在 try/catch 中,我們使用我們的參數呼叫 Cloud Function—— 這個例子的參數為電子郵件地址和獎勵點數。如果執行失敗,我們可以在 catch 中繼續處理失敗的情況。

ML 和 Google ML Kit

Google ML Kit 協助我們在 app 加入機器學習功能。我們不需要深入了解神經網路或模型優化即可使用例如條碼掃描,臉部偵測,文字辨識等功能。

Google ML Kit 提供的工具有:

  • 文字識別/光學字元辨識(OCR):識別照片中的文字。可以在設備上或透過雲端功能使用。
  • 臉部偵測:識別特定人物特徵。
  • 條碼掃描:掃描多種類型的條碼,可以在裝置上使用。
  • 圖像標記:識別圖片中的實體物件。
  • 地標識別:識別圖像中的知名地標。
  • 語言識別:識別一段文字的語言。
  • 自訂模型:自訂 TensorFlow 模型。

標記 on-device 的 API 可以離線執行。cloud-based API 需要 Google Cloud Platform 服務支援。要使用上面提到的相關功能可安裝插件 google_ml_kit

訊息

Firebase 也可以管理推播通知。你可以在伺服端或 Firebase 管理介面發送推播通知,我們可以應用程式在收到特定通知時執行對應的行為。 相較其他服務,推播的設定稍微困難,會有比較多的設定在伺服器端,如果訊息無法正確接收通常是設定的問題。建議在設定時多注意細節。同樣的如果我們要使用這個功能所需的插件是 firebase_messaging

要發送訊息到裝置上,我們需要先取得裝置的註冊 Token。

final fcmToken = await FirebaseMessaging.instance.getToken();

當我們取得 Token 之後,通常要儲存起來例如儲存在 Firestore。接著我們就可以推播通知了。

如果需要根據收到的推播進行操作則可以如下:

FirebaseMessaging.onMessage.listen((RemoteMessage msg) {
  print('$msg');
});

上面的程式碼我們註冊了一個監聽,當收到訊息的時候程式會執行對應的 callback。希望這個關於 Firebase 的章節讓你了解 Firebase 的強大功能,以及和 Flutter 無縫整合。

Google Map 和 Place

大概率你應該在在手機上使用過 Google Map 導航,但關於 Google Place 相較之下比較沒那麼明顯。通常我們在輸入框輸入地址的時候彈出完整地址建議的下拉選單就用到了 Google Place。

Google Place 通常就是用來搜尋地址以及提供相關資訊,而 Google Map 則是在地圖上顯示該地址。很多插件支援 Google Map 和 Google Place。

Google Map 的部分這裡推薦 google_maps_flutter 這是由 Flutter 團隊開發的。在 pub.dev 網站上你也可以查詢 flutter.dev 團隊開發的插件。

在 pub.dev 我們可以找到一些專門的 Google Place 插件,但另一個選擇是 google_maps_webservice 插件,它整合了 Google Maps Web 服務 API,可以存取 Places API 和其他 API,例如 Directions、Time Zone 和 Distance Matrix。

請注意,使用任何形式的 Google Map 都需要 API 金鑰,且 Google Place 是付費的服務,會通過 API 金鑰來計算費用。下面是使用 google_maps_webservice 搭配 google_maps_flutter 的範例:

final places = GoogleMapsPlaces(apiKey: "");
PlacesDetailsResponse response = await places.getDetailsByPlaceId("placeId");
LocationDetails location = response.result.geometry.location;

Set<Marker> markers = Set.from([
  Marker(
  	position: LatLng(location.lat, location.lng),
    markerId: MarkerId(response.result.placeId),
  ),
]);

Widget map = GoogleMap(
	mapType: MapType.hybrid,
  markers: markers,
  initialCameraPosition: CameraPosition(
  	zoom: 15,
    target: LatLng(locationDetails.lat, locationDetails.lng),
  ),
);

上面的範例使用 placeId 查詢一個位置資訊,然後使用 GoogleMap 組件在地圖上顯示標記位置,更多詳細教學請參考官方文件

行動裝置功能

行動裝置如手機、平板本身支援了很多功能,我們的應用程式可以使用以提升使用者體驗。一般來說,我們需要針對裝置設定和開發,但在 Flutter 中,存取這些功能一般使用插件的方式實作。在這個章節,我們將快速瀏覽一些常用功能,例如相機、瀏覽器、本地儲存,影片播放等。

相機和 QR Code

手機和平台一個重要的功能就是相機。除了可以用來照相外,還可以用來掃描 QR Code,這裡我們快速看一下 cameraqr_code_scanner

關於 qr_code_scanner 插件包含了下面兩個主要功能:

  • QRView 組件可以在組件結構中使用,用於呈現一個相機擷取功能的區塊
  • QRViewController 可以掛載到 QRView 組件並使用 ScanData 收到掃描的資訊

注意在 iOS 平台,如果使用 QRCode 掃描或相機插件需要設定存取相機的原因,修改 info.plist

<key>NSCameraUsageDescription</key>
<string>Why my app needs to use the camera</string>

該訊息會通知使用者,表示應用程式正在存取相機,使用者可以決定是否授權。

下面讓我們來看看範例:

import 'package:flutter/material.dart';
import 'package:qr_code_scanner/qr_code_scanner.dart';

class QRPage extends StatefulWidget {
  const QRPage({ super.key });
  
  @override
  State<StatefulWidget> createState() => _QRPageState();
}

class _QRPageState extends State<QRPage> {
  String? scanCode;
  QRViewController? qrController;
  final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
  
  // Hot Reload 模式下解決相機問題
  @override
  void reassemble() {
    super.reassemble();
    if (Platform.isAndroid) {
      qrController!.pauseCamera();
    } 
    qrController!.resumeCamera();
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
    	body: Column(
      	children: <Widget>[
          Expanded(
          	child: QRView(
            	key: qrKey,
              onQRViewCreated: _onQRViewCreated,
            ),
          ),
          if (_scanCode != null)
          	Text("Code: ${scanCode}")
        ]
      ),
    );
  }
  
  void _onQRViewCreated(QRViewController controller) {
    setState(() {
      qrController = controller;
    });
    
    controller.scannedDataStream.listen((scanData) {
      setState(() {
        scanCode = scanData.code;
      });
    });
  }
  
  @override
  void dispose() {
    qrController?.dispose();
    super.dispose();
  }
  
}

另外可以預期如果你有了掃描 QR Code 的功能,那麼大概率你也需要產生 QR Code。這時可以使用 qr_flutter 插件。

QrImage(
	data: "資料",
  version: QrVersions.auto,
  size: 200.0,
)

開啟網頁

有時候我們需要從應用程式開啟網頁。可能是為了顯示服務條款、廣告或其他資訊。通常手機都會內建瀏覽器因此我們只需要開啟指定的網址即可。例如使用 url_launcher 插件就可以輕鬆完成這個任務。

void open(url) async {
  bool isValid = await canLaunch(url);
  if (isValid) {
    await launch(url);
  } else {
    print('Failed to launch $url');
  }
}

Local Storage

有時候我們無法使用線上儲存例如 Firebase 即時資料庫。此時我們可以選擇將資料存放在本地的 local storage。例如 shared_preferences 插件。

一般來說,這類型的插件可以讓我們將資料以鍵值的形式例如

SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString('name', name);

若你希望儲存更多資料,那麼可以使用 sqflite 插件。這個插件可以使用 SQLite 資料庫並完整支援查詢,更新,刪除等操作。同時它也支援交易 transaction 和批次更新的功能。但注意,在 Flutter 網頁版應用程式目前不支援。

影片

裝置一般都可以播放影片,嵌入影片到應用程式 。video_player 插件可以輕易的設定支援播放加入 assets 目錄或者網路上檔案的影片。

如果你想使用串流影音,例如 YouTube 那麼可以使用專屬的 YouTube 插件。其中主流使用 youtube_player_flutter可以支援控制器和影片自動播放等功能。

金流

若你的應用程式希望支援付款功能,那麼你需要使用金流支付供應商提供的服務,並且最好支援 Flutter 。以全球來說,主流的供應商如 Stripe 和 Square 。兩者都需要提供伺服器的部分來處理付款,因此這個實作流程一般會比較困難。

上面提到的兩個服務都有支援 Apple 和 Google Pay,因此可以很大程度的提高良好的支付體驗。

至於台灣本地的金流供應商,在沒有 SDK 支援的情況下一般得自己串接 API,甚至使用 webview 的方式處理。

另外,如果你計畫使用 in-app 付款,那麼可以使用 in_app_purchase 插件,適合一次性購買,而不需要處理訂閱和取消的問題。若需要提高訂閱服務,則將需要伺服器端處理來自 Apple 或 Google 的續訂,取消流程。

若是需要在應用程式之外進行購買(通常是為了避免 in-app 付款抽成)那麼可以考率使用 RevenueCat 該服務可以管理所有購買、訂閱續訂和取消的功能,還可以整合 Stripe。它們的插件為 purchases_flutter 使用前需要測試帳號。

開啟檔案

跟網頁應用程式不同,網頁上的文件通常是在支援多種檔案類型的電腦上開啟,而應用程式通常會受到比較多的限制,因此需要一種開啟文件的方式。open_file 插件可以管理這個流程。使用 InkWell 或按鈕讓使用者可以點擊並開啟檔案,你可以在 onPressedonTap 方法中使用下面程式:

OpenFile.open('location');

支援類型插件

最後這些想提到一些在應用程式發佈之後提供支援的插件。例如 Crashlytics 可以記錄錯誤例外,任何應用程式基本都需要有回報錯誤或異常的功能,尤其在 Android 上,裝置類型眾多根本無法在所有裝置上進行測試額外需要。

App Version

雖然大部分的裝置都支援自動更新,但有些人不管是基於網路流量或其他因素選擇手動更新,進而持續使用老舊版本。此時如果問題發生,使用者在回報問題的時候確認版本是很重要。

package_info_plus 插件使用這個情境,它可以讀取 pubspec.yaml 的執行,並在組件中顯示這些訊息。將這些資料加入 app 或者在登入啟動之前提供也是很有幫助的,可以幫助用戶了解狀況。

_initialiseVersionInfo() async {
  PackageInfo info = await PackageInfo.fromPlatform();
  String version = "${info.version}-${info.buildNumber}";
  setState(() {
    _version = version;
  });
}

裝置資訊

另外當我們在除錯的時候,了解設備的資訊也是關鍵,通常使用者會知道設備的型號,當軟體版本就不是那麼直接了。 device_info_plus 插件可以獲取這些資訊,在查找問題的時候可以提供幫助

DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
print('Running on ${androidInfo.model}');

在這個章節,我們看了許多 Flutter 的插件,也大概了解面對各種需求或問題的方向,但上面都只是概略的介紹,具體的使用還需要深入參考各自官網的教學和文件。


上一篇
Day 19 套件管理
下一篇
Day 21 動畫
系列文
Flutter 開發實戰 - 30 天逃離新手村26
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言