使用 Flutter 開發知識小集 iOS/Android 客戶端

知識小集發表於2018-05-16

閱讀原文️

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 上可以找到。

設定開發環境

我們可以在 macOSLinux 或者 Windows 上開發 Flutter 應用。目前 Flutter 團隊為一些 IDE 開發了相應的外掛,這些 IDE 包括 IntelliJ IDEAAndroid StudioVisual Studio Code。我的開發環境主要為 macOS + Visual Studio Code,所以本文主要基這兩者來進行描述。

實際的配置過程可以參考官方文件 Get Started: Install on macOS。具體的步驟各個平臺稍有不同,但主要是以下幾步:

  1. 拷貝 Flutter 的 git 庫;
  2. 新增 Flutter bin 目錄到我們指定的目錄;
  3. 執行 flutter doctor 命令,這個命令將告訴我們缺少哪些依賴;
  4. 安裝缺失的依賴;
  5. 在 IDE 中安裝 Flutter 外掛/擴充套件;
  6. 測試

需要注意的是,如果想在 iOS 模擬器或 iOS 裝置上構建和測試應用,我們需要使用 macOS 系統,同時需要安裝 Xcode 9.0+

建立新工程

在安裝了 Flutter 外掛的 VS Code 中,我們可以通過 View > Command Palette... 或者快捷鍵 cmd+shift+p 來開啟 命令皮膚(command palette),然後輸入 Flutter:New Project 並回車:

使用 Flutter 開發知識小集 iOS/Android 客戶端

為工程取名為 awesome_tips_flutter,並回車。選擇一個目錄來儲存工程,然後等待 Flutter 配置好工程。配置的過程主要有幾個步驟:

  1. 建立工程所需要的模板檔案,包括對應的 iOS 和 Android 工程;
  2. 執行 flutter packages get 命令來獲取依賴包;
  3. 執行 flutter doctor 命令來檢測依賴包;

如圖是構建過程的部分資訊:

使用 Flutter 開發知識小集 iOS/Android 客戶端

工程建立完成後,IDE 會預設開啟 lib 目錄下的 main.dart 檔案,這也是我們 App 的入口。

使用 Flutter 開發知識小集 iOS/Android 客戶端

注意:從 Flutter Beta 3 開始,建立 Widget 時,new 關鍵字是可選的。目前我這生成的模板程式碼部分還是帶 new 關鍵字的。

在左側的工程目錄中,我們可以看到 iosandroidlib 這些目錄,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。初始效果如下圖:

使用 Flutter 開發知識小集 iOS/Android 客戶端

同時,我們可以在 VS Code 頂部看到一個除錯工具欄,我們可以通過這個工具欄來停止或者重新載入 App。

使用 Flutter 開發知識小集 iOS/Android 客戶端

Hot Reload

Flutter 開發最吸引人的一個方面就是當程式程式碼更改時,可以自動執行 Hot Reload 操作,來重新載入 App。我們來試試這個特性,對我們的程式做個小小的修改:

appBar: AppBar(title: Text('Awesome Tips for Test')),
複製程式碼

在我們儲存檔案時,VS Code 會自動啟動 Hot Reload 功能,載入完成後,模擬器會顯示新的內容。當然我們也可以手動點選除錯工具欄上的 Hot Reload 按鈕來啟動熱載入。來看看效果。

使用 Flutter 開發知識小集 iOS/Android 客戶端

注:由於 Flutter 還是 Beta 版,所以 Hot Reload 並不總是能正常工具。我就遇到了類似 Request to Dart VM Service timed out: _flutter.listViews({}) 這樣的問題,解決方法是重啟 Debug。

匯入檔案

通常我們都不希望在一個檔案中放入大量的程式碼,而是將程式碼分散在不同的檔案中,並通過一定的方式將這些檔案組織起來。然後如果一個檔案需要用到其它檔案的類或方法,只需要匯入相關檔案即可。在一個 Dart 檔案中,我們可以通過 import 關鍵字來實現這一目標。

比如上面程式碼中,我們希望將字串統一放在一個檔案中來管理,那麼可以建立一個 strings.dart 檔案。在 lib 目錄處點選右鍵,會彈出選單,選擇 New File,並輸入檔名。

使用 Flutter 開發知識小集 iOS/Android 客戶端

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(),				// 替換此處內容
    );
  }
}
複製程式碼

編譯執行程式,得到的結果和上面差不多。

使用 Flutter 開發知識小集 iOS/Android 客戶端

網路請求及資料轉換

我們最終要展示的是知識小集的內容清單,所以需要從伺服器上獲取到清單內容,並轉換成我們需要的 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 後的效果如下:

使用 Flutter 開發知識小集 iOS/Android 客戶端

很簡單吧?這樣,我們的任務基本完成。

這裡我們只是獲取了第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 開發知識小集 iOS/Android 客戶端

總結

在本文中,我們通過一個簡單的例子來了解了一下如果使用 Flutter 來構建 App,可以在 awesome-tips-flutter-app 下載完整的示例程式碼。當然,構建一個完整的 App 還需要做很多事情,還有許多技術學習。後期我們會逐步來完善這個 App,並讓其達到上線的標準,最終釋出到應用市場上。

為了更方便大家獲取 Flutter 相關的開發資源,我們在 Github 上開了一個 repo flutter-resources,歡迎大家一起來維護這個 repo。

參考

知識小集是一個團隊公眾號,主要定位在移動開發領域,分享移動開發技術,包括 iOS、Android、小程式、移動前端、React Native、weex 等。每週都會有 原創 文章分享,我們的文章都會在公眾號首發。歡迎關注檢視更多內容。

使用 Flutter 開發知識小集 iOS/Android 客戶端

相關文章