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

閱讀原文️前端

Flutter 目前仍是 Beta 3 版本,1.0 版本還在路上。不過它在 React Native/weex等跨平臺方案以外,又爲咱們提供了一種跨平臺的方案。並且其自身的許多特性,也爲咱們擴展了新的視野。若是 Fuchsia 系統最終能和 iOS、Android 成三足鼎立之式,甚至於取代 Android,那麼 Flutter 就能爲咱們帶來更多的可能。因此如今瞭解一下仍是有必要的。 本文將經過一個簡單的實例(知識小集 Flutter 版本客戶端,咱們後期會慢慢優化),同時半翻譯半參考 Raywenderlich 上的 Getting Started with Flutter 這篇文章,來一步步瞭解如何使用 Flutter 構建 App。android

在這個 App 的開發過程當中,咱們將學習如下關於 Flutter 的內容:ios

  • 設置開發環境
  • 建立新工程
  • Hot Reload
  • 導入文件
  • 使用 Widget 及自定義 Widget
  • 網絡請求
  • 在列表中展現信息
  • 爲 App 添加主題

在這個過程當中,咱們將同時學習一些 Dart 相關的知識。項目的完整代碼在 Github 上能夠找到。git

設置開發環境

咱們能夠在 macOSLinux 或者 Windows 上開發 Flutter 應用。目前 Flutter 團隊爲一些 IDE 開發了相應的插件,這些 IDE 包括 IntelliJ IDEAAndroid StudioVisual Studio Code。個人開發環境主要爲 macOS + Visual Studio Code,因此本文主要基這二者來進行描述。github

實際的配置過程能夠參考官方文檔 Get Started: Install on macOS。具體的步驟各個平臺稍有不一樣,但主要是如下幾步:macos

  1. 拷貝 Flutter 的 git 庫;
  2. 添加 Flutter bin 目錄到咱們指定的目錄;
  3. 運行 flutter doctor 命令,這個命令將告訴咱們缺乏哪些依賴;
  4. 安裝缺失的依賴;
  5. 在 IDE 中安裝 Flutter 插件/擴展;
  6. 測試

須要注意的是,若是想在 iOS 模擬器或 iOS 設備上構建和測試應用,咱們須要使用 macOS 系統,同時須要安裝 Xcode 9.0+json

建立新工程

在安裝了 Flutter 插件的 VS Code 中,咱們能夠經過 View > Command Palette... 或者快捷鍵 cmd+shift+p 來打開 命令面板(command palette),而後輸入 Flutter:New Project 並回車:小程序

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

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

如圖是構建過程的部分信息:xcode

工程建立完成後,IDE 會默認打開 lib 目錄下的 main.dart 文件,這也是咱們 App 的入口。

注意:從 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。初始效果以下圖:

同時,咱們能夠在 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 等。每週都會有 原創 文章分享,咱們的文章都會在公衆號首發。歡迎關注查看更多內容。

相關文章
相關標籤/搜索