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

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

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

這篇文章會介紹 Flutter 的安裝過程和工做原理,因此會比平時長一點。android

咱們將開發一個向用戶顯示從 JSONPlaceholder API 中檢索的帖子列表的應用程序。ios

什麼是 Flutter ?

Flutter 是一款 SDK,它可讓你開發基於 Android,iOS 或者 Google 的下一個操做系統 Fuschia 的原生應用。它使用 Dart 做爲主要編程語言。git

安裝所需的工具

Git,Android Studio 和 XCode

爲了獲取 Flutter,你須要克隆其官方倉庫。若是你想開發 Android 應用,則還須要 Android Studio 。若是要開發 iOS 應用,則還須要 XCode 。github

IntelliJ IDEA

你還須要 IntelliJ IDEA(這不是必須的,可是會頗有用)。安裝完 IntelliJ IDEA 以後,把 Dart 和 Flutter 插件添加到 IntelliJ IDEA。編程

獲取 Flutter

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

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

而後,你須要將把 bin 文件夾的路徑添加到 PATH 環境變量中。就這樣,你如今能夠開始用 Flutter 開發應用程序了。後端

雖然這已經足夠了,爲了避免讓這篇文章顯得冗長,我縮短了安裝過程的講解。若是你須要更完整的指南,請轉至 官方文檔數組

開發第一個項目

讓咱們如今打開 IntelliJ IDEA 並建立第一個項目。在左側面板中,選擇 Flutter (若是沒有,就請將 Flutter 和 Dart 插件安裝到你的 IDE 中)。bash

咱們以如下方式命名:

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

運行第一個項目並探索 Flutter

IntelliJ 的編輯器打開了一個名爲 main.dart 的文件,它是應用程序的主文件。若是你還不瞭解 Dart,別慌,這個教程的剩下部分不時必須的。

如今,將 Android 或 iOS 手機插入你的計算機,或運行一個模擬器。

你如今能夠經過點擊右上角的運行按鈕(帶有綠色三角形)來運行該應用程序:

點擊底部浮動動做按鈕來增長顯示的數字。咱們如今不會深刻研究其代碼,但咱們會用 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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索