官方文檔
- Flutter官方開發文檔地址: https://flutter.dev/docs
- Flutter中文開發文檔地址: https://flutterchina.club/docs/
預研內容主要分爲幾個部分:
- Flutter環境搭建(技術框架, 安裝,編輯器)
- Flutter的UI及交互(佈局,交互,手勢,動畫,路由導航)
- Flutter資源文件管理(圖片資源,文件資源,文件的讀寫)
- Flutter數據存儲
- Flutter的網絡請求(網絡HTTP,JSON序列化)
- Flutter與原生平臺(平臺特定代碼交互)
- Flutter開發語言(Dart語言)
- Flutter其他
Flutter環境搭建
- 技術框架
在Flutter中用Widget來描述界面,Widget只是View的“配置信息”,編寫的時候利用Dart語言一些聲明式特性來得到類似結構化標記語言的可讀性。Widget根據佈局形成一個層次結構。每個widget嵌入其中,並繼承其父項的屬性。沒有單獨的“應用程序”對象,相反,根widget扮演着這個角色。在Flutter中,一切皆爲Widget,甚至包括css樣式。
- Flutter環境安裝
mac下終端操作:
vim ~/.bash_profile 添加路徑(沒有.bash_profile的時候,需要通過vim命令創建)
export PUB_HOSTED_URL=https://pub.flutter-io.cn //國內用戶需要設置
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn //國內用戶需要設置
export PATH=PATH_TO_FLUTTER_GIT_DIRECTORY/flutter/bin:$PATH
修改 ~/.zshrc ,在其中添加:source ~/.bash_profile (沒有.zshrc的時候,需要通過vim命令創建)
- 編輯器
Android Studio 安裝
安裝Flutter和Dart插件
在Flutter插件中,可使用以下模板:
前綴stless: 創建一個StatelessWidget的子類.
前綴stful: 創建一個StatefulWidget子類並且關聯到一個State子類.
前綴stanim: 創建一個StatefulWidget子類, 並且它關聯的State子類包括一個 AnimationController
Flutter的UI及交互
- 佈局
Text:該 widget 可讓創建一個帶格式的文本。
Row、 Column: 這些具有彈性空間的佈局類Widget可讓您在水平(Row)和垂直(Column)方向上創建靈活的佈局
Stack: 取代線性佈局 (譯者語:和Android中的LinearLayout相似),Stack允許子 widget 堆疊
Container: Container 可讓您創建矩形視覺元素。container 可以裝飾爲一個BoxDecoration
爲了繼承主題數據,widget需要位於MaterialApp內才能正常顯示, 因此我們使用MaterialApp來運行該應用。 //Scaffold是Material中主要的佈局組件.
GestureDetector widget並不具有顯示效果,而是檢測由用戶做出的手勢。 當用戶點擊Container時, GestureDetector會調用它的onTap回調, 在回調中,將消息打印到控制檯。您可以使用GestureDetector來檢測各種輸入手勢,包括點擊、拖動和縮放。
StatefulWidgets是特殊的widget,它知道如何生成State對象,然後用它來保持狀態
在StatefulWidget調用createState之後,框架將新的狀態對象插入樹中,然後調用狀態對象的initState。 子類化State可以重寫initState,以完成僅需要執行一次的工作。 例如,您可以重寫initState以配置動畫或訂閱platform services。initState的實現中需要調用super.initState當一個狀態對象不再需要時,框架調用狀態對象的dispose。 您可以覆蓋該dispose方法來執行清理工作。例如,您可以覆蓋dispose取消定時器或取消訂閱platform services。 dispose典型的實現是直接調用super.dispose。
stateless widget 沒有內部狀態. Icon、 IconButton, 和Text 都是無狀態widget, 他們都是 StatelessWidget的子類。stateful widget 是動態的. 用戶可以和其交互 (例如輸入一個表單、 或者移動一個slider滑塊),或者可以隨時間改變 (也許是數據改變導致的UI更新)
可以使用key來控制框架將在widget重建時與哪些其他widget匹配。默認情況下,框架根據它們的runtimeType和它們的顯示順序來匹配。 使用key時,框架要求兩個widget具有相同的key和runtimeType。
各種widgets的目錄索引,有UI不熟悉的,可以在這裏找到說明:https://flutterchina.club/widgets/
各類Widgets的地址: https://flutterchina.club/widgets/basics/
各種UI佈局需要用到的控件介紹: https://flutterchina.club/widgets/material/
iOS 風格的控件集合 介紹: https://flutterchina.club/widgets/cupertino/
- UI的一些注意事項
類MyAppBar和MyScaffold中使用了Container、Row、Column、Text、IconButton、Icon、BoxDecoration、Center、Expanded等常用Widget
Theme.of(context)將查找Widget樹並返回樹中最近的Theme。如果我們的Widget之上有一個單獨的Theme定義,則返回該值。如果不是,則返回App主題。 事實上,FloatingActionButton真是通過這種方式找到accentColor的!
ListView的構造函數需要一次創建所有項目,但ListView.builder的構造函數不需要,它將在列表項滾動到屏幕上時創建該列表項。
new ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return new ListTile(
title: new Text('${items[index]}'),
);
},
);
滑動刪除有直接可用的Widget;
將響應轉換爲自定義Dart對象;
class Post {
final int userId;
final int id;
final String title;
final String body;
Post({this.userId, this.id, this.title, this.body});
factory Post.fromJson(Map<String, dynamic> json) {
return new Post(
userId: json['userId'],
id: json['id'],
title: json['title'],
body: json['body'],
);
}
}
http package提供了一種方便的方法來爲請求添加headers。您也可以使用dart:iopackage來添加。
Flutter提供各種按鈕和類似的交互式widget。這些widget中的大多數實現了Material Design 指南, 它們定義了一組具有質感的UI組件。可以使用GestureDetector來給任何自定義widget添加交互性。 可以在管理狀態和Flutter Gallery中找到GestureDetector的示例。
如果你要構建一個 CustomButton ,並在構造器中傳入它的 label?那就組合 RaisedButton 和 label,而不是擴展 RaisedButton。
Isolates 是分離的運行線程,並且不和主線程的內存堆共享內存。這意味着你不能訪問主線程中的變量,或者使用 setState() 來更新 UI。正如它們的名字一樣,Isolates 不能共享內存。
在 Flutter 中,最簡單的方法是使用 ListView widget。它表現得既和 iOS 中的 ScrollView 一致,也能和 TableView 一致,因爲你可以給它的 widget 做垂直排布:
- 交互
- 手勢
要從widget層監聽手勢,使用 GestureDetector.
- 動畫
參考動畫的說明: https://flutterchina.club/animations/
在 Flutter 中,使用 AnimationController 。這是一個可以暫停、尋找、停止、反轉動畫的 Animation<double> 類型。它需要一個 Ticker 當 vsync 發生時來發送信號,並且在每幀運行時創建一個介於 0 和 1 之間的線性插值(interpolation)。你可以創建一個或多個的 Animation 並附加給一個 controller。
- 路由
路由的介紹:https://docs.flutter.io/flutter/widgets/Navigator-class.html
在頁面之間跳轉,你有幾個選擇:
具體指定一個由路由名構成的 Map。(MaterialApp
直接跳轉到一個路由。(WidgetApp)
Navigator 類不僅用來處理 Flutter 中的路由,還被用來獲取你剛 push 到棧中的路由返回的結果。通過 await等待路由返回的結果來達到這點。
Map coordinates = await Navigator.of(context).pushNamed('/location');
Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});
Flutter資源文件管理
- 圖片資源
iOS 把 images 和 assets 作爲不同的東西,而 Flutter 中只有 assets。被放到 iOS 中 Images.xcasset 文件夾下的資源在 Flutter 中被放到了 assets 文件夾中。assets 可以是任意類型的文件,而不僅僅是圖片。
例如,你可以把 json 文件放置到 my-assets 文件夾中。在 pubspec.yaml 文件中聲明 assets:`assets:
- my-assets/data.json
然後在代碼中使用 AssetBundle 來訪問它:
Future<String> loadAsset() async {
return await rootBundle.loadString('my-assets/data.json');
}`
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
- 文件的讀寫
PathProvider 插件提供了一種平臺透明的方式來訪問設備文件系統上的常用位置。該類當前支持訪問兩個文件系統位置:
Flutter數據存儲
在 Flutter 中,可以使用 Shared Preferences plugin 來達到相似的功能。它包裹了 UserDefaluts 以及 Android 上等價的 SharedPreferences 的功能。
在 iOS 中,你通過 CoreData 來存儲結構化的數據。這是一個 SQL 數據庫的上層封裝,讓查詢和關聯模型變得更加簡單。在 Flutter 中,使用 SQFlite 插件來實現這個功能。
Flutter的網絡請求
- 網絡HTTP
使用dio 來發起網絡請求,它是一個強大易用的dart http請求庫,支持Restful API、FormData、攔截器、請求取消、Cookie管理、文件上傳/下載
參見具體說明: https://flutterchina.club/networking/
- JSON序列化
使用dart:convert庫可以簡單解碼和編碼JSON;要對簡單的JSON進行編碼,請將簡單值(字符串,布爾值或數字字面量)或包含簡單值的Map,list等傳給encode方法:
Flutter與原生平臺
- 平臺特定代碼交互
通過MethodChannel實現。在Flutter當中定義平臺管道,定義平臺需要捕獲的方法名稱->Appdelegate當中註冊定義的管道->管道方法調用的時候,實現平臺方法的調用(達到Flutter調用平臺方法的目的)
- Flutter頁面路由 與 原生頁面之間跳轉實現:
Flutter 的代碼並不直接在平臺之下運行,相反,Dart 代碼構建的 Flutter 應用在設備上以原生的方式運行,卻“側步躲開了”平臺提供的 SDK。這意味着,例如,你在 Dart 中發起一個網絡請求,它就直接在 Dart 的上下文中運行。你並不會用上平常在 iOS 或 Android 上使用的原生 API。你的 Flutter 程序仍然被原生平臺的 ViewController 管理作一個 view,但是你並不會直接訪問 ViewController 自身,或是原生框架。
我怎麼訪問 GPS 傳感器?使用 location 社區插件。
我怎麼訪問攝像頭?image_picker 在訪問攝像頭時非常常用。
Flutter開發語言
- Dart語言一些語法特性:
所有沒有初始化的變量值都是 null。
注意: 只有當名字衝突的時候才使用 this。否則的話, Dart 代碼風格樣式推薦忽略 this。
注意: 如果在構造函數的初始化列表中使用 super(),需要把它放到最後。 詳情參考 Dart 最佳實踐。
在構造函數體執行之前除了可以調用超類構造函數之外,還可以 初始化實例參數。 使用逗號分隔初始化表達式。
class Point {
num x;
num y;
Point(this.x, this.y);
// Initializer list sets instance variables before
// the constructor body runs.
Point.fromJson(Map jsonMap)
: x = jsonMap['x'],
y = jsonMap['y'] {
print('In Point.fromJson(): ($x, $y)');
}
}
有時候一個構造函數會調動類中的其他構造函數。 一個重定向構造函數是沒有代碼的,在構造函數聲明後,使用 冒號調用其他構造函數。
Point.alongXAxis(num x) : this(x, 0);
如果一個構造函數並不總是返回一個新的對象,則使用 factory 來定義 這個構造函數。例如,一個工廠構造函數 可能從緩存中獲取一個實例並返回,或者 返回一個子類型的實例。
class Logger {
final String name;
bool mute = false;
// _cache is library-private, thanks to the _ in front
// of its name.
static final Map<String, Logger> _cache =
<String, Logger>{};
factory Logger(String name) {
if (_cache.containsKey(name)) {
return _cache[name];
} else {
final logger = new Logger._internal(name);
_cache[name] = logger;
return logger;
}
}
Logger._internal(this.name);
void log(String msg) {
if (!mute) {
print(msg);
}
}
}
Abstract methods(抽象函數): 實例函數、 getter、和 setter 函數可以爲抽象函數, 抽象函數是隻定義函數接口但是沒有實現的函數,由子類來 實現該函數。如果用分號來替代函數體則這個函數就是抽象函數。
abstract class Doer {
// ...Define instance variables and methods...
void doSomething(); // Define an abstract method.
}
class EffectiveDoer extends Doer {
void doSomething() {
// ...Provide an implementation, so the method is not abstract here...
}
}
操作符可以被覆寫。 例如,如果你定義了一個 Vector 類, 你可以定義一個 + 函數來實現兩個向量相加。
如果你使用 noSuchMethod() 函數來實現每個可能的 getter 、setter、 以及其他類型的函數,你可以使用 @proxy 註解來避免警告信息:
枚舉類型通常稱之爲 enumerations 或者 enums, 是一種特殊的類,用來表現一個固定 數目的常量。枚舉的 values 常量可以返回 所有的枚舉值。
Mixins 是一種在多類繼承中重用 一個類代碼的方法。使用 with 關鍵字後面爲一個或者多個 mixin 名字來使用 mixin
class Musician extends Performer with Musical {
// ...
}
class Maestro extends Person
with Musical, Aggressive, Demented {
Maestro(String maestroName) {
name = maestroName;
canConduct = true;
}
}
如果你查看 List 類型的 API 文檔, 則可以看到 實際的類型定義爲 List<E>。 這個 <…> 聲明 list 是一個 泛型 (或者 參數化) 類型。 通常情況下,使用一個字母來代表類型參數, 例如 E, T, S, K, 和 V 等。T 是一個備用類型。這是一個類型佔位符, 在開發者調用該接口的時候會指定具體類型。
如果你導入的兩個庫具有衝突的標識符, 則你可以使用庫的前綴來區分。 例如,如果 library1 和 library2 都有一個名字爲 Element 的類, 你可以這樣使用:
import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;
// ...
Element element1 = new Element(); // Uses Element from lib1.
lib2.Element element2 = new lib2.Element(); // Uses Element from lib2.
Dart 有一些語言特性來支持 異步編程。 最常見的特性是 async 方法和 await 表達式。要使用 await,其方法必須帶有 async 關鍵字:
在一個方法上添加 async 關鍵字,則這個方法返回值爲 Future。 例如,下面是一個返回字符串 的同步方法:
String lookUpVersionSync() => '1.0.0';
如果使用 async 關鍵字,則該方法 返回一個 Future,並且 認爲該函數是一個耗時的操作。Future<String> lookUpVersion() async => '1.0.0'
;
在 await expression 中, expression 的返回值通常是一個 Future; 如果返回的值不是 Future,則 Dart 會自動把該值放到 Future 中返回。 Future 對象代表返回一個對象的承諾(promise)。 await expression 執行的結果爲這個返回的對象。 await expression 會阻塞住,直到需要的對象返回爲止。如果 await 無法正常使用,確保是在一個 async 方法中。 例如要在 main() 方法中使用 await, 則 main() 方法的函數體必須標記爲 async:
異步 for 循環具有如下的形式:使用 break 或者 return 語句可以 停止接收 stream 的數據, 這樣就跳出了 for 循環並且 從 stream 上取消註冊了。
await for (variable declaration in expression) {
// Executes each time the stream emits a value.
}
所有的 Dart 代碼在 isolates 中運行而不是線程。 每個 isolate 都有自己的堆內存,並且確保每個 isolate 的狀態都不能被其他 isolate 訪問。
第三方Flutter框架Demo 及文章參考
其他
- Flutter安裝包大小的問題?
在安卓的安裝包當中會根據工程的不同增加不同的大小。本地打的包和發佈的包大小也可能不一樣。
部分團隊經驗:在Release模式下,安卓端的安裝包大小增加約爲3~4M ,iOS端的安裝包大小增加約爲13~14M,實際增加的大小跟業務端代碼和資源大小有關係;
其他團隊的經驗,引入 Flutter 之前,漲樂財富通的安裝包爲 94MB,引入之後大小爲 100MB,發現增大了 6MB,這其中主要是引入了 Flutter 的 SDK,增加的大小在可以接受的範圍。
- Flutter的組件化集成(將Flutter代碼集成進現有的工程)?
對現有工程有侵入。不能完全按照官方的指導來集成。需要把Flutter編譯產物放入主工程。
對混合棧的管理,參考閒魚團隊的開源方案。
- 預研結果
採用Flutter框架,能夠滿足日常80%以上的基礎UI展示和常見需求。如果涉及複雜動畫或者特定平臺特性的調用,也可以使用Flutter的管道特性,和平臺進行交互實現。
Flutter的性能雖然不如官方宣稱60FPS,但是在流暢性上也接近原生,而且主要能夠在安卓和iOS平臺上實現統一且支持部分定製,儘管安裝包和內存上會有部分提升空間。
開發過程中遇到的實際問題及解決:
- 按鈕做倒計時功能
- 導航的時候,如果用Navigator.push處理,新的路由頁面如果用MaterailPageRoute創建,那麼 main.dart中 run 的需要是一個MaterailApp
- 在編輯的時候,有的時候編輯器輸入顏色的時候,會fetching Doc ,然後AS就會請求文檔地址,網絡阻塞造成卡死。 根據查找的資料,在mac下需要找到 ~/Library/preference/Android studio x.x下面的jav.table.xml這樣的文件,替換裏面JAVDOC節點下的文檔來源地址url,或者有解決方式是直接刪除掉這個文件。 試了一下刪除,效果不是很好,但是有所改善。可以參考這個回答: Android Studio hangs at fetching documentation
- ListView自定義顯示Item。 建立數據源 -> 自定義Adapter,處理Item顯示的內容 -> list的內容設置給Adapter -> Adapter設置給ListView;