Flutter學習指南:編寫第一個應用

玉剛說發表於2019-03-04

這是個系列文章,後面還有很多篇,希望對大家能有幫助。

Flutter 是 Google 推出的移動端跨平臺開發框架,使用的程式語言是 Dart。從 React Native 到 Flutter,開發者對跨平臺解決方案的探索從未停止,畢竟,它可以讓我們節省移動端一半的人力。本篇文章中,我們就通過編寫一個簡單的 Flutter 來了解他的開發流程。

這裡我們要開發的 demo 很簡單,只是在螢幕中間放一個按鈕,點選的時候,模擬搖兩個骰子並彈窗顯示結果。我們擼起袖子開幹吧。

建立專案

我們這裡假定讀者已經安裝好 Flutter,並且使用安裝了 Flutter 外掛的 Android Studio 進行開發。如果你還沒有配置好開發環境,可以參考 這篇文章

下面我們開始建立專案:

  1. 選擇 File > New > New Flutter project…
  2. 在接下來彈出的選擇皮膚裡,選擇 Flutter Application
  3. 這裡填應用的基本資訊。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 應用跑起來吧。沒有意外的話,你會看到下面這個頁面:

Screenshot with a button in the center
Screenshot with a button in the center

如果你遇到了什麼困難,可以檢視 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。

Screenshot with a dialog
Screenshot with a 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 來除錯。

  1. final rollResults = _roll() 這一行打個斷點
  2. 然後點選 Debug main.dart 開始除錯
  3. 點選 APP 裡的 Roll 按鈕

現在,應用停在了我們所打的斷點處:

debug step1
debug step1

接下來:

  1. Step Into 進入 _roll 方法
  2. 進入 _roll 後,Step Over 一行一行執行。
debug step2
debug step2

這裡我們看到,兩次 random 確實產生了不同的結果。我們繼續:

  1. 還是 Step Over,這個時候 _roll 就返回了
  2. 切換到 Variables 這個選項卡,檢視 rollResults 的值
debug step3
debug step3

可以發現,兩個結果居然變成一樣的了。再往回檢視一下程式碼,我們寫的是 return [roll1, roll1]。修改後一個為 roll2,程式就能夠按預期的正常執行了。

最終的程式碼,可以看 tag first_app_done。

除錯總結

本篇文章其實介紹了兩種除錯方法:打 log 和 debugger。雖然現在 Flutter 提供的 log 工具比較簡陋,可以預期未來還會進一步完善。

使用打 log 的方式,好處在於不會對執行流程產生較大的影響,在多執行緒環境尤為有用。它的速度也比較快,不需要我們去單步執行。不足之處在於,如果原先沒有對應的 log,我們只能修改程式碼重新執行,才能檢視相應的狀態。對於線上的應用,我們也只能夠通過分析 log 來定位問題。

debugger 跟打 log 方式是互補的。使用 debugger 時,我們可以隨意檢視我們需要知道的變數的值,一步一步近距離觀察程式碼的執行狀態。壞處當然就是太慢了。在什麼時候使用什麼方法,需要一些經驗;但有時候就全憑個人喜好了,沒有優劣之分。

更多的除錯方法,讀者可以根據需要檢視flutter.io/debugging/進一步學習。

打包

編寫完應用後,就得打包 apk 分發給使用者使用了。在這一小節,我們來看看怎麼給 Flutter 專案打包。

在專案的根目錄,有一個 android 資料夾,下面我們將主要對這個目錄的檔案進行修改。

  1. 檢視 AndroidManifest.xml。這是一個按模板生成的檔案,有些東西可能需要修改一下
  2. build.gradle,這裡面也可能有你需要修改的地方。對我們的應用來說,目前都先維持原樣
  3. 如果有需要,更新 res/mipmap 裡的應用啟動圖示,這裡我們不改
  4. 簽名,前面略微複雜一些,下面詳細展開一下。

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步。

  1. 新增 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.**  { *; }
    複製程式碼
  2. 編譯 apk。在專案的根目錄,執行 flutter build apk, 編譯後的應用在 build/app/outputs/apk/release/app-release.apk。

  3. 還是在根目錄下,執行 flutter install 就可以安裝這個 apk 了。

對於 iOS,讀者可以看flutter.io/ios-release…,這裡就不再演示了。 檢視最終的專案,可以 checkout 到 tag first_app_signing。恭喜你,第一個 Flutter 應用完成啦。

Flutter學習指南:編寫第一個應用
歡迎關注我的微信公眾號「玉剛說」,接收第一手技術乾貨

相關文章