Dart4Flutter - 拾遺01 - flutter-dart環境搭建
Flutter 例項 - 從本地到Flutter通訊 - Event Channels
自從10年前Android和iOS出現,在移動開發界,跨平臺開發一直是一個目標。一個跨平臺的應用可以幫助公司和團隊節省大量的時間和精力。
過去這些年,已經發布了很多跨平臺的開發工具,包括來自adobe基於web的PhoneGap,來自Microsoft的Xamarin,來自Facebook的React Native。
最近進入跨平臺界的工具是來自google的flutter,現在已經發布了第一個預覽版。flutter擁有開發效率高、UI渲染快,更容易的自定義元件、和在兩個平臺上和本地應用一樣的效能。
介紹
flutter應用是用dart語言開發的,Dart和其他語言,例如Kotlin Swift一樣,擁有很多共同的語言特性,而且可以轉化為JavaScript程式碼。
作為一個跨平臺框架,flutter非常像React Native,例如flutter支援響應式和宣告是程式設計。但是和React Native不一樣的是,flutter不需要JavaScript橋接,這樣可提升整個應用的效能和應用啟動的時間。Dart是通過AOT技術取得了上述的表現。
另一方面,擁有JIT的Flutter,支援在開發期間,不需要從頭構建整個應用,就可以重新整理UI,提供開發效率。
正如本教程將要展示的,flutter框架是建立在元件思想之上的。在flutter中,元件不單單指應用的檢視,而是整個介面甚至是真個應用。
除了開發跨Android和iOS應用,學習flutter將給你一個在Fuchsia平臺開發的超前開始。Fuchsia是google開發中的實驗性的作業系統。
本教程,我們構建一個app,首先從github api中獲取一個組織的成員資訊,然後將成員資訊展示在一個滾動列表中。
當開發app時,可是使用Android模擬器、iOS模擬器或者同時使用。
在構建app時,我們將學習flutter一下內容:
- 配置開發環境
- 建立工程
- Hot reload
- 引入檔案和庫
- 使用元件和建立自己的元件
- 建立網路請求
- 在列表中展示列表項
- 新增應用主題 有也將順便學習一點Dart。
開始
Flutter開發可以在macOS,Linux或Windows上完成。 雖然您可以在Flutter工具鏈中使用任何編輯器,但IntelliJ IDEA,Android Studio和Visual Studio Code的IDE外掛可以簡化開發工作。 我們將在本教程中使用Android Studio。
配置開發環境
您可以在這找到配置flutter開發環境的說明,具體步驟因平臺而異,但主要步驟是:
- 下載flutter SDK,地址
- 將flutter的bin目錄新增到path中
- 執行
flutter doctor
命令,他會安裝flutter框架,包括dart,而且提示你任何其他需要安裝的依賴。 - 安裝其他依賴
- 在IDE中安裝flutter外掛
- 測試驅動應用
Flutter網站上提供的說明做得非常好,並允許您在所選平臺上輕鬆設定開發環境。 本教程的其餘部分假設您已經為Flutter開發設定了Android Studio,並且您已經解決了
flutter doctor
發現的任何問題。
建立工程
- 如下所示選擇
File->new->new Flutter Project
- 如下所示選擇
Flutter Application
,點選next
. - 如下如所示填寫專案名稱、Flutter SDK的路徑、專案儲存目錄。
在Android Studio中,您會在左側看到一個顯示專案結構的皮膚。 有iOS和Android的資料夾,以及包含main.dart的lib資料夾。 您將本教程中的只在lib資料夾中工作。
將main.dart中的程式碼替換為以下內容:import 'package:flutter/material.dart';
void main() => runApp(new GHFlutterApp());
class GHFlutterApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'GHFlutter',
home: new Scaffold(
appBar: new AppBar(
title: new Text('GHFlutter'),
),
body: new Center(
child: new Text('GHFlutter'),
),
),
);
}
}
複製程式碼
頂部附近的main()函式使用=>運算子,為了單行函式執行應用程式。 您有一個名為GHFlutterApp的應用程式。 你在這裡看到你的應用程式本身是一個StatelessWidget(無狀態元件)。Flutter應用程式中的大多數實體都是元件,無論是無狀態的還是有狀態的。 您重寫元件的build()方法來建立您的應用的元件。 您正在使用MaterialApp 元件,該元件提供了大量遵循Material Design規範應用元件。
對於本入門教程,通過選擇它並敲擊Delete鍵,從專案中刪除test
資料夾中的測試檔案widget_test.dart
。
你可以在macOS上,啟動iOS模擬器。 您也可以在macOS,Linux或Windows上使用Android模擬器。
您看到的慢速模式banner表示應用程式正在以除錯模式執行。
您可以通過單擊Android Studio視窗頂部工具欄右側的停止按鈕來停止正在執行的應用程式:
Hot Reload
flutter支援Hot Reload 功能。flutter中的Hot reload和Android Studio的Instant Run類似。 構建並執行應用程式,使其在模擬器或模擬器上執行:
現在,不停止正在執行的應用程式,將應用程式欄字串更改為其他內容:appBar: new AppBar(
title: new Text('GHFlutter App'),
),
複製程式碼
現在單擊工具欄上的熱重新載入按鈕:
在一兩秒鐘內,您會看到正在執行的應用程式中反映了變化的內容:由於Flutter處於開發階段,熱重新載入功能可能無法正常工作,但總體而言,在構建UI時節省時間。
引入檔案
您不需要將所有的Dart程式碼儲存在一個main.dart檔案中,而是希望能夠從您建立的其他類中匯入。 現在您將看到一個用於匯入字串的示例,這將有助於您應用的本地化。 在lib資料夾中右鍵單擊並選擇New File建立一個名為strings.dart的檔案: 在新建的檔案中新增如下類:
class Strings {
static String appTitle = "GHFlutter";
}
複製程式碼
在main.dart的頂部新增如下引入:
import 'strings.dart';
複製程式碼
如下更新應用的title:
return new MaterialApp(
title: Strings.appTitle,
複製程式碼
更新其他的字串,使用新的字串檔案,所以最終GHFlutterApp類如下:
class GHFlutterApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: Strings.appTitle,
home: new Scaffold(
appBar: new AppBar(
title: new Text(Strings.appTitle),
),
body: new Center(
child: new Text(Strings.appTitle),
),
),
);
}
}
複製程式碼
構建並執行應用程式,您應該看不到任何變化,但現在您正在使用字串檔案中的字串。
元件
幾乎Flutter應用程式的每個元素都是一個元件。元件被設計成不可變的,因為使用不可變元件有助於保持應用程式UI的輕量級。
您將使用兩種基本型別的元件:
Stateless: 僅依賴於自己的配置資訊的元件,例如影像檢視中的靜態影像 Stateful: 需要維護動態資訊的元件,並通過與狀態物件互動來實現 無狀態和有狀態的元件在每個幀的Flutter應用程式中重繪,不同之處在於有狀態元件將其配置委託給狀態物件。
要開始製作自己的元件,請在main.dart的底部建立一個新類:
class GHFlutter extends StatefulWidget {
@override
createState() => new GHFlutterState();
}
複製程式碼
你已經建立了一個StatefulWidget子類,並且你重寫了createState()方法來建立它的狀態物件。 現在在GHFlutter上新增一個GHFlutterState類:
class GHFlutterState extends State<GHFlutter> {
}
複製程式碼
GHFlutterState使用GHFlutter的引數擴充套件狀態。
製作新元件時的主要任務是重寫在元件呈現在螢幕上時呼叫的build()方法。
重寫GHFlutterState 的build()方法:
@override
Widget build(BuildContext context) {
}
複製程式碼
最終如下:
@override
Widget build(BuildContext context) {
return new Scaffold (
appBar: new AppBar(
title: new Text(Strings.appTitle),
),
body: new Text(Strings.appTitle),
);
}
複製程式碼
Scaffold 是material design元件的容器。 它充當元件層次結構的根。 您已將AppBar和一個body新增到scaffold中,並且每個都包含一個Text元件
更新GHFlutterApp,以便它使用新的GHFlutter元件作為其home屬性,而不是構建預設的scaffold:
class GHFlutterApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: Strings.appTitle,
home: new GHFlutter(),
);
}
}
複製程式碼
構建並執行應用程式,您將看到新的元件正在執行:
現在還沒有多少變化,但是現在你已經開始構建新的元件。網路請求
之前,您將自己的strings.dart檔案匯入到專案中。 您可以同樣匯入Flutter框架的其他庫或者Dart的庫。
例如,您現在將使用框架中的庫來進行HTTP網路呼叫,並將生成的JSON響應解析為Dart物件。 在main.dart的頂部新增兩個新的引入:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'strings.dart';
複製程式碼
您會看到當前未使用的指標在引入上。
Dart應用程式是單執行緒的,但Dart支援在其他執行緒上執行程式碼以及執行非同步程式碼,通過使用async/await
模式,從而不會阻止UI執行緒。
您將進行非同步網路呼叫以檢索GitHub組織成員列表。 在GHFlutterState頂部新增一個空List作為屬性,並新增一個屬性來儲存文字樣式(_biggerFont
):
var _members = [];
final _biggerFont = const TextStyle(fontSize: 18.0);
複製程式碼
名字以下劃線開頭的變數表示類的私有成員變數。
要進行非同步HTTP呼叫,請在GHFlutterState中新增一個_loadData()
方法:
_loadData() async {
String dataURL = "https://api.github.com/orgs/raywenderlich/members";
http.Response response = await http.get(dataURL);
setState(() {
_members = JSON.decode(response.body);
});
}
複製程式碼
您已將async關鍵字新增到_loadData()
中,以告訴Dart它是非同步的,但是http.get()的await關鍵字表示阻塞執行。 通過dataUrl
設定API的請求地址。
當HTTP呼叫完成時,您將回撥傳遞給在UI執行緒上同步執行的setState()。 這時,您解碼JSON響應並將其賦值給_members變數。 在GHFlutterState中重寫initState方法,在initState中呼叫_loadData() 方法。當元件狀態初始化時,_loadData()會被呼叫,載入資料。
@override
void initState() {
super.initState();
_loadData();
}
複製程式碼
使用 ListView
現在您已經有了成員列表,您需要一種方法在列表UI中顯示它們。 Dart提供了一個ListView元件,可讓您在列表中顯示資料。 ListView的作用類似於Android上的RecyclerView和iOS上的UITableView,通過重複回收利用以後的view,使列表滾動更加順滑。
在GHFlutterState中新增_buildRow()
方法:
Widget _buildRow(int i) {
return new ListTile(
title: new Text("${_members[i]["login"]}", style: _biggerFont)
);
}
複製程式碼
您將返回一個ListTile元件,該元件顯示第iJSON成員的"login"值,還使用您之前建立的文字樣式(_biggerFont
)。
更新build方法,使body屬性值為ListView.builder:
body: new ListView.builder(
padding: const EdgeInsets.all(16.0),
itemCount: _members.length,
itemBuilder: (BuildContext context, int position) {
return _buildRow(position);
}),
複製程式碼
您已新增padding,將itemCount設定為成員數量,設定itemBuilder呼叫_buildRow(),傳遞postion變數。
您可以嘗試熱重新載入,但可能會收到"Full restart may be required"的訊息。 如果是這樣,重啟:
發起網路請求,解析資料,並在列表中顯示結果是很容易的!
新增分割線
為了在列表中增加分割線,需要翻倍list的itemCount屬性,然後list中的position為偶數時返回Divider元件。
body: new ListView.builder(
itemCount: _members.length * 2,
itemBuilder: (BuildContext context, int position) {
if (position.isOdd) return new Divider();
final index = position ~/ 2;
return _buildRow(index);
}),
複製程式碼
一定要itemCount翻倍,因為增加了分割線,所以刪除padding。在itemBuilder中,返回Divider元件,或者通過整數除法計算一個新的index,利用_buildRow(index)返回一個元件。
嘗試重新載入,你應該在列表中看到分割線:
要在每行中新增padding,可以在_buildRow()中使用Padding元件:Widget _buildRow(int i) {
return new Padding(
padding: const EdgeInsets.all(16.0),
child: new ListTile(
title: new Text("${_members[i]["login"]}", style: _biggerFont)
)
);
}
複製程式碼
ListTile現在是Padding的子部件。 熱載入以檢視行上的padding而不是分隔符上的Padding。
解析為自定義型別
在上一節中,JSON解析器將JSON響應中的每個成員都作為Dart Map型別例項新增到_members列表中,這相當於Kotlin中的Map或Swift中的Dictionary。 但是,如果你想使用自定義類。 在main.dart檔案中新增Member類:
class Member {
final String login;
Member(this.login) {
if (login == null) {
throw new ArgumentError("login of Member cannot be null. "
"Received: '$login'");
}
}
}
複製程式碼
member有一個login屬性,和一個建構函式,在建構函式中當login值為null時,會丟擲一個error。 更新GHFlutterState類中的_members變數宣告,所以現在他就是一個Member物件的list
var _members = <Member>[];
複製程式碼
更新_buildRow()方法使用Member物件的login屬性而不是map的"login"key的值。
title: new Text("${_members[i].login}", style: _biggerFont)
複製程式碼
現在更新在_loadData()中的傳遞給setState()的回撥,將map解析為Member物件,新增到member物件list中。
setState(() {
final membersJSON = JSON.decode(response.body);
for (var memberJSON in membersJSON) {
final member = new Member(memberJSON["login"]);
_members.add(member);
}
});
複製程式碼
如果你熱載入,有可能看到一個錯誤,這時你重新啟動。
下載圖片
每個member都有一個頭像url.你將新增頭像到Member類中,然後在app中顯示頭像。
在Member類中新增avatarUrl屬性,這個屬性不能為null。
class Member {
final String login;
final String avatarUrl;
Member(this.login, this.avatarUrl) {
if (login == null) {
throw new ArgumentError("login of Member cannot be null. "
"Received: '$login'");
}
if (avatarUrl == null) {
throw new ArgumentError("avatarUrl of Member cannot be null. "
"Received: '$avatarUrl'");
}
}
}
複製程式碼
更新_buildRow()方法,通過NetworkImage 和 the CircleAvatar 元件顯示頭像。
Widget _buildRow(int i) {
return new Padding(
padding: const EdgeInsets.all(16.0),
child: new ListTile(
title: new Text("${_members[i].login}", style: _biggerFont),
leading: new CircleAvatar(
backgroundColor: Colors.green,
backgroundImage: new NetworkImage(_members[i].avatarUrl)
),
)
);
}
複製程式碼
設定頭像為ListTile的leading屬性,頭像將顯示在title的前面。 更新_loadData()方法,使在建立一個新的Member時使用map的"avatar_url"值。
final member = new Member(memberJSON["login"], memberJSON["avatar_url"]);
複製程式碼
構建執行,你將能看到每一個行的頭像。
整理程式碼
大部分程式碼現在位於main.dart檔案中。 為了使程式碼更清晰一點,你可以重構元件和你已經新增到他們自己的檔案中的類。 在lib資料夾中建立名為member.dart和ghflutter.dart的檔案。 將Member類移入member.dart,並將GHFlutterState和GHFlutter類轉移到ghflutter.dart。 您不需要member.dart中的任何匯入語句,但flutter.dart中的匯入應如下所示:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'member.dart';
import 'strings.dart';
複製程式碼
您還需要更新main.dart中的匯入,所以整個檔案包含以下內容:
import 'package:flutter/material.dart';
import 'ghflutter.dart';
import 'strings.dart';
void main() => runApp(new GHFlutterApp());
class GHFlutterApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: Strings.appTitle,
home: new GHFlutter(),
);
}
}
複製程式碼
通過構建並執行應用程式,您應該看不到任何更改,但程式碼現在更清晰一些
新增主題
您可以通過將主題屬性新增到您在main.dart中建立的MaterialApp來輕鬆地將主題新增到應用程式:
lass GHFlutterApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: Strings.appTitle,
theme: new ThemeData(primaryColor: Colors.green.shade800),
home: new GHFlutter(),
);
}
}
複製程式碼
您正在使用綠色陰影作為Material Design主題顏色值。
構建並執行應用程式,以檢視新的主題:
大部分應用截圖都來自Android模擬器。 您也可以在iOS模擬器中執行最終的主題應用程式:
這就是我所說的跨平臺