這是個系列文章,後面還有很多篇,希望對大家能有幫助。
Flutter 是 Google 推出的移動端跨平臺開發框架,使用的程式語言是 Dart。從 React Native 到 Flutter,開發者對跨平臺解決方案的探索從未停止,畢竟,它可以讓我們節省移動端一半的人力。本篇文章中,我們就通過編寫一個簡單的 Flutter 來了解他的開發流程。
這裡我們要開發的 demo 很簡單,只是在螢幕中間放一個按鈕,點選的時候,模擬搖兩個骰子並彈窗顯示結果。我們擼起袖子開幹吧。
建立專案
我們這裡假定讀者已經安裝好 Flutter,並且使用安裝了 Flutter 外掛的 Android Studio 進行開發。如果你還沒有配置好開發環境,可以參考 這篇文章。
下面我們開始建立專案:
- 選擇 File > New > New Flutter project…
- 在接下來彈出的選擇皮膚裡,選擇 Flutter Application
- 這裡填應用的基本資訊。Project name 我們就寫 flutter_demo 好了。這裡要注意的是,Project name 必須是一個合法的 Dart 包名(小寫+下劃線,可以有數字)。填好以後點選 next,然後 finish。
第一次建立專案時,由於要下載 gradle,時間會稍微長一些。
編寫程式碼(1)
在上一小節裡我們所建立的專案,已經有了一些程式碼,感興趣的讀者可以跑到自己手機上看一看,相關的程式碼在 lib/main.dart 裡面。
為了體驗從頭開發一個應用的過程,這裡我們先把 lib/main.dart 裡的內容都刪除。
首先,建立一個 main
函式。跟其他語言一樣,main
函式是應用的入口:
void main() {
}
複製程式碼
下面我們編寫一個 Widget
作為我們的 app。在 Flutter 裡,所有的東西都是 Widget
。
import 'package:flutter/material.dart';
void main() {
// 建立一個 MyApp
runApp(MyApp());
}
/// 這個 widget 作用這個應用的頂層 widget.
///
/// 這個 widget 是無狀態的,所以我們繼承的是 [StatelessWidget].
/// 對應的,有狀態的 widget 可以繼承 [StatefulWidget]
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 建立內容
}
}
複製程式碼
現在我們進入正題,實現一個按鈕,在點選的時候彈框顯示結果:
@override
Widget build(BuildContext context) {
// 我們想使用 material 風格的應用,所以這裡用 MaterialApp
return MaterialApp(
// 移動裝置使用這個 title 來表示我們的應用。具體一點說,在 Android 裝置裡,我們點選
// recent 按鈕開啟最近應用列表的時候,顯示的就是這個 title。
title: 'Our first Flutter app',
// 應用的“主頁”
home: Scaffold(
appBar: AppBar(
title: Text('Flutter rolling demo'),
),
// 我們知道,Flutter 裡所有的東西都是 widget。為了把按鈕放在螢幕的中央,
// 這裡使用了 Center(它是一個 widget)。
body: Center(
child: RaisedButton(
// 使用者點選時候呼叫
onPressed: _onPressed,
child: Text('roll'),
),
),
),
);
}
void _onPressed() {
// TODO
}
複製程式碼
安裝、除錯(1)
現在,點選 Run,把我們的第一個 Flutter 應用跑起來吧。沒有意外的話,你會看到下面這個頁面:
如果你遇到了什麼困難,可以檢視 tag first_app_step1 的程式碼:
git clone https://github.com/Jekton/flutter_demo.git
cd flutter_demo
git checkout first_app_step1
複製程式碼
由於是第一次寫 Flutter 應用,我們對上面的程式碼是否能夠按照預期執行還不是那麼有信心,所以我們先打個 log 確認一下,點選按鈕後是不是真的會執行 onPress
。
打 log 可以使用 Dart 提供的 print
,但在日誌比較多的時候,print
的輸出可能會被 Android 丟棄,這個時候 debugPrint
會是更好的選擇。對應的日誌資訊可以在 Dart Console 裡檢視(View -> Tool Windows -> Run 或者 Mac 上使用 Command+4 開啟)。
void _onPressed() {
debugPrint('_onPressed');
}
複製程式碼
儲存後(會自動 Hot Reload),我們再次點選按鈕,在我的裝置上,列印出了下面這樣的資訊:
I/flutter (11297): _onPressed
V/AudioManager(11297): playSoundEffect effectType: 0
V/AudioManager(11297): querySoundEffectsEnabled...
複製程式碼
這裡的第一行,就是我們打的。現在我們有足夠的自信說,點選按鈕後,會執行 _onPressed
方法了。
編寫程式碼(2)
軟體開發通常是一個螺旋式上升的過程,不可能通過一次編碼、除錯就完成。現在,我們開始第二輪迭代。
接下來要做的,便是在 _onPressed
裡面彈一個框:
// context 這裡使用的是 MyApp.build 的引數
void _onPressed(BuildContext context) {
debugPrint('_onPressed');
showDialog(
context: context,
builder: (_) {
return AlertDialog(
content: Text('AlertDialog'),
);
}
);
}
複製程式碼
遺憾的是,這一次並不那麼順利。Dialog 沒有彈出來,而且報了下面這問題:
I/flutter (11297): Navigator operation requested with a context that does not include a Navigator.
I/flutter (11297): The context used to push or pop routes from the Navigator must be that of a widget that is a
I/flutter (11297): descendant of a Navigator widget.
複製程式碼
原因在於,stateless 的 widget
只能用於顯示資訊,不能有其他動作。所以,該讓 StatefulWidget
上場了。
class RollingButton extends StatefulWidget {
// StatefulWidget 需要實現這個方法,返回一個 State
@override
State createState() {
return _RollingState();
}
}
// 可能看起來有點噁心,這裡的泛型引數居然是 RollingButton
class _RollingState extends State<RollingButton> {
@override
Widget build(BuildContext context) {
return RaisedButton(
child: Text('Roll'),
onPressed: _onPressed,
);
}
void _onPressed() {
debugPrint('_RollingState._onPressed');
showDialog(
// 第一個 context 是引數名,第二個 context 是 State 的成員變數
context: context,
builder: (_) {
return AlertDialog(
content: Text('AlertDialog'),
);
}
);
}
}
複製程式碼
要實現一個 stateful 的 widget
,可以繼承 StatefulWidget
並在 createState
方法中返回一個 State
。除了這一部分,程式碼跟我們之前寫的並沒有太大的區別。
剩下的,就是替換 MyApp
裡面使用的按鈕,修改後的程式碼如下:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Our first Flutter app',
home: Scaffold(
appBar: AppBar(
title: Text('Flutter rolling demo'),
),
body: Center(
child: RollingButton(),
),
),
);
}
}
複製程式碼
再次執行,點選按鈕後,我們將看到夢寐以求的 dialog。
如果你遇到了麻煩,可以檢視 tag first_app_step2 的程式碼。
最後,我們來實現“roll”:
import 'dart:math';
class _RollingState extends State<RollingButton> {
final _random = Random();
// ...
List<int> _roll() {
final roll1 = _random.nextInt(6) + 1;
final roll2 = _random.nextInt(6) + 1;
return [roll1, roll1];
}
void _onPressed() {
debugPrint('_RollingState._onPressed');
final rollResults = _roll();
showDialog(
// 第一個 context 是引數名,第二個 context 是 State 的成員變數
context: context,
builder: (_) {
return AlertDialog(
content: Text('Roll result: (${rollResults[0]}, ${rollResults[1]})'),
);
}
);
}
}
複製程式碼
安裝、除錯(2)
還是一樣,重新執行後,我們就能夠看到每次點選按鈕的結果隨機地出現 [1, 6] 中的數……慢著,怎麼彈出的訊息裡的兩個號碼總是一樣的!好吧,肯定是哪裡出錯了。
這次,我們不採用打 log 的方法,改用 debugger 來除錯。
- 在
final rollResults = _roll()
這一行打個斷點 - 然後點選 Debug main.dart 開始除錯
- 點選 APP 裡的 Roll 按鈕
現在,應用停在了我們所打的斷點處:
接下來:
- Step Into 進入
_roll
方法 - 進入
_roll
後,Step Over 一行一行執行。
這裡我們看到,兩次 random 確實產生了不同的結果。我們繼續:
- 還是 Step Over,這個時候
_roll
就返回了 - 切換到 Variables 這個選項卡,檢視
rollResults
的值
可以發現,兩個結果居然變成一樣的了。再往回檢視一下程式碼,我們寫的是 return [roll1, roll1]
。修改後一個為 roll2
,程式就能夠按預期的正常執行了。
最終的程式碼,可以看 tag first_app_done。
除錯總結
本篇文章其實介紹了兩種除錯方法:打 log 和 debugger。雖然現在 Flutter 提供的 log 工具比較簡陋,可以預期未來還會進一步完善。
使用打 log 的方式,好處在於不會對執行流程產生較大的影響,在多執行緒環境尤為有用。它的速度也比較快,不需要我們去單步執行。不足之處在於,如果原先沒有對應的 log,我們只能修改程式碼重新執行,才能檢視相應的狀態。對於線上的應用,我們也只能夠通過分析 log 來定位問題。
debugger 跟打 log 方式是互補的。使用 debugger 時,我們可以隨意檢視我們需要知道的變數的值,一步一步近距離觀察程式碼的執行狀態。壞處當然就是太慢了。在什麼時候使用什麼方法,需要一些經驗;但有時候就全憑個人喜好了,沒有優劣之分。
更多的除錯方法,讀者可以根據需要檢視flutter.io/debugging/進一步學習。
打包
編寫完應用後,就得打包 apk 分發給使用者使用了。在這一小節,我們來看看怎麼給 Flutter 專案打包。
在專案的根目錄,有一個 android 資料夾,下面我們將主要對這個目錄的檔案進行修改。
- 檢視 AndroidManifest.xml。這是一個按模板生成的檔案,有些東西可能需要修改一下
- build.gradle,這裡面也可能有你需要修改的地方。對我們的應用來說,目前都先維持原樣
- 如果有需要,更新 res/mipmap 裡的應用啟動圖示,這裡我們不改
- 簽名,前面略微複雜一些,下面詳細展開一下。
1) 生成簽名的 key(如果你已經有了,跳過這一步),為了讓讀者也可以編譯,這裡我把 key 也放到了專案中。
keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key
複製程式碼
2) 新增一個 android/key.properties,內容如下:
storePassword=123456
keyPassword=123456
keyAlias=key
storeFile=../key.jks
複製程式碼
3) 更新 build.gradle 裡的簽名配置
def keystorePropertiesFile = rootProject.file("key.properties")
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
android {
// ...
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
useProguard true
// proguard 檔案我們在下一步新增
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
複製程式碼
簽名資訊配置完畢後,下面進行第5步。
新增 android/app/proguard-rules.pro:
#Flutter Wrapper
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.util.** { *; }
-keep class io.flutter.view.** { *; }
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }
複製程式碼編譯 apk。在專案的根目錄,執行
flutter build apk
, 編譯後的應用在 build/app/outputs/apk/release/app-release.apk。還是在根目錄下,執行
flutter install
就可以安裝這個 apk 了。
對於 iOS,讀者可以看flutter.io/ios-release…,這裡就不再演示了。 檢視最終的專案,可以 checkout 到 tag first_app_signing。恭喜你,第一個 Flutter 應用完成啦。