什麼是flutter
- google 推出的跨平臺UI框架
環境搭建(MAC環境)
- Flutter 依賴下面這些命令行工具
bash, mkdir, rm, git, curl, unzip, which
獲取Flutter SDK
flutter官網下載其最新可用的安裝包
安裝包下載完成則可以進行解壓
unzip /指定解壓目錄/flutter_macos_v1.9.1+hotfix.4-stable.zip
- 解壓完成之後我們會在解壓好的目錄下會多出一個flutter目錄,獲取並記住該目錄路徑,下一步我們會用到
# 使用pwd 命令查看目錄路徑
/Users/XXXXX/development/flutter
設置環境變量
設置環境變量目的是以便我們可以運行flutter命令在任何終端會話中
確定Flutter SDK的目錄,上一步我們解壓獲取了flutter的路徑/Users/XXXXX/development/flutter
打開(或創建) HOME 指的是 路徑是 /Users/用戶名XX/ )
vim $HOME/.bash_profile
- 文件加入環境變量
export PUB_HOSTED_URL=https://pub.flutter-io.cn //國內用戶需要設置
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn //國內用戶需要設置
//
export PATH=/Users/XXXXX/development/flutter/bin:$PATH
- 最後我們執行創建好的.bash_profile文件
source $HOME/.bash_profile
注意: 如果你使用的是zsh,終端啓動時 ~/.bash_profile 將不會被加載,解決辦法就是修改 ~/.zshrc ,在其中添加:source ~/.bash_profile
執行 flutter docter 檢查本機軟件環境,沒安裝的插件根據提示安裝就行了
flutter doctor
手動升級 Flutter
- flutter 版本升級迭代很快,前面我們下載SDK默認是stable分支,也就是穩定版,如何手動升級呢?只需要下面一條flutter命令
flutter upgrade
Dart 語法
- 我們知道Flutter框架是使用Dart 語言來編寫的,Dart 是一個面向對象編程語言, 每個對象都是一個類的實例,所有的類都繼承於 Object.如果熟悉Java,語言是很容易上手的。首先來熟悉一下Dart語法
Dart 變量
- var 聲明變量,和 kt 、js語法很像,需要注意的是如下示例 name 只要複製字符串,則他就是String類型,number就是int 類型,不能再更改它的類型,而沒有初始化的變量自動獲取一個默認值爲 null。
var name = 'maoqitian';
var number = 1;
Dart 常量
- final 和 const聲明都是表示常量,一個 final 變量只能賦值一次,可以省略變量類型,如下聲明一個存放WordPair值的List 數組
final List _suggestions = new List<WordPair>();
final _suggestions = <WordPair>[];
- const 關鍵字不僅僅只用來定義常量, 也可以用來創建不變的值
//如下定義一個字體大小的值一直都是 18 ,不會改變
final _biggerFont = const TextStyle(fontSize: 18.0)
final 和 const區別
- const 的值在編譯期確定,final 的值要到運行時才確定
Dart 函數方法
- Dart 是一個真正的面嚮對象語言,方法也是對象他的類型是 Function。 這意味着,方法可以賦值給變量,也可以當做其他方法的參數。
//定義一個返回 bool(布爾)類型的方法
bool isNoble(int atomicNumber) {
return _nobleGases[atomicNumber] != null;
}
//轉換如下可以忽略類型定義
isNoble(atomicNumber) {
return _nobleGases[atomicNumber] != null;
}
//只有一個表達式的方法,你可以選擇 使用縮寫語法來定義
// => expr 語法是 { return expr; } 形式的縮寫
bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;
Dart 方法參數
- 方法可以定義兩種類型的參數:必需的和可選的。 必需的參數在參數列表前面, 後面是可選參數,必選參數沒啥好說的,我們來了解可選參數。可選參數可以是自己命名參數或者基於可選位置的參數,但是這兩種參數不能同時當做可選參數來一起用。
可選命名參數
- 調用可選命名參數方法的時候可以使用 paramName: value (key:value形式,只不是過key 是參數名稱)來指定參數值
//調用有可選命名參數方法 playGames
playGames(bold: true, hidden: false);
//playGames 方法
playGames({bool bold, bool hidden}) {
// ...
}
可選位置參數
- 方法參數列表中用[]修飾的參數就是可選位置參數
// 定義可選位置參數方法
String playGames (String from, String msg, [String sports]) {
var result = '$from suggest $msg';
if (sports != null) {
result = '$result playing $sports together';
}
return result;
}
// 不用可選參數
playGames('Bob', 'Howdy'); // 返回值 Bob suggest Howdy
//使用可選參數
playGames('I', 'Xiao Ming', 'basketball'); //返回值 I suggest Xiao Ming playing basketball together.
Dart 方法參數默認值
- 在定義方法的時候,可以使用 = 來定義可選參數的默認值。 默認值只能是編譯時常量。 如果沒有提供默認值,則默認值爲 null
// 定義可選位置參數方法
String playGames (String from , String msg, [String sports = 'football']) {
var result = '$from suggest $msg';
if (sports != null) {
result = '$result playing $sports together';
}
return result;
}
playGames('I', 'Xiao Ming'); //返回值 I suggest Xiao Ming playing football together.
入口函數(The main() function)
- 每個應用都需要有個頂級的 main() 入口方法才能執行
// Android studio 創建Demo 項目 main.dart 文件開頭
void main() => runApp(MyApp());
//可以轉換爲
void main(){
runApp(MyApp());
}
異步操作
- async 方法和 await 異步操作,直接看看一個網絡請求例子就能夠了解
static Future<ArticleListData> getArticleData(int pageNum) async{
String path = '/article/list/$pageNum/json';
Response response = await HttpUtils.get(Api.BASE_URL+path);
ArticleBaseData articleBaseData = ArticleBaseData.fromJson(response.data);
return articleBaseData.data;
}
先了解這麼多,更多Dart 相關內容可以查看Dart語言官網
Flutter
官方文檔
Flutter Hello world
在開始Flutter Hello world程序之前,作爲一名Android 開發者,首先我們要認識到Flutter中沒有原生開發的XML,所有界面和邏輯代碼都在.dart文件中,Flutter給我提供了一套視覺、結構、平臺、和交互式的Widgets,所以在Flutter中一構架的一切界面都是Widgets。接下來我們先看一個簡單的Hello World Flutter應用。
Android Studio 新建Flutter demo
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget{
@override
Widget build(BuildContext context) {
// TODO: implement build
return new MaterialApp(
theme: ThemeData(
primaryColor: Colors.blueAccent,
),
home: new Scaffold(
appBar: new AppBar(
title: new Center(
child: new Text("Welcome to Flutter"),
)
),
body: DemoStatelessWidget("Flutter Hello World ! 無狀態的Widget"),
),
);
}
}
//無狀態 Widget
class DemoStatelessWidget extends StatelessWidget{
final String text;
//構造方法傳入 text 值
DemoStatelessWidget(this.text);
@override
Widget build(BuildContext context) {
// TODO: implement build
return Container(
constraints: BoxConstraints.expand(
height: Theme.of(context).textTheme.display1.fontSize * 1.1 + 200.0,
),
padding: const EdgeInsets.all(8.0),
color: Colors.blue[600],
alignment: Alignment.center,
child: Text(text,
style: Theme.of(context)
.textTheme
.display1
.copyWith(color: Colors.white)),
transform: Matrix4.rotationZ(0.1),
);
}
}
- 運行結果
- 如上代碼所示,首先main函數運行MyApp,MyApp 是一個DemoStatelessWidget,字面可以理解爲一個沒有狀態的Widget,他的build 方法創建了MaterialApp這個Widget才使得應用可以跑在手機上,接着創建了Scaffold 這個Widget,可以讓我們創建AppBar和頁面內容,body 的頁面內容又包含了一個無狀態的DemoStatelessWidget,通過構造方法可以傳入我們想要現實的頁面內容,該widget包含白色背景和一個現實文字的Text widget。
Flutter ListView
- 上個小例子中我們提到無狀態 StatelessWidget,想必也能猜到,肯定會有一個有狀態的widget,這個widget就是StatefulWidget,該widget爲何說是有狀態的呢,主要是在其管理的State中我們可以調用setState來動態改變頁面顯示。接着我們看一個顯示數據列表的例子,並加入一個可以點擊收藏的按鈕。
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';
void main() => runApp(MyApp());
// StatelessWidget 無狀態的widget
class MyApp extends StatelessWidget { //Stateless widgets是不可變的, 這意味着它們的屬性不能改變 - 所有的值都是最終的.
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
//title: 'Welcome to Flutter',
theme: ThemeData(
primaryColor: Colors.blueAccent,
),
home: new RandomWords(),
);
}
}
//StatefulWidget 有狀態的widget
class RandomWords extends StatefulWidget{
@override
createState() => new RandomWordsState();
}
// 返回 顯示單詞對的ListView Widget
class RandomWordsState extends State<RandomWords> {
//保存建議的單詞對列表(變量以下劃線(_)開頭,在Dart語言中使用下劃線前綴標識符,會強制其變成私有的) final _suggestions = <WordPair>[];
final List _suggestions = new List<WordPair>();
//設置字體大小的變量
final _biggerFont = const TextStyle(fontSize: 18.0);
// 保存喜歡單詞組的集合 set 集合不允許值重複
final Set _saved = new Set<WordPair>();
/// State 生命週期方法
@override
void initState() {
// state 初始化
super.initState();
}
@override
void didChangeDependencies() {
// 在 initState 之後調用,此時可以獲取其他 State
super.didChangeDependencies();
}
@override
void dispose() {
// state 銷燬
super.dispose();
}
@override
Widget build(BuildContext context) {
//return new Text(new WordPair.random().asPascalCase);
//返回單詞對的ListView。
return new Scaffold(
appBar: new AppBar(
title:new Center( //居中顯示
child: new Text('Flutter ListView'),
),
),
body: _buildSuggestions(),
);
}
//構建顯示建議單詞對的ListView。
Widget _buildSuggestions(){
return new ListView.builder(
padding: const EdgeInsets.all(16.0),
// 對於每個建議的單詞對都會調用一次itemBuilder,然後將單詞對添加到ListTile行中
// 在偶數行,該函數會爲單詞對添加一個ListTile row.
// 在奇數行,該函數會添加一個分割線widget,來分隔相鄰的詞對。
//itemBuilder 值是一個匿名回調函數, 接受兩個參數- BuildContext和行迭代器i。迭代器從0開始,
// 每調用一次該函數,i就會自增1,對於每個建議的單詞對都會執行一次。該模型允許建議的單詞對列表在用戶滾動時無限增長。
itemBuilder: (context,i){
// 在每一列之前,添加一個1像素高的分隔線widget
if(i.isOdd) return new Divider();
// 語法 "i ~/ 2" 表示i除以2,但返回值是整形(向下取整),比如i爲:1, 2, 3, 4, 5
// 時,結果爲0, 1, 1, 2, 2, 這可以計算出ListView中減去分隔線後的實際單詞對數量
final index = i~/2;
if(index >= _suggestions.length){
// 如果是建議單詞列表中最後一個單詞對 接着再生成10個單詞對,然後添加到建議列表
_suggestions.addAll(generateWordPairs().take(10));
}
return _buildRow(_suggestions[index]);
}
);
}
//創建 ListTile中顯示每個新詞對
Widget _buildRow(WordPair suggestion) {
//獲取是否保存了該單詞狀態
final isSaved = _saved.contains(suggestion);
return new ListTile(
// 設置 標題
title: new Text(
suggestion.asPascalCase,
style: _biggerFont,
),
//圖標
trailing: new Icon(
//星型圖標狀態
isSaved ? Icons.favorite : Icons.favorite_border,
color: isSaved ? Colors.deepOrange : null ,
),
onTap: (){ // 當用戶點擊 ListTile 擊時, ListTile 會調用它的onTap回調
setState(() { //調用setState() 會爲State對象觸發build()方法,從而導致對UI的更新
if(isSaved){
_saved.remove(suggestion);
}else{
_saved.add(suggestion);
}
});
},
);
}
}
- 運行效果
如上代碼,將原來的無狀態Widget改成了StatefulWidget,並在build中構建ListView,到此你可能有疑惑,不是說有狀態的Widget,怎麼還是創建Widget,有狀態如何體現呢? 別急,我們看到_buildRow方法,方法中構建了ListTile 這個Widget,它響應點擊事件回到爲onTap方法,也就是當我們點擊ListTile,我們在onTap方法中就可以調用setState方法來動態改變頁面顯示,也就是改變桃心收藏按鈕變化(注意setState方法需要在State類中才能調用)。
State 是有周期的,其中包括三個函數:
- initState():初始化方法
- didChangeDependencies():在 initState 之後調用,此時可以獲取其他 State
- dispose():state 銷燬
- 如果你使用Android Studio 我們可以使用快捷鍵快速創建StatelessWidget和StatefulWidget,創建StatelessWidget快捷鍵爲stl,創建StatefulWidget快捷鍵爲stf。
小節總結
- Flutter 頁面都是由一個個 widget 搭建而來的
- widget類型有兩種,一種是無狀態頁面內容固定的StatelessWidget,一種是頁面內容可以動態改變的 StatefulWidget
Widget 速覽
- 在開始開發實戰之前,我們需要對基本常用的Widget有個大概的認識。
layout Widget
- 在剛開始寫原生界面的時候,我們最先了解的也應該是佈局,Flutter 也提供了不少佈局widget,接下來列舉一些常用的佈局widget
佈局名稱 | 特點描述 |
---|---|
Container | 擁有單個子元素的佈局widget,可以靈活設置 |
Padding | 擁有單個子元素,給其子widget添加指定的填充 |
Center | 將其子widget居中顯示 |
Align | 將其子widget對齊,並可以根據子widget的大小自動調整大小。 |
Row | 可以擁有多個子元素,在水平方向上排列子widget的列表,和原生控件 LinerLayout orientation="horizontal" 類似 |
Column | 可以擁有多個子元素,在豎直方向上排列子widget的列表,和原生控件 LinerLayout orientation="vertical" 類似 |
Stack | 可以擁有多個子元素,允許其子widget簡單的堆疊在一起 |
Flow | 實現流式佈局算法的widget |
ListView | 可滾動的列表控件 |
- 瞭解更多的widget請參考 Flutter widget 目錄.
界面 Widget
- 有了佈局,還需要在佈局中填放各種控件,才最終組成我們的頁面,比如我們開發Material Design 風格的App,Flutter 就給我們提供了Material Components Widgets,接下來選取一些常用的控件來了解。瞭解更多的widget請參考 Material Components Widgets 目錄。
Widget名稱 | 特點描述 |
---|---|
MaterialApp | 封裝了應用程序實現Material Design所需要的一些widget,由前面demo可以發現它一般爲應用頂層入口widget |
Scaffold | Material Design佈局結構的基本實現。此類提供了用於顯示drawer、snackbar和底部sheet的API。 |
Appbar | 一般和Scaffold結合使用,可以設置頁面標題和各種按鈕等(Toolbar) |
BottomNavigationBar | 底部導航條,可以很容易地在tap之間切換和瀏覽頂級視圖 |
Drawer | 和Scaffold結合使用,從Scaffold邊緣水平滑動以顯示應用程序中導航鏈接的Material Design面板 |
RaisedButton | Material Design中的button,響應點擊事件(button) |
IconButton | 一個Material圖標按鈕,可以設置icon,點擊時會有水波動畫 |
TextField | 文本輸入框 (EditText) |
image | 顯示圖片的widget(ImageView) |
Text | 單一格式的文本 (TextView) |
導航欄返回按鈕監聽
- WillPopScope ,Flutter中可以通過WillPopScope來實現返回按鈕攔截
- 以下示例提供兩種效果,雙擊返回Toast提示(Toast庫 fluttertoast: ^3.1.3),或者彈出提示dialog是否退出。
import 'package:flutter/material.dart';
class AppPage extends StatefulWidget {
@override
_AppPageState createState() => _AppPageState();
}
class _AppPageState extends State<AppPage> {
@override
Widget build(BuildContext context) {
return WillPopScope( ///通過WillPopScope 嵌套,可以用於監聽處理 Android 返回鍵的邏輯。 WillPopScope 並不是監聽返回按鍵,只是當前頁面將要被pop時觸發的回調
child: Container(),
onWillPop: () async{
return _doubleExitApp();
}
);
}
//雙擊返回 退出應用
bool _doubleExitApp(){
if (_lastPressedAt == null ||
DateTime.now().difference(_lastPressedAt) > Duration(seconds: 1)) {
ToolUtils.ShowToast(msg: "再點一次退出應用");
//兩次點擊間隔超過1秒則重新計時
_lastPressedAt = DateTime.now();
return false;
}
//應用關閉直接取消 Toast
Fluttertoast.cancel();
return true;
}
///如果返回 return new Future.value(false); popped 就不會被處理
///如果返回 return new Future.value(true); popped 就會觸發
///這裏可以通過 showDialog 彈出確定框,在返回時通過 Navigator.of(context).pop(true);決定是否退出
/// 單擊提示退出
Future<bool> _dialogExitApp(BuildContext context) {
return showDialog(
context: context,
builder: (context) => new AlertDialog(
content: new Text("是否退出"),
actions: <Widget>[
new FlatButton(onPressed: () => Navigator.of(context).pop(false), child: new Text("取消")),
new FlatButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: new Text("確定"))
],
));
}
}
最後
- 萬事開頭難,對於Flutter到此可以說是邁出了第一步,對於未嘗試的東西,開始可能會有畏懼心理,但只要敢於嘗試,敢於邁出第一步,相信難不難只在於自己的用心程度而言。本篇文章就先到這裏,文章中如果有錯誤,請大家給我提出來,大家一起學習進步,如果覺得我的文章給予你幫助,也請給我一個喜歡和關注,同時也歡迎訪問我的個人博客。
- Flutter完整開源項目: https://github.com/maoqitian/flutter_wanandroid