- 原文地址:Develop your first Application with Flutter
- 原文作者:Gahfy
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:mysterytony
- 校對者:rockzhai, zhaochuanxing
用 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 熱過載
你可以看到,這個應用的主要顏色是藍色。我們可以改成紅色。在 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
的文件告訴我們至少要初始化 home
,routes
,onGenerateRoute
或者 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 獲得。
如果你喜歡這篇文章,你可以關注 我的推特 來獲得下一篇的推送。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。