[譯] 用 Flutter 開發你的第一個應用程式

tony li發表於2018-03-15

用 Flutter 開發你的第一個應用程式

[譯] 用 Flutter 開發你的第一個應用程式

一週前,Flutter 在巴塞羅那的 MWC 上釋出了第一版公測版本。本文的主要目的是向你展示如何用 Flutter 開發第一個功能齊全的應用程式。

這篇文章會介紹 Flutter 的安裝過程和工作原理,所以會比平時長一點。

我們將開發一個向使用者顯示從 JSONPlaceholder API 中檢索的帖子列表的應用程式。

什麼是 Flutter ?

Flutter 是一款 SDK,它可以讓你開發基於 Android,iOS 或者 Google 的下一個作業系統 Fuschia 的原生應用。它使用 Dart 作為主要程式語言。

安裝所需的工具

Git,Android Studio 和 XCode

為了獲取 Flutter,你需要克隆其官方倉庫。如果你想開發 Android 應用,則還需要 Android Studio 。如果要開發 iOS 應用,則還需要 XCode 。

IntelliJ IDEA

你還需要 IntelliJ IDEA(這不是必須的,但是會很有用)。安裝完 IntelliJ IDEA 之後,把 Dart 和 Flutter 外掛新增到 IntelliJ IDEA。

獲取 Flutter

你所要做的就是克隆 Flutter 官方倉庫:

git clone -b beta https://github.com/flutter/flutter.git
複製程式碼

然後,你需要將把 bin 資料夾的路徑新增到 PATH 環境變數中。就這樣,你現在可以開始用 Flutter 開發應用程式了。

雖然這已經足夠了,為了不讓這篇文章顯得冗長,我縮短了安裝過程的講解。如果你需要更完整的指南,請轉至 官方文件

開發第一個專案

讓我們現在開啟 IntelliJ IDEA 並建立第一個專案。在左側皮膚中,選擇 Flutter (如果沒有,就請將 Flutter 和 Dart 外掛安裝到你的 IDE 中)。

我們以以下方式命名:

  • 專案名稱: feedme
  • 描述: A sample JSON API project
  • 組織: net.gahfy
  • Android 語言: Kotlin
  • iOS 語言: Swift

執行第一個專案並探索 Flutter

IntelliJ 的編輯器開啟了一個名為 main.dart 的檔案,它是應用程式的主檔案。如果你還不瞭解 Dart,別慌,這個教程的剩下部分不時必須的。

現在,將 Android 或 iOS 手機插入你的計算機,或執行一個模擬器。

你現在可以通過點選右上角的執行按鈕(帶有綠色三角形)來執行該應用程式:

[譯] 用 Flutter 開發你的第一個應用程式

點選底部浮動動作按鈕來增加顯示的數字。我們現在不會深入研究其程式碼,但我們會用 Flutter 發現一些有趣的功能。

Flutter 熱過載

你可以看到,這個應用的主要顏色是藍色。我們可以改成紅色。在 main.dart 檔案中,找到以下程式碼:

return new MaterialApp(
  title: 'Flutter Demo',
  theme: new ThemeData(
    // This is the theme of your application.
    //
    // Try running your application with "flutter run". You'll see the
    // application has a blue toolbar. Then, without quitting the app, try
    // changing the primarySwatch below to Colors.green and then invoke
    // "hot reload" (press "r" in the console where you ran "flutter run",
    // or press Run > Flutter Hot Reload in IntelliJ). Notice that the
    // counter didn't reset back to zero; the application is not restarted.
    primarySwatch: Colors.blue,
  ),
  home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
複製程式碼

在這個部分,用 Colors.red 來代替 Colors.blue。Flutter 允許你熱載入應用程式,也就是說應用程式的當前狀態不會被修改,但是會使用新的程式碼。

在應用程式中,點選底部浮動的 + 按鈕開增加 counter 。

然後,在 IntelliJ 右上角,點選 Hot Reload 按鈕(帶有黃色閃電)。你可以開到主要的顏色變成了紅色,但是 counter 保持著一樣的數字。

開發最終的應用程式

讓我們現在刪除 main.dart 檔案裡所有內容,這豈不是一個更好的學習方式嗎。

最小的應用程式

我們要做的第一件事就是開發最小的應用程式,也就是能執行的最少程式碼。因為我們會用 Material Design 來設計我們的應用程式,所以首先要匯入包含 Material Design Widgets 的包。

import 'package:flutter/material.dart';
複製程式碼

現在我們來建立一個繼承 StatelessWidget 的類來建立我們應用程式的一個例項(之後會深入討論 StatelessWidget)。

import 'package:flutter/material.dart';
 
class MyApp extends StatelessWidget {
 
}
複製程式碼

IntelliJ IDEA 在 MyApp 下顯示紅色下劃線。實際上 StatelessWidget 是一個需要實現 build() 方法的抽象類。為此,將游標移動到 MyApp 上,然後按 Alt + Enter 。

import 'package:flutter/material.dart';
 
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
  }
}
複製程式碼

現在我們來實現 build() 方法,我們可以看到它必須返回一個 Widget 例項。我們要在這裡構建應用程式時返回一個 MaterialApp。為此,在 build() 中新增以下程式碼:

return new MaterialApp();
複製程式碼

MaterialApp 的文件告訴我們至少要初始化 homeroutesonGenerateRoute 或者 builder 。我們只會在這裡定義 home 屬性。這將是應用程式的主介面。因為我們希望我們的應用程式是基於 Material Design 的佈局,所以我們把 home 設定為一個空的 Scaffold

import 'package:flutter/material.dart';
 
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
        home: new Scaffold()
    );
  }
}
複製程式碼

最後我們需要設定當執行 main.dart 時,我們想執行 MyApp 應用程式。因此,我們需要在匯入語句後面新增以下行:

void main() => runApp(new MyApp());
複製程式碼

你現在已經可以執行你的應用程式。目前只是一個沒有任何內容的白色介面。所以我們現在要做的第一件事就是新增一些使用者介面。

開發使用者介面

幾句關於狀態的話

我們可能要開發兩種使用者介面。一種是與當前應用狀態無關的使用者介面,而另一種是與當前狀態相關的使用者介面。

當談到狀態時,我們的意思是,當事件被觸發時,使用者介面可能會改變,這正是我們要做的:

  • 應用程式啟動事件: - 顯示迴圈進度條
    • 執行檢索帖子的操作
  • API 請求結束:
    • 如果成功,顯示檢索帖子的結果
    • 如果失敗, 在空白介面上顯示帶失敗資訊的 Snackbar

目前,我們只用了 StatelessWidget,正如你所猜測的那樣,它並不涉及程式狀態。那麼讓我們先初始化一個 StatefulWidget

初始化 StatefulWidget

讓我們新增一個繼承 StatefulWidget 的類到我們的應用程式:

import 'package:flutter/material.dart';
 
void main() => runApp(new MyApp());
 
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
        home: new PostPage()
    );
  }
}
 
class PostPage extends StatefulWidget {
  PostPage({Key key}) : super(key: key);
 
  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
  }
}
複製程式碼

像我們看到的一樣,我們需要實現返回一個 State 物件的 createState() 方法。所以讓我們建立一個繼承 State 的類:

class PostPage extends StatefulWidget {
  PostPage({Key key}) : super(key: key);
 
  @override
  _PostPageState createState() => new _PostPageState();
}
 
class _PostPageState extends State<PostPage>{
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
  }
}
複製程式碼

就像看到的,我們需要實現 build() 方法,讓它返回一個 Widget 。為此,我們先建立一個空部件 (Row):

class _PostPageState extends State<PostPage>{
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        appBar: new AppBar(
          title: new Text('FeedMe'),
        ),
        body: new Row()//TODO add the widget for current state
    );
  }
}
複製程式碼

我們事實上返回了一個 Scaffold 物件,因為我們應用程式的工具欄不會改變,也不依賴於當前狀態。只是他的 body 會取決於當前狀態。

讓我們現在建立一個方法,它將返回 Widget 以顯示當前狀態,以及一種返回一個包含居中的迴圈進度條的 Widget 的方法:

class _PostPageState extends State<PostPage>{
  Widget _getLoadingStateWidget(){
    return new Center(
      child: new CircularProgressIndicator(),
    );
  }
 
  Widget getCurrentStateWidget(){
    Widget currentStateWidget;
    currentStateWidget = _getLoadingStateWidget();
    return currentStateWidget;
  }
 
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        appBar: new AppBar(
          title: new Text('FeedMe'),
        ),
        body: getCurrentStateWidget()
    );
  }
}
複製程式碼

如果你現在執行這個應用程式,你會看到一個居中的迴圈進度條。

顯示帖子列表

我們先定義 Post 物件,因為它是在 JSONPlaceholder API 中定義的。為此,建立一個包含以下內容的 Post.dart 檔案:

class Post {
  final int userId;
 
  final int id;
 
  final String title;
 
  final String body;
 
  Post({
    this.userId,
    this.id,
    this.title,
    this.body
  });
}
複製程式碼

現在我們在同一個檔案中定義一個 PostState 類來設計應用程式的當前狀態:

class PostState{
  List<Post> posts;
  bool loading;
  bool error;
 
  PostState({
    this.posts = const [],
    this.loading = true,
    this.error = false,
  });
 
  void reset(){
    this.posts = [];
    this.loading = true;
    this.error = false;
  }
}
複製程式碼

現在要做的就是在 PostState 類中定義一個方法來從 API 中獲取 Post 的列表。稍後我們將看到如何做到這一點,因為現在我們只能非同步地返回一個靜態的 Post 列表:

Future<void> getFromApi() async{
  this.posts = [
    new Post(userId: 1, id: 1, title: "Title 1", body: "Content 1"),
    new Post(userId: 1, id: 2, title: "Title 2", body: "Content 2"),
    new Post(userId: 2, id: 3, title: "Title 3", body: "Content 3"),
  ];
  this.loading = false;
  this.error = false;
}
複製程式碼

現在完成了,讓我們回到 main.dart 檔案中的 PostPageState 類來看看如何使用我們剛定義的類。我們在 PostPageState 類中初始化一個 postState 屬性:

class _PostPageState extends State<PostPage>{
  final PostState postState = new PostState();
 
  // ...
}
複製程式碼

如果 IntelliJ IDEA 在 PostState 下顯示紅色下劃線,這意味著 PostState 類沒有在當前檔案中定義。所以你需要匯入它。將游標移至紅色下劃線部分,然後按Alt + Enter,然後選擇匯入。

現在,讓我們定義一個方法,當我們成功獲取 Post 列表時就返回一個 Widget :

Widget _getSuccessStateWidget(){
  return new Center(
    child: new Text(postState.posts.length.toString() + " posts retrieved")
  );
}
複製程式碼

如果我們成功獲得 Post 的列表,現在要做的就是編輯 getCurrentStateWidget() 方法來顯示這個 Widget :

Widget getCurrentStateWidget(){
  Widget currentStateWidget;
  if(!postState.error && !postState.loading) {
    currentStateWidget = _getSuccessStateWidget();
  }
  else{
    currentStateWidget = _getLoadingStateWidget();
  }
  return currentStateWidget;
}
複製程式碼

最後要做的,也許最重要的一件事就是執行請求以檢索 Post 的列表。為此,定義一個 _getPosts() 方法並在初始化狀態時呼叫它:

@override
void initState() {
  super.initState();
  _getPosts();
}
 
_getPosts() async {
  if (!mounted) return;
 
  await postState.getFromApi();
  setState((){});
}
複製程式碼

噹噹噹,你可以執行應用程式來看結果。實際上,即使真的顯示了迴圈進度條,也幾乎沒有機會看得到。這是因為檢索 Post 的列表非常快,以致它幾乎立即消失。

從 API 中檢索帖子列表

為了確保實際顯示迴圈進度條,讓我們從 JSONPlaceholder API 中檢索該帖子。如果我們看一下 API 的 post 服務,我們可以看到它返回一個帖子的 JSON 陣列。

因此,我們必須先為 Post 類新增一個靜態方法,以便將 Post 的 JSON 陣列轉換為 Post 列表:

static List<Post> fromJsonArray(String jsonArrayString){
  List data = JSON.decode(jsonArrayString);
  List<Post> result = [];
  for(var i=0; i<data.length; i++){
    result.add(new Post(
        userId: data[i]["userId"],
        id: data[i]["id"],
        title: data[i]["title"],
        body: data[i]["body"]
    ));
  }
  return result;
}
複製程式碼

我們現在只需編輯檢索 PostState 類中的 Post 列表的方法,讓它從 API 真正地檢索帖子:

Future<void> getFromApi() async{
  try {
    var httpClient = new HttpClient();
    var request = await httpClient.getUrl(Uri.parse('https://jsonplaceholder.typicode.com/posts'));
    var response = await request.close();
    if (response.statusCode == HttpStatus.OK) {
      var json = await response.transform(UTF8.decoder).join();
      this.posts = Post.fromJsonArray(json);
      this.loading = false;
      this.error = false;
    }
    else{
      this.posts = [];
      this.loading = false;
      this.error = true;
    }
  } catch (exception) {
    this.posts = [];
    this.loading = false;
    this.error = true;
  }
}
複製程式碼

你現在可以執行該應用程式,根據網速或多或少地可以看到迴圈進度條。

顯示帖子列表

目前,我們只顯示檢索的帖子數量,但不會像我們預期的那樣顯示帖子列表。為了能夠顯示它,讓我們編輯 PostPageState 類的 _getSuccessStateWidget() 方法:

Widget _getSuccessStateWidget(){
  return new ListView.builder(
    itemCount: postState.posts.length,
    itemBuilder: (context, index) {
      return new Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          new Text(postState.posts[index].title,
            style: new TextStyle(fontWeight: FontWeight.bold)),
 
          new Text(postState.posts[index].body),
 
          new Divider()
        ]
      );
    }
  );
}
複製程式碼

如果再次執行應用程式,你就會看到帖子列表。

處理錯誤

我們還有最後一件事要做:處理錯誤。您可以嘗試在飛航模式下執行應用程式,然後就可以看到無限迴圈進度條。所以我們要返回一個空白錯誤:

Widget _getErrorState(){
  return new Center(
    child: new Row(),
  );
}
 
Widget getCurrentStateWidget(){
  Widget currentStateWidget;
  if(!postState.error && !postState.loading) {
    currentStateWidget = _getSuccessStateWidget();
  }
  else if(!postState.error){
    currentStateWidget = _getLoadingStateWidget();
  }
  else{
    currentStateWidget = _getErrorState();
  }
  return currentStateWidget;
}
複製程式碼

現在,當發生錯誤時,它會顯示一個空白的介面。你可以隨意更改內容來顯示錯誤介面。但是我們說過,我們希望顯示一個 Snackbar,以便在出現錯誤時重試。為此,讓我們在 PostPageState 類中開發 showError()retry() 方法:

class _PostPageState extends State<PostPage>{
  // ...
  BuildContext context;
 
  // ...
  _retry(){
    Scaffold.of(context).removeCurrentSnackBar();
    postState.reset()
    setState((){});
    _getPosts();
  }
 
  void _showError(){
    Scaffold.of(context).showSnackBar(new SnackBar(
      content: new Text("An unknown error occurred"),
      duration: new Duration(days: 1), // Make it permanent
      action: new SnackBarAction(
        label : "RETRY",
        onPressed : (){_retry();}
      )
    ));
  }
 
  //...
}
複製程式碼

正如我們所看到的,我們需要一個 BuildContext 來獲得 ScaffoldState,它可以讓 Snackbar 出現並消失。但是我們必須使用 Scaffold 物件的 BuildContext 來獲得 ScaffoldState 。為此,我們需要編輯 PostPageState 類的 build() 方法:

Widget currentWidget = getCurrentStateWidget();
return new Scaffold(
    appBar: new AppBar(
      title: new Text('FeedMe'),
    ),
    body: new Builder(builder: (BuildContext context) {
      this.context = context;
      return currentWidget;
    })
);
複製程式碼

現在在飛航模式下執行你的應用程式,它現在就會顯示 Snackbar 了。如果您離開飛航模式,然後點選重試,就可以看到帖子了。

總結

我們瞭解了用 Flutter 開發一個功能齊全的應用程式並不困難。所有 Material Design 的元素都是被提供的,並且就在剛剛,你用它們在 Android 和 iOS 平臺上開發了一個應用程式。

該專案的所有原始碼均可在 Feed-Me Flutter project on GitHub 獲得。


如果你喜歡這篇文章,你可以關注 我的推特 來獲得下一篇的推送。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章