Flutter 目前還是 Beta 3 版本,1.0 版本還在路上。不過它在 React Native/weex等跨平臺方案之外,又為我們提供了一種跨平臺的方案。而且其自身的許多特性,也為我們擴充套件了新的視野。如果
Fuchsia
系統最終能和 iOS、Android 成三足鼎立之式,甚至於取代 Android,那麼 Flutter 就能為我們帶來更多的可能。所以現在瞭解一下還是有必要的。 本文將通過一個簡單的例項(知識小集 Flutter 版本客戶端,我們後期會慢慢優化),同時半翻譯半參考Raywenderlich
上的 Getting Started with Flutter 這篇文章,來一步步瞭解如何使用 Flutter 構建 App。
在這個 App 的開發過程中,我們將學習以下關於 Flutter 的內容:
- 設定開發環境
- 建立新工程
- Hot Reload
- 匯入檔案
- 使用 Widget 及自定義 Widget
- 網路請求
- 在列表中展示資訊
- 為 App 新增主題
在這個過程中,我們將同時學習一些 Dart 相關的知識。專案的完整程式碼在 Github 上可以找到。
設定開發環境
我們可以在 macOS
、Linux
或者 Windows
上開發 Flutter 應用。目前 Flutter 團隊為一些 IDE 開發了相應的外掛,這些 IDE 包括 IntelliJ IDEA
、Android Studio
和 Visual Studio Code
。我的開發環境主要為 macOS + Visual Studio Code,所以本文主要基這兩者來進行描述。
實際的配置過程可以參考官方文件 Get Started: Install on macOS。具體的步驟各個平臺稍有不同,但主要是以下幾步:
- 拷貝 Flutter 的
git
庫; - 新增 Flutter
bin
目錄到我們指定的目錄; - 執行
flutter doctor
命令,這個命令將告訴我們缺少哪些依賴; - 安裝缺失的依賴;
- 在 IDE 中安裝 Flutter 外掛/擴充套件;
- 測試
需要注意的是,如果想在 iOS 模擬器或 iOS 裝置上構建和測試應用,我們需要使用 macOS 系統,同時需要安裝
Xcode 9.0+
。
建立新工程
在安裝了 Flutter 外掛的 VS Code 中,我們可以通過 View > Command Palette...
或者快捷鍵 cmd+shift+p
來開啟 命令皮膚(command palette),然後輸入 Flutter:New Project
並回車:
為工程取名為 awesome_tips_flutter
,並回車。選擇一個目錄來儲存工程,然後等待 Flutter 配置好工程。配置的過程主要有幾個步驟:
- 建立工程所需要的模板檔案,包括對應的 iOS 和 Android 工程;
- 執行
flutter packages get
命令來獲取依賴包; - 執行
flutter doctor
命令來檢測依賴包;
如圖是構建過程的部分資訊:
工程建立完成後,IDE 會預設開啟 lib
目錄下的 main.dart
檔案,這也是我們 App 的入口。
注意:從 Flutter Beta 3 開始,建立 Widget 時,
new
關鍵字是可選的。目前我這生成的模板程式碼部分還是帶 new 關鍵字的。
在左側的工程目錄中,我們可以看到 ios
、android
、lib
這些目錄,lib 目錄下的程式碼將應用於兩個平臺,目前我們也主要是在這個目錄下工作。
為了構建我們自己的應用,先刪除 main.dart 中現有的程式碼,並用如下程式碼替代:
import 'package:flutter/material.dart';
void main() => runApp(new AwesomeTips());
class AwesomeTips extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Awesome Tips',
home: Scaffold(
appBar: AppBar(title: Text('Awesome Tips')),
body: Center(
child: Text('Awesome Tips'),
)
)
);
}
}
複製程式碼
頂部的 main() 函式使用 =>
操作符來指定單行函式的函式體(類似於 ES6 中的箭頭函式),並執行 App。runApp
的引數是我們的 AwesomeTipsApp
類(根 Widget)。
在這裡,我們的 AwesomeTipsApp 類繼承自 StatelessWidget
。Flutter 中大部分實體都是 Widget,或者是無狀態的(stateless),或者是有狀態的(stateful)。我們重寫 Widget 的 build()
方法來構建自定義的 App Widget。
我們先來執行一下這個 App。首先啟動 iOS 模擬器。選擇選單 Debug -> Start Debugging
構建並執行工程。可以看到 VS Code 開啟了 Debug Console
(除錯控制檯) 皮膚,同時 xcode-builder
開始構建並啟動 App。初始效果如下圖:
同時,我們可以在 VS Code 頂部看到一個除錯工具欄,我們可以通過這個工具欄來停止或者重新載入 App。
Hot Reload
Flutter 開發最吸引人的一個方面就是當程式程式碼更改時,可以自動執行 Hot Reload
操作,來重新載入 App。我們來試試這個特性,對我們的程式做個小小的修改:
appBar: AppBar(title: Text('Awesome Tips for Test')),
複製程式碼
在我們儲存檔案時,VS Code 會自動啟動 Hot Reload 功能,載入完成後,模擬器會顯示新的內容。當然我們也可以手動點選除錯工具欄上的 Hot Reload 按鈕來啟動熱載入。來看看效果。
注:由於 Flutter 還是 Beta 版,所以 Hot Reload 並不總是能正常工具。我就遇到了類似
Request to Dart VM Service timed out: _flutter.listViews({})
這樣的問題,解決方法是重啟 Debug。
匯入檔案
通常我們都不希望在一個檔案中放入大量的程式碼,而是將程式碼分散在不同的檔案中,並通過一定的方式將這些檔案組織起來。然後如果一個檔案需要用到其它檔案的類或方法,只需要匯入相關檔案即可。在一個 Dart 檔案中,我們可以通過 import
關鍵字來實現這一目標。
比如上面程式碼中,我們希望將字串統一放在一個檔案中來管理,那麼可以建立一個 strings.dart
檔案。在 lib 目錄處點選右鍵,會彈出選單,選擇 New File
,並輸入檔名。
在 string.dart
中新增以下程式碼:
class Strings {
static String appTitle = "Awesome Tips";
}
複製程式碼
然後在 main.dart
中通過以下方式匯入:
import 'strings.dart';
複製程式碼
現在就可以在 AwesomeTipsApp 中使用 appTitle
了:
class AwesomeTipsApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: Strings.appTitle,
home: Scaffold(
appBar: AppBar(title: Text(Strings.appTitle)),
body: Center(
child: Text(Strings.appTitle),
)
)
);
}
}
複製程式碼
Widgets
在 Flutter App 中,幾乎所有的介面元素都是 Widget。Widget 被設計成是不可變的(immutable),因為這樣可以讓 App 的 UI 輕量化。我們可以使用兩種型別的 Widget:
- Stateless:無狀態 Widget,只依賴於自身的配置資訊,例如一個 image view 的靜態圖片;
- Stateful:有狀態 Widget,需要處理動態資訊,並與
State
物件互動。
兩種型別的 Widget 都會在 Flutter App 的每一幀進行重繪,不同的是 Stateful Widgets 會將其配置交給 State 物件來管理。關於 Flutter 介面開發,可以參考阿里閒魚團隊 的**《深入瞭解Flutter介面開發》**一文。
我們現在來建立一個 Widget 展示列表。在 lib 目錄中新建檔案 content_list.dart
,在檔案中加入如下程式碼:
import 'package:flutter/material.dart';
class ContentList extends StatefulWidget {
@override
createState() => _ContentListState();
}
複製程式碼
這裡我們建立了 StatefulWidget
的一個子類 ContentList
並重寫了 createState()
方法,該方法返回 ContentList 對應的 State 物件。然後我們在同一檔案中新增以下程式碼:
class _ContentListState extends State<ContentList> {
}
複製程式碼
_ContentListState
繼承自泛型引數為 ContentList 的 State 物件。在 _ContentListState 中,我們的主要工作就是重寫 build()
方法,這個方法在 Widget 被渲染到螢幕上時會呼叫。目前我們還沒有涉及到資料的處理,所以暫時和之前一樣,在 ContentList 中顯示一個簡單的文字。在 build() 方法中新增以下程式碼:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(Strings.appTitle)),
body: Text(Strings.appTitle),
);
}
複製程式碼
Scaffold
類是Material Design Widgets
的容器。它通常作為 Widget 層級的根。
上面的程式碼我們新增了一個 AppBar 和一個 body 到 Scaffold 中。接下來我們用這個 ContentList Widget 替換 main.dart 中的 home
屬性的內容:
import 'content_list.dart';
void main() => runApp(AwesomeTipsApp());
class AwesomeTipsApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: Strings.appTitle,
home: ContentList(), // 替換此處內容
);
}
}
複製程式碼
編譯執行程式,得到的結果和上面差不多。
網路請求及資料轉換
我們最終要展示的是知識小集的內容清單,所以需要從伺服器上獲取到清單內容,並轉換成我們需要的 Dart 物件。這裡我們需要用到兩個庫:
package:http/http.dart
:負責網路請求,從服務端獲取資料;dart:convert
:將服務端返回的字串轉換成JSON
物件;
我們在 main.dart 中匯入這兩個模組:
import 'package:http/http.dart';
import 'dart:convert';
複製程式碼
需要注意的是:Dart 應用是單執行緒的,但是 Dart 支援程式碼執行在其它執行緒上,同時也支援使用
async/await
模式讓程式碼非同步執行,而不會阻塞 UI 執行緒。
接下來我們需要通過非同步網路呼叫來獲取知識小集的內容列表。首先我們在 _ContentListState 類的頂部新增一個空列表屬性,用於儲存內容清單:
var _items = [];
複製程式碼
Dart 語言中,如果屬性/方法名是以_開頭,則表示這個屬性/方法是類私有的。
然後新增一個 _loadData() 方法,我們在這做網路請求:
void _loadData() async {
String dataURL =
"https://app.kangzubin.com/iostips/api/feed/list?page=1&from=flutter-app&version=1.0";
http.Response response = await http.get(dataURL);
// ...
}
複製程式碼
這裡我們在 _loadData() 後面加上 async
關鍵字,用於告訴 Dart 這是一個非同步方法,同時在 http.get
前使用 await
關鍵字,來阻塞後面的程式碼執行。當 HTTP 呼叫完成後,服務端返回的是一個 JSON
字串,具體結構如下:
{
"code": 0,
"msg": "SUCCESS",
"data": {}
}
複製程式碼
對於 feed/list
介面,其 data
中的結構如下:
"data": {
"feeds": [{
"fid": "96",
"auther": "halohily",
"title": "如何重寫自定義物件的 hash 方法",
"url": "https://weibo.com/3656155132/GfEGebnEN",
"platform": "0",
"postdate": "2018-05-08"
}, {
"fid": "95",
"auther": "南峰子",
"title": "微博一週推送",
"url": "https://weibo.com/3321824014/GfviNzT3z",
"platform": "0",
"postdate": "2018-05-07"
}]
}
複製程式碼
在獲取到 JSON 字串後,我們首先需要將其轉換成 JSON 物件,然後根據 code 是否為 0 做處理。如果請求成功,則需要從 data
中取出 feeds
的資料。同時,我們希望將 feed 資料轉換成一個 Dart 物件,所以我們建立一個 feed.dart
檔案,並新增如下程式碼:
class Feed {
final String author;
final String title;
final String postdate;
Feed(this.author, this.title, this.postdate);
}
複製程式碼
然後我們就可以對返回的資料做處理,將每一條 feed 轉換成一個 Feed
物件,並儲存在 _items 中。完整的 _loadData() 程式碼如下所示:
void _loadData() async {
String dataURL =
"https://app.kangzubin.com/iostips/api/feed/list?page=1&from=flutter-app&version=1.0";
http.Response response = await http.get(dataURL);
final body = JSON.decode(response.body);
final int code = body["code"];
if (code == 0) {
final feeds = body["data"]["feeds"];
var items = [];
feeds.forEach((item) =>
items.add(Feed(item["author"], item["title"], item["postdate"])));
setState(() {
_items = items;
});
}
}
複製程式碼
如果我們希望在狀態改變時,觸發介面重新渲染,則需要呼叫 setState() 方法來設定我們的屬性值。
有了載入資料的方法,我們就需要在合適的位置來呼叫。我們暫且在 _ContentListState 類中重寫 State 的 initState()
方法,如下所示:
@override
void initState() {
super.initState();
_loadData();
}
複製程式碼
Widget 生命週期相關的內容,我們有機會再講。
使用 ListView
至此,我們已經有了列表資料,接下來就需要將資料顯示在介面上了。Flutter 提供了 ListView
Widget 來顯示一個列表,這個 Widget 能很流暢地展示列表內容。
我們先在 _ContentListState 類中新增一個私有方法 _buildRow()
,以建立顯示單元格的 widget:
Widget _buildRow(int i) {
Feed feed = this._items[i];
return ListTile(
title: Text(
feed.title,
overflow: TextOverflow.fade,
),
subtitle: Text(
'${feed.postdate} @${feed.author}',
));
}
複製程式碼
我們暫且返回一個 ListTile
來顯示內容的標題及釋出日期和作者。接下來我們修改 build()
方法中 Scaffold 的 body:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(Strings.appTitle)),
body: new ListView.builder(
padding: const EdgeInsets.all(13.0),
itemCount: _items.length * 2,
itemBuilder: (BuildContext context, int position) {
// 此處為新增分割線
if (position.isOdd) return Divider();
final index = position ~/ 2;
return _buildRow(index);
},
),
);
}
複製程式碼
在這段程式碼中,我們通過 ListView.builder
來建立一個 ListView,並通過引數來配置列表的顯示。這裡我們沒有處理單元格點選等事件,後續我們會做改進。
OK,儲存程式碼,Hot Reload 後的效果如下:
很簡單吧?這樣,我們的任務基本完成。
這裡我們只是獲取了第1頁的資料,分頁處理後續再完善。
新增主題(Theme)
最後我們來看看如何為 App 新增主題。可以說這很容易,只需要設定 main.dart 中 MaterialApp 的 theme
屬性,我們來試試:
class AwesomeTipsApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: Strings.appTitle,
theme: ThemeData(primaryColor: Colors.red.shade800),
home: ContentList(),
);
}
}
複製程式碼
我們使用了 Material Design
顏色值來設定主題顏色,效果如下:
總結
在本文中,我們通過一個簡單的例子來了解了一下如果使用 Flutter 來構建 App,可以在 awesome-tips-flutter-app 下載完整的示例程式碼。當然,構建一個完整的 App 還需要做很多事情,還有許多技術學習。後期我們會逐步來完善這個 App,並讓其達到上線的標準,最終釋出到應用市場上。
為了更方便大家獲取 Flutter 相關的開發資源,我們在 Github 上開了一個 repo flutter-resources,歡迎大家一起來維護這個 repo。
參考
知識小集是一個團隊公眾號,主要定位在移動開發領域,分享移動開發技術,包括 iOS、Android、小程式、移動前端、React Native、weex 等。每週都會有 原創 文章分享,我們的文章都會在公眾號首發。歡迎關注檢視更多內容。