上一篇主要介紹了
Dart
語言的語法基礎,從這一篇開始就要真正涉及到Flutter的開發了,希望自己在寫作的過程中能溫故知新,同時給Flutter初學者帶來一些幫助。
一個最簡單的Flutter App
建立專案,新增程式碼
還記得在上一篇中,我們使用Android Studio建立了一個Flutter專案嗎?新建立的Flutter專案自動為我們生成了一些程式碼,程式碼在/lib/main.dart
檔案中,這裡我們先清空/lib/main.dart
檔案中的程式碼,用下面的程式碼代替:
// main.dart檔案內容
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new Scaffold(
appBar: new AppBar(
title: new Text('First App')
),
body: new Center(
child: new Text('Hello world'),
),
),
);
}
}
複製程式碼
啟動模擬器
為了在手機上跑起我們的App來,首先我們得執行一個模擬器(當然你也可以用真機除錯)。如果你的電腦上Flutter開發環境配置得沒有問題,該裝的都裝了(執行flutter doctor
命令檢查依賴是否安裝,AndroidStudio的dart和flutter外掛也必須安裝),那麼在Android Studio的工具欄上,應該可以看到如下圖的圖示:
這裡由於我的電腦上還沒有任何執行的Android / iOS模擬器,所以這裡顯示的是<no devices>
,點選該按鈕,選擇Open iOS Simulator
即可啟動一個iOS模擬器(確保你的電腦上安裝了Xcode)如下圖所示:
如果你想建立Android模擬器,必須先確保你有可用的Android模擬器,在AndroidStudio的工具欄上找到AVD Manager
圖示,如下圖:
點選開啟Android模擬器管理對話方塊,如下圖:
這裡我已經建立了一個API 27的Android模擬器,如果你的列表為空,點選圖中Create Virtual Devices...
建立模擬器即可。
執行Dart程式碼
第一步中我們已經寫好了程式碼,第二步中我們的模擬器也啟動了,點選AndroidStudio工具欄中的Run
按鈕即可執行Flutter專案到我們的模擬器中了,Run
按鈕在下圖所示位置:
可以看到,我們僅僅用了20多行程式碼,就完成了一個精美的Demo App(雖然沒有實現任何功能,但是對比下如果要用Android或iOS原生開發方式,可以做到這麼簡單實現嗎),這一切都歸功於Flutter為我們提供的Widgets,下面的篇幅裡會針對常用的Widgets做一些講解。
Flutter專案結構
新建立的Flutter專案的結構如下圖所示:
各個目錄/檔案說明如下:
.
├── README.md ---markdown專案描述檔案
├── android ---Android原始碼目錄
├── build ---專案構建後輸出的相關檔案目錄
├── flutter_app.iml ---專案相關的配置檔案
├── flutter_app_android.iml ---Android相關的配置檔案
├── ios ---iOS原始碼目錄
├── lib ---Dart原始碼目錄
├── pubspec.lock ---安裝鎖定檔案
├── pubspec.yaml ---flutter依賴配置檔案,類似Android中的build.grale
└── test ---測試程式碼目錄
複製程式碼
我們開發的程式碼主要存放在lib/
目錄下,專案的入口檔案main.dart
也在lib/
目錄下。
Flutter App是怎樣的App
關於一個Flutter App,你需要了解如下幾個點:
- Flutter App的佈局檔案都是使用Dart程式碼來寫的(業務邏輯程式碼和UI程式碼都用dart來寫),沒有像Android中的xml佈局檔案或者iOS中的xib, storyboard檔案等。
- Flutter App中的介面都是由Widget組成的,Widget分為兩種:StatefulWidget和StatelessWidget。StatefulWidget表示一個有狀態的元件,這個元件的狀態發生改變時,元件UI會同步發生改變;StatelessWidget表示一個無狀態的元件,它沒有狀態的改變,UI也不會發生改變。如果你熟悉Reactjs,對Flutter中的這兩種元件就很容易理解了。
- Dart是一門單執行緒語言,這意味著在Flutter開發過程中你不用去考慮執行緒的同步非同步、鎖、執行緒切換等問題,網路請求也好,UI更新也好,都在一個執行緒中執行,只不過那些比較耗時的操作(網路IO,檔案IO等等)會被放入延遲運算佇列中以免阻塞了其他的操作而造成卡頓。
- Flutter跟ReactNative或者WEEX這類移動端跨平臺框架最大的區別在於:Flutter通過AOT(Ahead of Time)或者JIT(Just In Time)的方式(Debug模式下采用JIT編譯,Release模式下使用AOT編譯)將Dart程式碼直接編譯成對應平臺的程式碼用於在移動裝置上執行,而ReactNative、WEEX則是有一套自己的jsRender,將js程式碼通過渲染引擎渲染成原生的UI,這個過程有js和native的互操作,也就是一個jsBridge,所以ReactNative或者WEEX雖然寫出來的也是原生應用,但是由於有了jsBridge的存在,導致程式碼執行的效率沒有直接編譯成原生程式碼的Flutter App的執行效率高。
Flutter常用Widgets
在移動開發中,我們經常會跟按鈕、文字輸入框、圖片等打交道,Flutter中也不例外,使用Flutter開發的App,介面上的每一個UI元素都是一個Widget,通過不同的Widget組合形成一整個頁面。除了按鈕、輸入框、圖片等Widget外,Flutter還給我們提供了很多功能強大,介面美觀的Widget,比如在本文最開始的一段程式碼:
// main.dart檔案內容
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new Scaffold(
appBar: new AppBar(
title: new Text('First App')
),
body: new Center(
child: new Text('Hello world'),
),
),
);
}
}
複製程式碼
在上面的程式碼中,MyApp
是我們自定義的一個類,它繼承自StatelessWidget,代表它是一個無狀態的元件,UI不會發生改變。build
方法是父類的一個方法,被MyApp
類重寫了,繼承自StatelessWidget的類必須實現build
方法並返回一個Widget物件。所以在上面的程式碼中,MaterialApp
也是一個Widget,如果你用AndroidStudio檢視原始碼,會發現MaterialApp的引數home
也是一個Widget物件,所以上面的Scaffold也是一個Widget。
StatefulWidget和StatelessWidget
StatefulWidget和StatelessWidget是Flutter中所有Widget的兩個分類,StatefulWidget的內部儲存有狀態,當狀態發生改變時,Widget的介面也會隨之改變(這點跟React類似);StatelessWidget的內部沒有儲存狀態,它的介面也不會發生改變。上面的程式碼中已經展示了定義一個無狀態Widget的步驟:繼承StatelessWidget並實現build方法即可。如果是定義一個有狀態的Widget,程式碼會稍微多一點,如下程式碼所示:
import 'package:flutter/material.dart';
void main() => runApp(new MyStatefulWidget());
// 定義一個有狀態的元件
class MyStatefulWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return new MyStatefulWidgetState();
}
}
// 定義一個有狀態的元件時,必須為該元件建立一個狀態類,這個類繼承自State類
class MyStatefulWidgetState extends State<MyStatefulWidget> {
String text = "Click Me!";
changeText() {
if (text == "Click Me!") {
setState(() {
text = "Hello World!";
});
} else {
setState(() {
text = "Click Me!";
});
}
}
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: "Test",
home: new Scaffold(
appBar: new AppBar(
title: new Text("Test"),
),
body: new Center(
// InkWell是Flutter內建的一個Widget,用於給其他Widget新增點選事件,並且在點選時會有水波紋擴散效果
child: new InkWell(
child: new Text(text),
onTap: () {
this.changeText();
},
),
),
),
);
}
}
複製程式碼
上面的程式碼執行後,會在頁面中央顯示文字,點選該文字時,文字內容會在"Click Me!"和"Hello World!"間切換,如下圖所示:
- 建立類繼承自StatefulWidget並實現
createState
方法,注意,這裡跟StatelessWidget不同了,不是實現build
方法。createState
方法返回的是一個狀態State。 - 為了讓第一步中的
createState
方法有返回值,還需要建立一個狀態類繼承自State類,State類是個泛型類,你需要將第一步中建立的類傳給State。 - 建立完自定義的State類後,實現
build
方法,並返回你所需要的Widget。 - 在自定義的State類中,用變數儲存元件的狀態,並在合適的時候改變這個狀態值。比如在上面的程式碼中,我們需要在點選文字時切換文字,所以用一個
text
變數儲存元件的文字值,當點選按鈕時,通過呼叫State元件的setState()
方法,重新為text
變數賦值,從而達到改變文字的目的。
如果你瞭解Reactjs,那麼對於Flutter的這種狀態機制肯定也不陌生。React中也是通過一個state物件儲存Component的狀態,當狀態需要改變時,呼叫setState()方法修改狀態,元件就會自動重新整理。
MaterialApp和Scaffold
MaterialApp和Scaffold是Flutter提供的兩個Widget,其中:
- MaterialApp是一個方便的Widget,它封裝了應用程式實現Material Design所需要的一些Widget。(參考)
- Scaffold元件是Material Design佈局結構的基本實現。此類提供了用於顯示drawer、snackbar和底部sheet的API。(參考)
在基於Flutter的開源中國客戶端App中,我也使用到了MaterialApp和Scaffold兩個元件,下面是部分程式碼:
@override
Widget build(BuildContext context) {
initData();
return new MaterialApp(
theme: new ThemeData(
// 設定主題顏色
primaryColor: const Color(0xFF63CA6C)
),
home: new Scaffold(
// 設定App頂部的AppBar
appBar: new AppBar(
// AppBar的標題
title: new Text(appBarTitles[_tabIndex],
// 標題文字的顏色
style: new TextStyle(color: Colors.white)),
// AppBar上的圖示的顏色
iconTheme: new IconThemeData(color: Colors.white)
),
body: _body,
// 頁面底部的導航欄
bottomNavigationBar: new CupertinoTabBar(
items: <BottomNavigationBarItem>[
new BottomNavigationBarItem(
icon: getTabIcon(0),
title: getTabTitle(0)),
new BottomNavigationBarItem(
icon: getTabIcon(1),
title: getTabTitle(1)),
new BottomNavigationBarItem(
icon: getTabIcon(2),
title: getTabTitle(2)),
new BottomNavigationBarItem(
icon: getTabIcon(3),
title: getTabTitle(3)),
],
currentIndex: _tabIndex,
// 底部Tab的點選事件處理
onTap: (index) {
setState((){
_tabIndex = index;
});
},
),
// 側滑選單,這裡的MyDrawer是自定義的Widget
drawer: new MyDrawer(),
),
);
}
複製程式碼
Text元件
Text元件是非常常用的元件,任何需要顯示文字的地方基本都會用到。通過檢視Text類的原始碼,可以發現Text是一個無狀態的元件,下面的程式碼演示瞭如何修改Text元件的字號、顏色,給字型加粗、設定下劃線、設定斜體等:
import 'package:flutter/material.dart';
void main() => runApp(new MaterialApp(
title: "Text Demo",
home: new Scaffold(
appBar: new AppBar(
title: new Text("Text Demo"),
),
body: new Center(
child: new Text(
"Hello Flutter",
style: new TextStyle(
color: Colors.red, // 或者用這種寫法:const Color(0xFF6699FF) 必須使用AARRGGBB
fontSize: 20.0, // 字號
fontWeight: FontWeight.bold, // 字型加粗
fontStyle: FontStyle.italic, // 斜體
decoration: new TextDecoration.combine([TextDecoration.underline]) // 文字加下劃線
),
),
),
),
));
複製程式碼
注意:
- MaterialApp的title引數是字串型別,而AppBar的title引數是一個Text元件型別。
- 開發基於Flutter的開源中國客戶端時,Flutter還是beta版本,導致在設定中文文字的某些樣式時不起作用,比如字型加粗,斜體等。目前的Flutter Preview版本,該問題好像已經修復了。
TextField元件
TextFiled元件用於文字的輸入,示例程式碼如下:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: "Test",
home: new Scaffold(
appBar: new AppBar(
title: new Text("Test")
),
body: new Padding(
padding: const EdgeInsets.all(8.0),
child: new TextField(
maxLines: 8, // 設定輸入框顯示的最大行數(不是可輸入的最大行數)
maxLength: 30, // 設定輸入框中最多可輸入的字元數
decoration: new InputDecoration( // 給輸入框新增樣式
hintText: "Input something...", // 輸入框中placeholder文字
border: new OutlineInputBorder( // 輸入框的邊框
borderRadius: const BorderRadius.all(Radius.circular(1.0))
)
),
)
)
),
);
}
}
複製程式碼
在模擬器中執行介面如下圖:
InkWell和GestureDetector
這兩個元件放到一起說,是因為在處理元件的點選事件時,會經常用到它們。 比如某個列表的item的點選事件,某個圖示的點選事件等等。Flutter有專門設計MaterialDesign風格的按鈕,但是更多時候我們希望自定義按鈕樣式或者為某個元件新增點選事件,所以在處理點選事件時,最常見的做法是,用InkWell或者GestureDetector將某個元件包起來。
InkWell的使用方法如下:
new InkWell(
child: new Text("Click me!"),
onTap: () {
// 單擊
},
onDoubleTap: () {
// 雙擊
},
onLongPress: () {
// 長按
}
);
複製程式碼
GestureDetector用法與InkWell類似,不過GestureDetector有更多處理手勢的方法,這裡暫時不做介紹(其實我也用得不多)。
按鈕
Flutter提供了幾種型別的按鈕元件:RaisedButton
FloatingActionButton
FlatButton
IconButton
PopupMenuButton
,下面用一段程式碼說明這幾種按鈕的用法:
import 'package:flutter/material.dart';
main() {
runApp(new MyApp());
}
enum WhyFarther { harder, smarter, selfStarter, tradingCharter }
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Test',
home: new Scaffold(
appBar: new AppBar(
title: new Text('Test')
),
body: new Column(
children: <Widget>[
new RaisedButton(
child: new Text("Raised Button"),
onPressed: (){},
),
new FloatingActionButton(
child: new Icon(Icons.add),
onPressed: (){},
),
new FlatButton(
onPressed: (){},
child: new Text("Flat Button")
),
new IconButton(
icon: new Icon(Icons.list),
onPressed: (){}
),
new PopupMenuButton<WhyFarther>(
onSelected: (WhyFarther result) {},
itemBuilder: (BuildContext context) => <PopupMenuEntry<WhyFarther>>[
const PopupMenuItem<WhyFarther>(
value: WhyFarther.harder,
child: const Text('Working a lot harder'),
),
const PopupMenuItem<WhyFarther>(
value: WhyFarther.smarter,
child: const Text('Being a lot smarter'),
),
const PopupMenuItem<WhyFarther>(
value: WhyFarther.selfStarter,
child: const Text('Being a self-starter'),
),
const PopupMenuItem<WhyFarther>(
value: WhyFarther.tradingCharter,
child: const Text('Placed in charge of trading charter'),
),
],
)
],
)
)
);
}
}
複製程式碼
在模擬器中上面的程式碼執行效果如下圖所示:
Dialog元件
Flutter提供了兩種型別的對話方塊:SimpleDialog和AlertDialog。SimpleDialog是一個可以顯示附加的提示或操作的簡單對話方塊,AlertDialog則是一個會中斷使用者操作的對話方塊,需要使用者確認的對話方塊,下面用程式碼來說明其用法:
import 'package:flutter/material.dart';
main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Test',
home: new Scaffold(
appBar: new AppBar(
title: new Text('Test')
),
// body: new MyAlertDialogView()
body: new MySimpleDialogView(),
),
);
}
}
class MyAlertDialogView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new RaisedButton(
child: new Text('顯示AlertDialog'),
onPressed: () {
showDialog<Null>(
context: context,
barrierDismissible: false, // 不能點選對話方塊外關閉對話方塊,必須點選按鈕關閉
builder: (BuildContext context) {
return new AlertDialog(
title: new Text('提示'),
content: new Text('微軟重申Windows 7將在2020年1月到達支援終點,公司希望利用這個機會說服使用者在最新更新發布之前升級到Windows 10。'),
actions: <Widget>[
new FlatButton(
child: new Text('明白了'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
},
);
}
}
class MySimpleDialogView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new RaisedButton(
child: new Text('顯示SimpleDialog'),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext ctx) {
return new SimpleDialog(
title: new Text('這是SimpleDialog'),
children: <Widget>[
new SimpleDialogOption(
onPressed: () { Navigator.pop(context); },
child: const Text('確定'),
),
new SimpleDialogOption(
onPressed: () { Navigator.pop(context); },
child: const Text('取消'),
),
],
);
}
);
},
);
}
}
複製程式碼
上面的程式碼分別展示了SimpleDialog和AlertDialog的基本用法。需要注意的是,這裡並沒有直接將按鈕和顯示對話方塊的邏輯寫到MyApp類中,而是分兩個StatelessWidget來寫的,如果你直接將按鈕及顯示對話方塊的邏輯寫到MyApp的build
方法裡,是會報錯的,具體報錯資訊為:
Navigator operation requested with a context that does not include a Navigator.
複製程式碼
意思是導航操作需要一個不包含Navigator的上下文物件,而如果我們將showDialog的邏輯寫到MyApp的build
方法中時,使用的是MaterialApp的上下文物件,這個上下文物件是包含Navigator的,所以就會報錯。上面的程式碼在模擬器中執行效果如下圖:
Image元件
Image元件用於顯示一張圖片,可以載入本地(專案中或手機儲存中)或網路圖片。
載入本地圖片
使用下面的方法載入一張專案中的圖片:
new Image.asset(path, width: 20.0, height: 20.0, fit: BoxFit.cover)
複製程式碼
其中path是專案中的圖片目錄。
載入專案中的圖片一定要注意編輯pubspec.yaml檔案:
假設當前我們在跟
lib/
同級的目錄下建立了images/
目錄,在images/
目錄下存放了若干圖片供專案使用,那麼一定要記得在專案根目錄下(也是跟images/
同級的目錄)編輯pubspec.yaml檔案,開啟pubspec.yaml檔案,預設情況下assets是被註釋了的,這裡我們要取消註釋assets並新增images/目錄下的每個圖片的路徑,如下圖所示:
在上圖中我們配置了檔案路徑images/ic_nav_news_normal.png
,所以可以用下面的程式碼來載入圖片了:
new Image.asset('images/ic_nav_news_normal.png', width: 20.0, height: 20.0, fit: BoxFit.cover)
複製程式碼
width
和height
是圖片長寬,為double型別,如果你傳整型20
則會報錯。
如果要載入手機儲存中的圖片,使用下面的方法:
new Image.file(path, width: 20.0, height: 20.0, fit: BoxFit.cover)
複製程式碼
fit屬性指定了圖片顯示的不同方式,有如下幾個值:
- contain:儘可能大,同時仍然包含圖片完全在目標容器內。
- cover:儘可能小,同時仍然覆蓋整個目標容器。
- fill:通過拉伸圖片的長寬比填充目標容器。
- fitHeight:確保是否顯示了圖片的完整高度,而不管是否意圖片高度溢位了目標容器。
- fitWidth:確保是否顯示了圖片的完整寬度,而不管是否圖片高度溢位目標容器。
- none:對齊目標容器內的圖片(預設情況下居中)並丟棄位於容器外的圖片的任何部分。圖片原始大小不會被調整。
- scaleDown:對齊目標容器內的圖片(預設情況下居中),如果必要的話,對圖片進行縮放,以確保圖片適合容器。這與
contain
的情況相同,否則它與沒有一樣。
載入網路圖片
載入網路圖片使用下面的方法:
new Image.network(imgUrl, width: 20.0, height: 20.0, fit: BoxFit.cover)
複製程式碼
ListView元件
ListView元件用於顯示一個列表,在基於Flutter的開源中國客戶端App中,新聞列表、動彈列表等都需要用到ListView,一個最簡單的ListView可以用如下程式碼實現:
import 'package:flutter/material.dart';
void main() {
List<Widget> items = new List();
for (var i = 0; i < 20; i++) {
items.add(new Text("List Item $i"));
}
runApp(new MaterialApp(
title: "Text Demo",
home: new Scaffold(
appBar: new AppBar(
title: new Text("Text Demo"),
),
body: new Center(
child: new ListView(children: items)
),
),
));
}
複製程式碼
執行上面的程式碼,結果如下圖所示:
這樣的ListView顯示不是我們需要的,太難看,每個item沒有邊距而且沒有分割線,所以我們用下面的程式碼改造一下:
import 'package:flutter/material.dart';
void main() {
// 裝有ListView中所有item的集合
List<Widget> items = new List();
for (var i = 0; i < 20; i++) {
var text = new Text("List Item $i");
// Padding也是一個Widget,是一個有內邊距的容器,可以裝其他Widget
items.add(new Padding(
// 內邊距設定為15.0,上下左右四邊都是15.0
padding: const EdgeInsets.all(15.0),
// Padding容器中裝的是Text元件
child: text
));
}
runApp(new MaterialApp(
title: "Text Demo",
home: new Scaffold(
appBar: new AppBar(
title: new Text("Text Demo"),
),
body: new Center(
// build是ListView提供的靜態方法,用於建立ListView
child: new ListView.builder(
// itemCount是ListView的item個數,這裡之所以是items.length * 2是因為將分割線也算進去了
itemCount: items.length * 2,
itemBuilder: (context, index) {
// 如果index為奇數,則返回分割線
if (index.isOdd) {
return new Divider(height: 1.0);
}
// 這裡index為偶數,為了根據下標取items中的元素,需要對index做取整
index = index ~/ 2;
return items[index];
},
)
)
),
));
}
複製程式碼
此時再次執行上面的程式碼,UI就好看多了:
關於ListView的用法,上面的程式碼中已有相關注釋,更詳細的用法會在後面的篇幅中介紹,比如ListView中的item實現不同的佈局,下拉重新整理,載入更多等等。
小結
關於Flutter常用的部分Widget,在上面已有相關示例程式碼和說明,你還可以在Flutter中文網上檢視更多元件及其用法。下一篇中我將記錄Flutter中的佈局,任何移動開發,甚至Web開發和桌面端應用開發中都不可避免的需要了解佈局的知識。
我的開源專案
- 基於Google Flutter的開源中國客戶端,希望大家給個Star支援一下,原始碼:
- 基於Flutter的俄羅斯方塊小遊戲,希望大家給個Star支援一下,原始碼:
上一篇 | 下一篇 |
---|---|
從0開始寫一個基於Flutter的開源中國客戶端(2) ——Dart語法基礎 |
從0開始寫一個基於Flutter的開源中國客戶端(4) ——Flutter佈局基礎 |