還在學iOS?是時候學習Flutter了(二)

RiverLi發表於2019-06-06

概述

本文承接上文,是Flutter For iOS 的第二篇文章,通過閱讀本文你將獲取如下資訊:

  • 執行緒和非同步
  • 專案結構與本地化
  • 檢視控制器
  • 佈局
  • 手勢
  • 表單
  • 列表
  • 其他

執行緒和非同步

如何寫非同步程式碼

Dart擁有單執行緒執行模型,同時也支Isolate (一種將Dart程式碼執行在另一個執行緒的方式)、事件迴圈和非同步程式設計。除非你建立一個Isolate ,你的Dart程式碼將一直在主UI執行緒中執行,並由事件迴圈驅動。Flutter的事件迴圈相當於iOS中的主迴圈,也就是說Looper 繫結在主執行緒上。

Dart的單執行緒模型並不意味著你必須將一切程式碼作為一個導致UI卡頓的阻塞塊來執行。相反,你可以使用Dart提供的非同步功能比如說:async/awiat 來執行非同步任務。

比如說,你可以使用asyn/await執行網路程式碼和繁重的工作而避免UI卡頓。

還在學iOS?是時候學習Flutter了(二)

一旦網路請求結束,通過呼叫setState()更新UI,觸發當前widget的子樹和更新資料。

下面例子非同步載入資料並展示在ListViews上:

還在學iOS?是時候學習Flutter了(二)

參考下一節瞭解如何在後臺執行緒執行任務,與iOS有何不同。

如何將任務放到後臺執行緒

由於Flutter的單執行緒模型和事件迴圈,你不用擔心執行緒管理或者開啟後臺執行緒。你可以放心的使用async/await方法執行I/O操作,比如訪問磁環或者請求網路。另一方面,如何你想執行復雜的計算而使CPU持續的處於繁忙狀態,你可以將任務已到Isolate而避免阻塞事件迴圈。

對於iOS操作,將方法宣告為async方法,使用await等待耗時任務完成。

loadData() async {
  String dataURL = "https://jsonplaceholder.typicode.com/posts";
  http.Response response = await http.get(dataURL);
  setState(() {
    widgets = json.decode(response.body);
  });
}
複製程式碼

這是對常的I/O操作如網路請求,訪問資料庫的常規操作。

但是,當你處理大量資料的時候這仍然可能會導致UI掛起。在Flutter中,使用Isolate 來使用CPU多核的優勢來執行耗時任務或者計算密集型任務。

Isolates 是分離執行緒,它不和主執行緒共享任何堆記憶體,這也就意味著,你不能訪問主執行緒中的變臉,或者直接呼叫setState()更新主執行緒。Isolates正如其名,不能共享記憶體。

下面程式碼展示了一個簡單的isolate, 如何將資料返回到主執行緒並更新UI的。

還在學iOS?是時候學習Flutter了(二)

上面程式碼中,dataLoader()Isolate,它在一個獨立的執行緒中執行。在這個isolate中你可以執行CPU密集型任務如解析JSON,或者執行浮躁的數學計算任務,如加密或者訊號處理。

你可以執行完整程式碼,如下:

還在學iOS?是時候學習Flutter了(二)

如何發生網路請求

在Flutter中使用流行的第三方庫http package 來請求網路是非常簡單的。它抽象了大量的本需要你自己實現的操作,使得傳送請求非常簡單。

為了使用http這個框架,你需要在pubspec.yaml中增加依賴。

dependencies:
  ...
  http: ^0.11.3+16
複製程式碼

為了發起網路請求,在async方法http.get() 前新增await

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
[...]
  loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = json.decode(response.body);
    });
  }
}
複製程式碼

如何展示耗時任務的進度

在iOS中,當在後臺執行一個耗時任務的時候,你通過會使用UIProgressView展示進度。

在Flutter中,使用ProgressIndicator 元件。通過給它傳遞一個布林標識來控制它的展示,告訴Flutter去更新它的狀態在耗時任務執行之前和執行結束之後隱藏掉它。

在下面的例子中,build方法被分割為三個不同方法。如果showLoadingDialog()是true,那就渲染ProgressIndicator 否則使用網路返回的資料渲染ListView

還在學iOS?是時候學習Flutter了(二)

專案結構、本地化、依賴和資源管理

如何在Flutter中管理圖片,如何放置多種解析度的圖片

與iOS將圖片和資源作為不同的型別來處理不同的是Flutter中只有一種assets。iOS中資源被放在Image.xcassert中檔案中,而Flutter中放在assets檔案中。與iOS一樣,assets是許多型別的檔案,不僅僅是圖片,比如說你可以將json檔案放到my-assets資料夾中。

my-assets/data.json
複製程式碼

pubspec.yaml檔案中宣告:

assets:
	- my-assets/data.json
複製程式碼

然後就可以在程式碼中使用AssetBunlde訪問:

import 'dart:async' show Future;
import 'package:flutter/services.dart' show rootBundle;

Future<String> loadAsset() async {
  return await rootBundle.loadString('my-assets/data.json');
}
複製程式碼

對於圖片,Flutter和iOS的格式一樣,圖片可以是1倍圖,2倍圖,3倍圖或者其他任何倍數。這些所謂的 devicePixelRatio 表示的是物理畫素到單個邏輯畫素的比率。

Assets可以被放到任何型別的資料夾中,Flutter中沒有事先預定義檔案的結構。在pubSpec.yaml檔案中宣告assets,然後Flutter就能識別出來。

比如說:將my_icon.png放置到Flutter專案中,你可能把儲存的資料夾叫作images。把相關係數的圖片放在不同的子檔案家中,如下:a

images/my_icon.png       // Base: 1.0x image
images/2.0x/my_icon.png  // 2.0x image
images/3.0x/my_icon.png  // 3.0x image
複製程式碼

接下來在pubspec.yaml中宣告圖片

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

你現在就可以使用AssetImage返回圖片

return AssetImage("images/a_dot_burr.jpeg");
複製程式碼

或者直接使用Image元件

@override
Widget build(BuildContext context) {
  return Image.asset("images/my_image.png");
}
複製程式碼

更多細節參考Adding Assets and Images in Flutter

如何存放字串,如何管理本地化

iOS中,我們使用Localizable.strings檔案管理本地化字串,而Flutter中沒有專門的模組處理本地化字串,所以最好的辦法就是將字串統一放到一個類中,以靜態欄位的形式儲存。如下:

class Strings {
  static String welcomeMessage = "Welcome To Flutter";
}
複製程式碼

訪問方式如下:

Text(Strings.welcomeMessage)
複製程式碼

預設情況下,Flutter只支援英文字串,如果你想支援其他語言,可以通過引入flutter_localizations庫。 同時你需要將Dart的intl包以便支援 i10n 機制,比如日期/時間格式化。

dependencies:
  # ...
  flutter_localizations:
    sdk: flutter
  intl: "^0.15.6"
複製程式碼

為了使用flutter_localizations ,需要在App widget上指定 localizationsDelegatessupportedLocales 屬性。

import 'package:flutter_localizations/flutter_localizations.dart';

MaterialApp(
 localizationsDelegates: [
   // Add app-specific localization delegate[s] here
   GlobalMaterialLocalizations.delegate,
   GlobalWidgetsLocalizations.delegate,
 ],
 supportedLocales: [
    const Locale('en', 'US'), // English
    const Locale('he', 'IL'), // Hebrew
    // ... other locales the app supports
  ],
  // ...
)
複製程式碼

代理中包含了實際的本地化值,supportedLocales定義了要支援那些語言的本地化。上面的例子使用的是MaterialApp, 它既有針對基本Widget的本地化值GlobalWidgetsLocalizations,也有針對Material widget的MaterialWidgetsLocalizations本地化。如果你的App使用的是WidgetApp,那麼後者就不需要了。值得注意的是這兩個代理都包含預設值,但如果你想讓你的App本地化,你扔需要提供一個或者多個代理作為你的App本地化副本。

當初始化完成的時候,WidgetsApp或者MaterialApp使用你指定的代理為你建立了一個Localizationswidget。你可從LocalizationsWidget中隨時訪問當前裝置的本地化資訊,或者使用window.locale

為了訪問本地化資源,使用Localizations.of()方法訪問有給定的delegate提供的特有的本地化類。使用intl_translation取出翻譯副本到 arb 檔案中。將它們引入App中,並用intl來使用它們。

更多國際化和本地化的內容參考: internationalization guide,它包含了不使用intl示例程式碼。

需要注意的是:Flutter1.0 beta2 之前 fullter中定義的資原始檔不能被原生訪問,同時原生定義的資源不能被flutter訪問,因為它們儲存在不能的檔案目錄下。

如何管理依賴

在iOS中,我們將依賴新增到Podfile檔案中,Flutter使用的是Dart語言構建的系統和Pub包管理器操作依賴。這些工具將原生 Android 和 iOS 包裝應用程式的構建委派給相應的構建系統。

如果在你的Flutter專案中iOS目錄下包含Podfile,只需要使用它新增iOS原生的依賴。使用 pubspec.yaml 宣告Flutter 中的外部依賴。 Pub網站可以找到一些比較好用的第三方依賴。

檢視控制器

Flutter中與ViewControllers相等的元素是什麼?

在iOS中,ViewController表示使用者介面的一部分,通常表示一個螢幕或者部分螢幕。多個ViewController組合在一起構造複雜的使用者介面,並幫助你規整應用的UI部分。在Flutter中,這項工作落在了Widget頭上,正如導航那一個章節提到的,螢幕由Widget所表示,因"一切都是Widget"。使用Navigator在不同的路由間切換表示不同的螢幕或者頁面或者表示不同的狀態或者渲染相同的資料。

如何監聽iOS的生命週期事件

在iOS中,你可以重寫ViewController中的方法來捕獲檢視的生命週期,或者在AppDelegate中註冊生命週期的回撥。在Flutter中沒有這兩個概念,但是我們可以通過hookWidgetsBinding並在didChangeAppLifecycleState()方法中監聽生命週期事件。

能夠監聽到的生命週期事件如下:

  • Inactive — 應用程式處於不活躍狀態,不能相應使用者輸入。該事件只在iOS中有效。
  • paused — 應用程式當前不可用,不響應使用者輸入,但是還在後臺執行。
  • resumed — 應用程式可用,並能響應使用者輸入。
  • suspending — 應用程式暫時被掛起。該事件只在Android系統上有效。

更多細節參考:AppLifecycleStatus documentation

佈局

Flutter中的UITableViewUICollectionView

Flutter中使用ListView實現iOS中的UITableViewUICollectionView。實現程式碼如下:

還在學iOS?是時候學習Flutter了(二)

如何知道那個cell被點選

在iOS中,通過實現 tableView:didSelectRowAtIndexPath:方法來相應cell的點選事件,在Flutter中,使用所包含的widget本身提供的事件來處理相應。

還在學iOS?是時候學習Flutter了(二)

如何動態更新ListView

在iOS中,我們使用reloadData來重新整理表格檢視。

在Flutter中,如果更新setState()中的小部件列表,你會發現列表資料沒有發生變化。這是因為當呼叫setState()時,Flutter呈現引擎會檢視widget樹以檢視是否有任何更改。當它到達ListView時,它執行==檢查,並確定兩個ListView是相同的。沒有任何改變,因此不需要更新。

在setState()方法內建立一個新List是更新ListView的一個簡單的方法。並將舊列表中的資料複製到新列表中。雖然這種方法很簡單,但不建議用於大型資料集,如下一個示例所示。

還在學iOS?是時候學習Flutter了(二)

我們推薦使用ListView.Builder來構建列表,它比較高效。當你的列表包含大量資料的列表時,此方法非常有用。

還在學iOS?是時候學習Flutter了(二)

與建立一個ListView不同的是,建立ListView.builder 攜帶兩個引數:列表的初始長度和ItemBuilder方法。

ItemBuilder方法和iOS中的table或者collection的cellForItemAt代理相似,一樣的攜帶一個位置,並返回該位置需要渲染的cell。

最後也是最重要的,onTap方法並沒有重新建立一個list,而是.add了一個Widget。

如何使用類似ScrollView的功能

@override
Widget build(BuildContext context) {
  return ListView(
    children: <Widget>[
      Text('Row One'),
      Text('Row Two'),
      Text('Row Three'),
      Text('Row Four'),
    ],
  );
}
複製程式碼

手勢檢測和觸控事件處理

如何向widget新增一個事件監聽

如果widget支援事件處理,如RaisedButton,可以直接將相應方法傳遞給對應的屬性,如RaisedButton的onPressed。

Widget build(BuildContext context) {
  return RaisedButton(
    onPressed: () {
      print("click");
    },
    child: Text("Button"),
  );
}
複製程式碼

如果widget不支援事件處理,可以使用GestureDetector包裹一下,然後給onTap屬性傳遞一個方法。

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text('Sample App'),
    ),
    body: Center(
      child: GestureDetector(
        child: FlutterLogo(
          size: 200,
        ),
        onTap: () {
          print('taped');
        },
      ),
    ));
}
複製程式碼

如何處理widget上的其他型別的事件

我們可以使用 GestureDetector 來實現如下事件的監聽:

  • 單擊
    • onTapDown — 按下手勢事件
    • onTapUp — 抬起事件
    • onTap — 點選事件
    • onTapCancel — 取消點選事件,onTapDown發生,但onTap沒有發生。
  • 雙擊
    • onDoubleTap — 雙擊事件
  • 長按
    • onLongPress — 長按事件
  • 垂直拖動
    • onVerticalDragStart —開始垂直移動
    • onVerticalDragUpdate — 垂直移動進行中。
    • onVerticalDragEnd — 垂直移動結束。
  • 水平拖動
    • onHorizontalDragStart — 開始水平移動。
    • onHorizontalDragUpdate — 水平移動進行中。
    • onHorizontalDragEnd — 水平移動結束。

下面程式碼展示了使用 GestureDetector 實現雙擊事件:

還在學iOS?是時候學習Flutter了(二)

執行效果:

還在學iOS?是時候學習Flutter了(二)

主題和文字

如何為應用程式設定主題

Flutter提供了一套完美符合Material Design的主題,它幫你處理了大多數需要你自己處理的樣式和主題。

為了在你的App中充分發揮Material元件的優勢,在頂層元件上宣告MaterialApp,作為你的應用的入口。MaterialApp 是一個便利的元件,它包含了許多App通常需要的Materail Desigin風格的元件。它通過由給WidgetsApp增加MD功能實現的。

同時 Flutter 足夠地靈活和富有表現力來實現任何其他的設計語言。在 iOS 上,你可以用 Cupertino library 來製作遵守 Human Interface Guidelines 的介面。檢視這些 widget 的集合,請參閱 Cupertino widgets gallery

你也可以在你的 App 中使用 WidgetApp,它提供了許多相似的功能,但不如 MaterialApp那樣豐富。

對任何子元件定義顏色和樣式,可以給 MaterialApp widget 傳遞一個 ThemeData 物件。舉個例子,在下面的程式碼中,primary swatch 被設定為藍色,並且文字的選中顏色是紅色:

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        textSelectionColor: Colors.red
      ),
      home: SampleAppPage(),
    );
  }
}
複製程式碼

如何在Text widget上使用自定義字型

在 iOS 中,你在專案中引入任意的 ttf 檔案,並在 info.plist 中設定引用。在 Flutter 中,在資料夾中放置字型檔案,並在 pubspec.yaml 中引用它,就像新增圖片那樣。

fonts:
   - family: MyCustomFont
     fonts:
       - asset: fonts/MyCustomFont.ttf
       - style: italic
複製程式碼

然後在你的 Text widget 中指定字型:

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text("Sample App"),
    ),
    body: Center(
      child: Text(
        'This is a custom font text',
        style: TextStyle(fontFamily: 'MyCustomFont'),
      ),
    ),
  );
}
複製程式碼

如何設定Text widget的樣式

除了字型以外,你也可以給 Text widget 的樣式元素設定自定義值。Text widget 接受一個 TextStyle 物件,你可以指定許多引數,如下:

  • color
  • decoration
  • decorationColor
  • decorationStyle
  • fontFamily
  • fontSize
  • fontStyle
  • fontWeight
  • hashCode
  • height
  • inherit
  • letterSpacing
  • textBaseline
  • wordSpacing

表單輸入

表單在Flutter中如何工作的,如何取回使用者輸入的值

在iOS中,我們通常在使用者提交的時候獲取元件上的內容,對於具有使用獨立狀態的不可變元件的Flutter來講,你可能會好奇如何獲取使用者輸入內容。

對於表單操作而言,與其他功能一樣也是通過特定的Widget實現的。通過使用 TextField或者TextFormField 可以通過 TextEditingController 取回輸入內容。

示例程式碼如下:

還在學iOS?是時候學習Flutter了(二)

執行效果

還在學iOS?是時候學習Flutter了(二)

更多資訊參考: Flutter CookbookRetrieve the value of a text field

如何實現類似文字輸入框佔位符的功能

通過給decoration屬性傳遞一個InputDecoration物件來給TextField實現佔位符的功能。

body: Center(
  child: TextField(
    decoration: InputDecoration(hintText: "This is a hint"),
  ),
)
複製程式碼

如何展示驗收錯誤資訊

與上面程式碼一樣,只不過是再新增一個errorText欄位,通過state控制錯誤資訊的提示。

示例程式碼如下:

還在學iOS?是時候學習Flutter了(二)

執行效果:

還在學iOS?是時候學習Flutter了(二)

與硬體、第三方服務和平臺的互動

如何與平臺和平臺原生程式碼互動

Flutter不是在直接在平臺下執行程式碼的,相反,由Dart語言構建的FlutterApp在裝置本機執行,"迴避"平臺提供的SDK。比如說:在Dart中傳送一個網路請求,它是直接在Dart上下文中執行的,而不適用我們在寫原生App的時候所使用的Android或者iOSAPI。我們的FlutterApp仍然被原生app的ViewController當做一個View所持有,但我們不用直接訪問ViewController或者原生框架。

這並不意味著Flutter應用不能與原生API或者其他你寫的原生程式碼互動。Flutter提供了 platform channels,它可以與持有你Flutter檢視的VIewController通訊或者交換資料。platform channels 本質上是一個非同步通訊機制,橋接了Dart程式碼和其宿主ViewController,iOS框架。比如說。你可以用platform channels執行一個原生的函式,或者是從裝置的感測器中獲取資料。

除了直接使用platform channels之外,你還可以使用一系列預先製作好的 plugins。例如,你可以直接使用外掛來訪問相機膠捲或是裝置的攝像頭,而不必編寫你自己的整合層程式碼。你可以在 Pub 上找到外掛,這是一個 Dart 和 Flutter 的開源包倉庫。其中一些包可能會支援整合 iOS 或 Android,或兩者均可。

如果你在 Pub 上找不到符合你需求的外掛,你可以自己編寫 ,並且釋出在 Pub 上

如何訪問GPS感測器

使用 geolocator

如何訪問相機

使用 image_picker

如何使用FaceBook登陸

使用 flutter_facebook_login

如何使用Firebase

大多數 Firebase 特性被 first party plugins 包含了。這些第一方外掛由 Flutter 團隊維護:

如何建立原生整合層程式碼

如果有一些 Flutter 和社群外掛遺漏的平臺相關的特性,可以根據 developing packages and plugins 頁面構建自己的外掛。 Flutter 的外掛結構,簡要來說,就像 Android 中的 Event bus。你傳送一個訊息,並讓接受者處理並反饋結果給你。在這種情況下,接受者就是在 Android 或 iOS 上的原生程式碼。

資料庫和本地儲存

如何在Flutter中使用UserDefaults

在iOS中,我們可以使用UserDefaults 來儲存鍵值對集合,在Flutter中,可以使用 Shared Preferences plugin外掛來顯示類似的功能。 這個外掛包裝了UserDefaults和Android 上的 SharedPreferences

Flutter中和Coredata相等的功能。

可以使用 SQFlite 外掛實現iOS中CoreData相關的功能。

通知

如何設定推送通知

在iOS,你需要在開發者網站上註冊app以便獲取推送許可權。在Flutter中使用firebase_messaging 外掛可以實現推送。 更多關於使用Firebase Cloud Messaging API的文件請參考: firebase_messaging

參考

本文主要參考Flutter官方文件,Flutter中文網。 由於排版原因,文中我使用了圖片的形式展示程式碼,如果你需要原始碼,可以關注我的公眾號,回覆關鍵字"flutter"獲取相關程式碼。

本文首發自微信公眾號:RiverLi

相關文章