計算小費

1. 事前準備

在本程式碼研究室中,您將編寫小費計算機的程式碼,與先前在程式碼研究室「為 Android 建立 XML 版面配置」中建立的 UI 搭配使用。

必要條件

課程內容

  • Android 應用程式的基本結構。
  • 如何讀取 UI 中的值,將這些值寫入程式碼並進行操控。
  • 如何使用檢視區塊繫結 (而不是 findViewById()) 來更輕鬆地編寫與檢視區塊互動的程式碼。
  • 如何搭配 Double 資料類型使用 Kotlin 中的十進位數字。
  • 如何將數字的格式設定為貨幣。
  • 如何使用字串參數來動態建立字串。
  • 如何在 Android Studio 中使用 Logcat 來找出應用程式的問題。

建構項目

  • 一款小費計算機應用程式,帶有可正常運作的「Calculate」按鈕。

軟硬體需求

  • 已安裝最新穩定版 Android Studio 的電腦。
  • Tip Time 應用程式的範例程式碼,其中包含小費計算機的版面配置。

2. 範例應用程式總覽

上一個程式碼研究室中的 Tip Time 應用程式會提供小費計算機所需的所有 UI,但不顯示用於計算小費的程式碼。系統顯示「Calculate」按鈕,但該按鈕尚無法正常運作。使用者可以使用「Cost of Service」EditText 輸入服務費用。RadioButtons 清單可讓使用者選取小費百分比,而 Switch 可讓使用者選擇是否應將小費四捨五入。小費金額顯示在 TextView 中,最後「Calculate」Button 會通知應用程式從其他欄位取得資料並計算小費金額。本程式碼研究室從這裡繼續介紹。

ebf5c40d4e12d4c7.png

應用程式專案結構

IDE 中的應用程式專案由多個部分組成,包括 Kotlin 程式碼、XML 版面配置,以及字串和圖片等其他資源。建議您先熟悉環境,然後再對應用程式進行變更。

  1. 在 Android Studio 中開啟 Tip Time 專案。
  2. 如果系統未顯示「Project」視窗,請選取 Android Studio 左側的「Project」分頁標籤。
  3. 從下拉式選單中選擇 Android 檢視區塊 (如果尚未選取該檢視區塊)。

2a83e2b0aee106dd.png

  • Kotlin 檔案 (或 Java 檔案) 的 java 資料夾
  • MainActivity - 小費計算機邏輯的所有 Kotlin 程式碼所屬的類別
  • 應用程式資源的 res 資料夾
  • activity_main.xml - Android 應用程式的版面配置檔案
  • strings.xml - 包含 Android 應用程式的字串資源
  • Gradle Scripts 資料夾

Gradle 是 Android Studio 使用的自動建構系統。當您變更程式碼、新增資源或對應用程式進行其他變更時,Gradle 會判斷變更的內容,並採取必要步驟來重新建構應用程式。它還會在模擬器或實體裝置上安裝您的應用程式,並控管其執行作業。

建構應用程式時還須用到其他資料夾和檔案,但這些是您在本程式碼研究室和接下來的程式碼研究室中將會使用的主要資料夾和檔案。

3. 檢視區塊繫結

為了計算小費,程式碼必須存取所有 UI 元素才能讀取使用者的輸入內容。您可以回想一下,在先前的程式碼研究室中,程式碼必須先找到 View 的參照 (例如 ButtonTextView),然後才能呼叫 View 上的方法或存取其屬性。Android 架構提供 findViewById() 方法,可根據您的需求執行動作,即在指定 View ID 的情況下傳回其參照)。這種方法有用,但隨著您在應用程式中新增更多檢視區塊,而 UI 也變得更加複雜,使用 findViewById() 可能有點麻煩。

為方便起見,Android 還提供一項名為檢視區塊繫結的功能。只要提前多做一點工作,檢視區塊繫結就能讓您在 UI 的檢視區塊中更輕鬆、更快速地呼叫方法。您必須在 Gradle 中為應用程式啟用檢視區塊繫結,並對程式碼進行一些變更。

啟用檢視區塊繫結

  1. 開啟應用程式的 build.gradle 檔案 (Gradle Scripts > build.gradle (模組:Tip_Time.app))
  2. android 部分中,新增以下行:
buildFeatures {
    viewBinding = true
}
  1. 注意以下訊息:「Gradle files have changed since last project sync」
  2. 按下「Sync Now」

349d99c67c2f40f1.png

片刻過後,您應該會在 Android Studio 視窗底部看到訊息「Gradle sync finished」。您可以視需要關閉 build.gradle 檔案。

初始化繫結物件

在先前的程式碼研究室中,您已經看到屬於 MainActivity 類別的 onCreate() 方法。這是應用程式啟動並初始化 MainActivity 時最先呼叫的內容之一。您將建立並初始化繫結物件一次,而無須為應用程式中的每個 View 呼叫 findViewById()

674d243aa6f85b8b.png

  1. 開啟 MainActivity.kt (依序點選「app」>「java」>「com.example.tiptime」>「MainActivity」)。
  2. MainActivity 類別的所有現有程式碼替換為此程式碼,設定 MainActivity 以使用檢視區塊繫結:
class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }
}
  1. 此行在繫結物件的類別中宣告頂層變數。之所以在此層級定義該變數,是因為將在 MainActivity 類別的多種方法中使用該變數。
lateinit var binding: ActivityMainBinding

lateinit 關鍵字是全新內容。應保證程式碼先初始化變數,然後再使用該變數。否則,您的應用程式將當機。

  1. 此行會初始化 binding 物件,您將使用該物件存取 activity_main.xml 版面配置中的 Views
binding = ActivityMainBinding.inflate(layoutInflater)
  1. 設定活動的內容檢視區塊。此行將指定應用程式中檢視區塊階層的根層級 binding.root,而不是傳遞版面配置 R.layout.activity_main 的資源 ID。
setContentView(binding.root)

您可以回想起父檢視區塊和子檢視區塊的概念;該根層級可連結所有這些檢視區塊。

現在,在應用程式中需要 View 的參照時,可從 binding 物件中取得,而無須呼叫 findViewById()binding 物件會自動為應用程式中每個擁有 ID 的 View 定義參照。使用檢視區塊繫結這種方式更為簡潔,通常您甚至無須建立變數來保留 View 的參照,只要直接在繫結物件中使用該變數即可。

// Old way with findViewById()
val myButton: Button = findViewById(R.id.my_button)
myButton.text = "A button"

// Better way with view binding
val myButton: Button = binding.myButton
myButton.text = "A button"

// Best way with view binding and no extra variable
binding.myButton.text = "A button"

這真是太棒了!

4. 計算小費

使用者輕觸「Calculate」按鈕時,系統開始計算小費。須執行的動作包括檢查 UI,瞭解具體服務費用以及使用者想給的小費百分比。運用這些資訊,您可以計算服務費用的總金額並顯示小費金額。

將點擊事件監聽器新增至按鈕

第一步是新增點擊事件監聽器,以指定使用者輕觸「Calculate」按鈕時,該按鈕應執行的操作。

  1. onCreate()MainActivity.kt 中,呼叫 setContentView() 之後,請在「Calculate」按鈕上設定點擊事件監聽器,並讓它呼叫 calculateTip()
binding.calculateButton.setOnClickListener{ calculateTip() }
  1. 仍在 MainActivity 類別中 (但在 onCreate() 之外),新增名為 calculateTip() 的輔助方法。
fun calculateTip() {

}

請在這裡新增程式碼,以便檢查 UI 並計算小費。

MainActivity.kt

class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.calculateButton.setOnClickListener{ calculateTip() }
    }

    fun calculateTip() {

    }
}

取得服務費用

如要計算小費,首先須計算服務費用。文字會儲存在 EditText 中,但您需要數字格式才能計算。您可能還記得其他程式碼研究室中介紹過的 Int 類型,但 Int 只能保留整數。如要在應用程式中使用十進位數字,請使用名為 Double 的資料類型,而並非 Int。如要進一步瞭解 Kotlin 中的數字資料類型,請參閱説明文件。Kotlin 提供用於將 String 轉換為 Double 的方法 (稱為 toDouble())。

  1. 首先,請取得服務費用的文字。在 calculateTip() 方法中,取得「Cost of Service」EditText的文字屬性,然後指派給稱為 stringInTextField 的變數。請記住,您可以使用 binding 物件存取 UI 元素,還可以根據 UI 元素採用駝峰式大小寫形式的資源 ID 來參照 UI 元素。
val stringInTextField = binding.costOfService.text

請注意結尾處的 .text。第一部分,binding.costOfService 參照服務費用的 UI 元素。在結尾處加上 .text 表示要取得該結果 (EditText 物件) 並從中取得 text 屬性。這就是所謂的鏈結,是 Kotlin 中一種很常見的模式。

  1. 接下來,將文字轉換為十進位數字。在 stringInTextField 上呼叫 toDouble(),並儲存在名為 cost 的變數中。
val cost = stringInTextField.toDouble()

但這種做法行不通 — 必須在 String 上呼叫 toDouble()。結果顯示,EditTexttext 屬性為 Editable,因為它代表可以變更的文字。幸好,您可以透過在 Editable 上呼叫 toString() 將其轉換為 String

  1. binding.costOfService.text 上呼叫 toString() 並將其轉換為 String
val stringInTextField = binding.costOfService.text.toString()

現在,stringInTextField.toDouble() 可以正常運作。

此時,calculateTip() 方法應如下所示:

fun calculateTip() {
    val stringInTextField = binding.costOfService.text.toString()
    val cost = stringInTextField.toDouble()
}

取得小費百分比

到目前為止,您已取得服務費用。現在您需要使用者從 RadioButtonsRadioGroup 中選取的小費百分比。

  1. calculateTip() 中,取得 tipOptions RadioGroupcheckedRadioButtonId 屬性,並將其指派給名為 selectedId 的變數。
val selectedId = binding.tipOptions.checkedRadioButtonId

現在您知道選取哪個 RadioButton (R.id.option_twenty_percentR.id.option_eighteen_percentR.id.fifteen_percent 其中之一),但還需要相應的百分比。您可以編寫一系列 if/else 陳述式,但使用 when 運算式會簡單許多。

  1. 新增下列行即可取得小費百分比。
val tipPercentage = when (selectedId) {
    R.id.option_twenty_percent -> 0.20
    R.id.option_eighteen_percent -> 0.18
    else -> 0.15
}

此時,calculateTip() 方法應如下所示:

fun calculateTip() {
    val stringInTextField = binding.costOfService.text.toString()
    val cost = stringInTextField.toDouble()
    val selectedId = binding.tipOptions.checkedRadioButtonId
    val tipPercentage = when (selectedId) {
        R.id.option_twenty_percent -> 0.20
        R.id.option_eighteen_percent -> 0.18
        else -> 0.15
    }
}

計算小費並四捨五入

現在您已得知服務費用和小費百分比,要計算小費就十分容易:只要將費用乘以小費百分比,即可計算出小費,即小費 = 服務費用 *小費百分比。您可以視需要將該值四捨五入。

  1. calculateTip() 中您新增的其他程式碼後面,將 tipPercentage 乘以 cost,然後將計算出的值指派給名為 tip 的變數。
var tip = tipPercentage * cost

請注意,使用 var 而非 val。這是因為使用者選取該選項時,您可能需要將這個值四捨五入,因此值可能會有所異動。

如果是 Switch 元素,請檢查 isChecked 屬性,確認切換按鈕是否「開啟」。

  1. 將四捨五入切換按鈕的 isChecked 屬性指派給名為 roundUp 的變數。
val roundUp = binding.roundUpSwitch.isChecked

字詞「四捨五入」是指將小數點四捨五入至最接近的整數值,但在這種情況下,您只會無條件進位或無條件進位至最接近的指定基數倍數。您可以使用 ceil() 函式來執行這項操作。有數個函式都採用該名稱,但您需要的是在 kotlin.math 中定義的函式。您可以新增 import 陳述式,但在這種情況下,較簡單的做法是直接使用 kotlin.math.ceil() 告知 Android Studio 您所需的函式。

32c29f73a3f20f93.png

如要使用多個數學函式,新增 import 陳述式會是比較簡單的方式。

  1. 新增 if 陳述式,用於在 roundUp 為 true 時將小費上限指派給 tip 變數。
if (roundUp) {
    tip = kotlin.math.ceil(tip)
}

此時,calculateTip() 方法應如下所示:

fun calculateTip() {
    val stringInTextField = binding.costOfService.text.toString()
    val cost = stringInTextField.toDouble()
    val selectedId = binding.tipOptions.checkedRadioButtonId
    val tipPercentage = when (selectedId) {
        R.id.option_twenty_percent -> 0.20
        R.id.option_eighteen_percent -> 0.18
        else -> 0.15
    }
    var tip = tipPercentage * cost
    val roundUp = binding.roundUpSwitch.isChecked
    if (roundUp) {
        tip = kotlin.math.ceil(tip)
    }
}

設定小費格式

您的應用程式幾乎可以正常運作。您已計算小費,現在只要設定小費格式並加以顯示即可。

正如您所預期的那樣,Kotlin 提供用於為不同類型的數字設定格式的方法。但小費金額會略有不同,它代表貨幣價值。不同的國家/地區使用不同的貨幣,並且在設定十進位數字的格式方面有不同的規則。例如,以美元為單位時,1234.56 的格式應為 $1,234.56 美元,但以歐元為單位時,格式應為 €1.234,56 歐元。幸好,Android 架構提供用於將數字的格式設定為貨幣的方法,因此您不必瞭解所有可能的做法。系統會根據使用者在手機上選擇的語言和其他設定,自動設定貨幣格式。如要進一步瞭解數字格式,請參閱 Android 開發人員說明文件。

  1. calculateTip() 中的其他程式碼後面,呼叫 NumberFormat.getCurrencyInstance()
NumberFormat.getCurrencyInstance()

系統會為您提供數字格式設定工具,以便您將數字格式設為貨幣。

  1. 使用數字格式設定工具時,可將 format() 方法的呼叫鏈結至 tip,並將結果指派給名為 formattedTip 的變數。
val formattedTip = NumberFormat.getCurrencyInstance().format(tip)
  1. 請注意,NumberFormat 顯示為紅色。這是因為 Android Studio 無法自動判斷出您需要哪個版本的 NumberFormat
  2. 將游標懸停在 NumberFormat 上方,然後在顯示的彈出式視窗中選擇「Import」d9d2f92d5ef01df6.png
  3. 從可能的匯入內容清單中選擇 NumberFormat (java.text)。Android Studio 會在 MainActivity 檔案的頂端加上 import 陳述式,但 NumberFormat 不再顯示為紅色。

顯示小費

現在,您必須在應用程式的小費金額 TextView 元素中顯示小費。您可以將 formattedTip 指派給 text 屬性,不過最好能夠加上標籤説明金額代表的意義。在使用英文的美國,系統可能會顯示「Tip Amount: $12.34」,但使用其他語言時,數字可能需要出現在字串開頭甚或中間。Android 架構提供名為「字串參數」的機制,讓翻譯應用程式的人可視需要變更數字的顯示位置。

  1. 開啟 strings.xml (「app」>「res」>「values」>「strings.xml」)
  2. tip_amount 字串從 Tip Amount 變更為 Tip Amount: %s
<string name="tip_amount">Tip Amount: %s</string>

%s 插入已設定格式的貨幣。

  1. 現在請設定 tipResult 的文字。返回 MainActivity.kt 中的 calculateTip() 方法,呼叫 getString(R.string.tip_amount, formattedTip),然後將其指派給小費結果 TextViewtext 屬性。
binding.tipResult.text = getString(R.string.tip_amount, formattedTip)

此時,calculateTip() 方法應如下所示:

fun calculateTip() {
    val stringInTextField = binding.costOfService.text.toString()
    val cost = stringInTextField.toDouble()
    val selectedId = binding.tipOptions.checkedRadioButtonId
    val tipPercentage = when (selectedId) {
        R.id.option_twenty_percent -> 0.20
        R.id.option_eighteen_percent -> 0.18
        else -> 0.15
    }
    var tip = tipPercentage * cost
    val roundUp = binding.roundUpSwitch.isChecked
    if (roundUp) {
        tip = kotlin.math.ceil(tip)
    }
    val formattedTip = NumberFormat.getCurrencyInstance().format(tip)
    binding.tipResult.text = getString(R.string.tip_amount, formattedTip)
}

就快大功告成了。開發應用程式 (以及檢視預覽畫面) 時,建議您為該 TextView 設定預留位置,這之後會派上用場。

  1. 開啟 activity_main.xml (依序點選「App」>「Res」>「Layout」>「activity_main.xml」)。
  2. 找出 tip_result TextView
  3. 移除包含 android:text 屬性的那一行。
android:text="@string/tip_amount"
  1. 為設定為 Tip Amount: $10tools:text 屬性新增一行。
tools:text="Tip Amount: $10"

由於這是預留位置,因此無須將字串擷取至資源。執行應用程式時,它不會顯示。

  1. 請注意,工具文字會顯示在版面配置編輯器中。
  2. 執行應用程式。輸入費用金額並選取一些選項,然後按「Calculate」按鈕。

42fd6cd5e24ca433.png

恭喜,它可以正常運作!如果沒有取得正確的小費金額,請返回本部分的步驟 1,確認您已對程式碼進行了所有必要的變更。

5. 測試並偵錯

您已經在各個步驟中執行過該應用程式,確保它符合預期,但現在必須進行一些額外的測試。

現在,不妨思考使用 calculateTip() 方法時,資訊是如何在應用程式中移動的,以及每個步驟可能會出現哪些問題。

例如,在此行中:

val cost = stringInTextField.toDouble()

如果 stringInTextField 不代表數字,會發生什麽?如果使用者未輸入任何文字,stringInTextField 顯示空白,會發生什麽?

  1. 在模擬器中執行應用程式,但使用「Run」>「Debug ‘app'」,而不是使用「Run」>「Run ‘app'」
  2. 請嘗試費用、小費金額以及小費是否四捨五入的不同組合,並確認您在各種情況下輕觸「Calculate」時是否能夠取得預期結果。
  3. 現在,請嘗試刪除「Cost of Service」欄位中的所有文字,然後輕觸「Calculate」。糟糕,您的程式已當機。

偵錯當機問題

處理錯誤的第一步是找出問題。Android Studio 會將系統中發生的情況保留在記錄中,讓您瞭解問題所在。

  1. 按下 Android Studio 底部的「Logcat」按鈕,或在選單中選擇「View」>「Tools Windows」>「Logcat」

1b68ee5190018c8a.png

  1. 「Logcat」視窗顯示在 Android Studio 的底部,其中填滿奇怪的文字。22139575476ae9d.png

這段文字是「堆疊追蹤」,其中會列出當機時將會呼叫哪個方法。

  1. Logcat 文字中向上捲動,直到找出包含 FATAL EXCEPTION 文字的行。
2020-06-24 10:09:41.564 24423-24423/com.example.tiptime E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.tiptime, PID: 24423
    java.lang.NumberFormatException: empty String
        at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)
        at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
        at java.lang.Double.parseDouble(Double.java:538)
        at com.example.tiptime.MainActivity.calculateTip(MainActivity.kt:22)
        at com.example.tiptime.MainActivity$onCreate$1.onClick(MainActivity.kt:17)
  1. 向下閱讀,直到找出 NumberFormatException 所在的那一行。
java.lang.NumberFormatException: empty String

畫面右側顯示 empty String。例外狀況的類型會指出它與數字格式相關,其餘部分會顯示問題的基本資訊:找到空白的 String,它原本應該是包含值的 String

  1. 繼續往下閱讀,即可看到 parseDouble() 的呼叫。
  2. 在這些呼叫下方,找出 calculateTip 所在的那一行。請注意,其中也包含 MainActivity 類別。
at com.example.tiptime.MainActivity.calculateTip(MainActivity.kt:22)
  1. 仔細查看該行,您可以看到在程式碼中進行呼叫的確切位置,即 MainActivity.kt 中的第 22 行 (如果您採用不同的方式輸入程式碼,可能會是另一個數字)。該行會將 String 轉換為 Double,並將結果指派給 cost 變數。
val cost = stringInTextField.toDouble()
  1. 查看 Kotlin 說明文件,瞭解適用於 StringtoDouble() 方法。此方法稱為 String.toDouble()
  2. 頁面會顯示「Exceptions: NumberFormatException - if the string is not a valid representation of a number」。

「例外狀況」是系統表示發生問題的方式。在這種情況下,問題在於 toDouble() 無法將空白的 String 轉換為 Double。雖然 EditText 具有 inputType=numberDecimal,但您仍可輸入 toDouble() 無法處理的值 (例如空字串)。

瞭解空值

在空字串或不代表有效十進位數字的字串上呼叫 toDouble() 時,將無法運作。幸好,Kotlin 也提供名為 toDoubleOrNull() 的方法,可用於處理這些問題。如果可以,系統會傳回十進位數字;如果發生問題,系統會傳回 null

「空值」是代表「無任何值」的特殊值。它與值為 0.0Double 或不帶任何字元的空白 String ("") 不同。Null 表示無任何值,沒有 DoubleString。許多方法會預期一個值,並且可能不知道如何處理 null,而這會導致應用程式當機,因此 Kotlin 會嘗試限制使用 null 的位置。您將在日後的課程中詳細瞭解。

應用程式會檢查 toDoubleOrNull() 是否傳回 null,然後視情況採取不同的行動,以免應用程式當機。

  1. calculateTip() 中,變更宣告 cost 變數的那一行,呼叫 toDoubleOrNull() 而非呼叫 toDouble()
val cost = stringInTextField.toDoubleOrNull()
  1. 在此行後面加入陳述式,檢查 cost 是否為 null,如果是,系統會從該方法傳回。return 指令表示在不執行其餘指令的情況下結束方法。如果系統必須傳回一個值,您應使用包含運算式的 return 指令來指定該值。
if (cost == null) {
    return
}
  1. 再次執行應用程式。
  2. 如果「Cost of Service」欄位中沒有任何文字,請輕觸「Calculate」。這次,應用程式並未當機!做得好 — 您已找到並修正錯誤!

處理其他情況

並非所有錯誤都會導致應用程式當機,但結果有時可能會讓使用者感到困惑。

以下是您必須考量的其他情況。如果使用者執行以下操作,會發生什麼事:

  1. 輸入服務費用的有效金額
  2. 輕觸「Calculate」以計算小費
  3. 刪除服務費用
  4. 再次輕觸「Calculate」

第一次,系統會按預期計算並顯示小費。第二次,由於您剛剛新增的檢查,calculateTip() 方法會提早傳回結果,但應用程式仍會顯示先前的小費金額。這可能會讓使用者感到困惑,因此建議新增一些程式碼,以便在出現問題時清除小費金額。

  1. 如要確認發生這個問題,請輸入有效的服務費用,然後輕觸「Calculate」(計算),接著刪除文字後再次輕觸「Calculate」(計算)。系統仍會顯示第一次的小費值。
  2. 在剛剛新增的 if 中,在 return 陳述式之前新增一行,用於將 tipResulttext 屬性設為空字串。
if (cost == null) {
    binding.tipResult.text = ""
    return
}

這樣,系統會在從 calculateTip() 傳回結果之前清除小費金額。

  1. 再次執行應用程式,然後嘗試處理上述情況。再次輕觸「Calculate」時,第一次的小費值應該會消失。

恭喜!您已建立可正常運作的 Android 小費計算機應用程式,並處理一些極端情況!

6. 採用完善的程式設計做法

小費計算機現在可以正常運作,但您可以採用完善的程式設計做法,讓程式碼更臻完美,日後更容易使用。

  1. 開啟 MainActivity.kt (依序點選「app」>「java」>「com.example.tiptime」>「MainActivity」)。
  2. 看看 calculateTip() 方法的開頭,您會發現它帶有一條波浪形的灰色底線。

3737ebab72be9a5b.png

  1. 將游標懸停在 calculateTip() 上方時,系統會顯示訊息「Function ‘calculateTip' could be private」,下方還會顯示建議「Make ‘calculateTip' ‘private」6205e927b4c14cf3.png

回想先前的程式碼研究室,private 代表系統只會向屬於該類別 (在這種情況下為 MainActivity 類別) 中的程式碼顯示方法或變數。MainActivity 之外的程式碼沒有理由呼叫 calculateTip(),因此您可以放心地將該程式碼設為 private

  1. 選擇「Make ‘calculateTip' ‘private」,或者在 fun calculateTip() 前新增 private 關鍵字。calculateTip() 下方的灰色線條消失。

檢查程式碼

灰色線條很細,容易被忽略。您可以瀏覽整個檔案,尋找更多灰色線條,但其實有更簡便的方式能確保您找到所有建議。

  1. MainActivity.kt 仍然開啟時,從選單中選擇「Analyze」>「Inspect Code…」。系統隨即顯示名為「Specify Inspection Scope」的對話方塊。1d2c6f8415e96231.png
  2. 選擇以 File 開頭的檔案,然後按「OK」。這樣可將檢查範圍限制為僅包含 MainActivity.kt
  3. 畫面底部會出現「Inspection Results」視窗。
  4. 按一下「Kotlin」旁邊的灰色三角形圖示,然後再按一下「Style issues」旁邊的同一個圖示,直到系統顯示兩則訊息。第一項訊息的內容為「Class member can have ‘private' visibility」e40a6876f939c0d9.png
  5. 按一下灰色三角形圖示,直到畫面上顯示「Property 'binding' is private」訊息,然後按一下該訊息。Android Studio 會顯示 MainActivity 中的部分程式碼,並醒目顯示 binding 變數。8d9d7b5fc7ac5332.png
  6. 按「Make ‘binding' ‘private'」按鈕。Android Studio 會從「Inspection Results」中移除問題。
  7. 如果查看程式碼中的 binding,您會發現 Android Studio 在宣告之前新增關鍵字 private
private lateinit var binding: ActivityMainBinding
  1. 按一下結果中的灰色三角形圖示,直到系統顯示「Variable declaration could be inlined」訊息。Android Studio 會再次顯示部分程式碼,但這次會醒目顯示 selectedId 變數。781017cbcada1194.png
  2. 查看程式碼時,您會發現 selectedId 僅使用過兩次:第一次是在指派 tipOptions.checkedRadioButtonId 值的醒目顯示的那一行,第二次是 when 中的下一行。
  3. 按「Inline variable」按鈕。Android Studio 會將 when 運算式中的 selectedId 替換為先前指派給該行的值。它會完全移除不再需要的上一行!
val tipPercentage = when (binding.tipOptions.checkedRadioButtonId) {
    R.id.option_twenty_percent -> 0.20
    R.id.option_eighteen_percent -> 0.18
    else -> 0.15
}

這項功能很酷!程式碼減少一行,變數減少一個。

移除不必要的變數

Android Studio 不會再顯示檢查作業的結果。不過,仔細查看程式碼時,您會發現與您剛剛變更的模式類似的模式:roundUp 變數指派給某一行,在下一行中使用,在其他任何位置都不使用。

  1. 從指派 roundUp 的行複製 = 右側的運算式。
val roundUp = binding.roundUpSwitch.isChecked
  1. 將下一行中的 roundUp 替換為您剛剛複製的運算式 binding.roundUpSwitch.isChecked
if (binding.roundUpSwitch.isChecked) {
    tip = kotlin.math.ceil(tip)
}
  1. 刪除包含 roundUp 的行,因為不再需要使用該行。

您執行的操作與 Android Studio 幫助您使用 selectedId 變數執行的操作相同。同樣地,程式碼減少一行,變數減少一個。這些異動雖然微小,但可以讓程式碼更簡潔易懂。

(選用) 移除重複的程式碼

應用程式運作正常後,您可以尋找其他機會來清理程式碼,讓程式碼更簡潔。舉例來說,如果您沒有在服務費用中輸入值,應用程式會將 tipResult 更新為空字串 ""。如果顯示值,請使用 NumberFormat 來設定格式。可以在應用程式中的其他位置套用此功能,例如顯示小費 0.0 而非空字串。

如要減少非常類似的程式碼重複出現,您可以將這兩行程式碼擷取至其專屬函式中。這個輔助函式可以將 Double 形式的小費金額做為輸入內容,設定其格式,並更新畫面上的 tipResult TextView

  1. 識別 MainActivity.kt 中的重複程式碼。這些程式碼行可以在 calculateTip() 函式中使用多次,一次用於 0.0 情況,一次用於一般情況。
val formattedTip = NumberFormat.getCurrencyInstance().format(0.0)
binding.tipResult.text = getString(R.string.tip_amount, formattedTip)
  1. 將重複的程式碼移到專屬函式。對程式碼進行的一項變更是採用小費參數,以便在多個位置使用程式碼。
private fun displayTip(tip : Double) {
   val formattedTip = NumberFormat.getCurrencyInstance().format(tip)
   binding.tipResult.text = getString(R.string.tip_amount, formattedTip)
}
  1. 請更新 calculateTip() 函式以使用 displayTip() 輔助函式,並檢查 0.0

MainActivity.kt

private fun calculateTip() {
    ...

        // If the cost is null or 0, then display 0 tip and exit this function early.
        if (cost == null || cost == 0.0) {
            displayTip(0.0)
            return
        }

    ...
    if (binding.roundUpSwitch.isChecked) {
        tip = kotlin.math.ceil(tip)
    }

    // Display the formatted tip value on screen
    displayTip(tip)
}

附註

雖然應用程式現在可正常運作,但尚未準備發布正式版。您必須進行更多測試。並且,您必須新增視覺設計,並遵循質感設計指南。以下程式碼研究室也會說明如何變更應用程式主題和應用程式圖示。

7. 解決方案程式碼

本程式碼研究室的解決方案程式碼如下。

966018df4a149822.png

MainActivity.kt

(請注意第一行:如果您的套件名稱與 com.example.tiptime 不同,請替換套件名稱)

package com.example.tiptime

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.tiptime.databinding.ActivityMainBinding
import java.text.NumberFormat

class MainActivity : AppCompatActivity() {

   private lateinit var binding: ActivityMainBinding

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)

       binding = ActivityMainBinding.inflate(layoutInflater)
       setContentView(binding.root)

       binding.calculateButton.setOnClickListener { calculateTip() }
   }

   private fun calculateTip() {
       val stringInTextField = binding.costOfService.text.toString()
       val cost = stringInTextField.toDoubleOrNull()
       if (cost == null) {
           binding.tipResult.text = ""
           return
       }

       val tipPercentage = when (binding.tipOptions.checkedRadioButtonId) {
           R.id.option_twenty_percent -> 0.20
           R.id.option_eighteen_percent -> 0.18
           else -> 0.15
       }

       var tip = tipPercentage * cost
       if (binding.roundUpSwitch.isChecked) {
           tip = kotlin.math.ceil(tip)
       }

       val formattedTip = NumberFormat.getCurrencyInstance().format(tip)
       binding.tipResult.text = getString(R.string.tip_amount, formattedTip)
   }
}

修改 strings.xml

<string name="tip_amount">Tip Amount: %s</string>

修改 activity_main.xml

...

<TextView
   android:id="@+id/tip_result"
   ...
   tools:text="Tip Amount: $10" />

...

修改應用程式模組的 build.gradle

android {
    ...

    buildFeatures {
        viewBinding = true
    }
    ...
}

8. 摘要

  • 檢視區塊繫結可讓您更輕鬆地編寫與應用程式中 UI 元素互動的程式碼。
  • Kotlin 中的 Double 資料類型可以儲存十進位數字。
  • 使用 RadioGroupcheckedRadioButtonId 屬性找出已選取的 RadioButton
  • 使用 NumberFormat.getCurrencyInstance() 取得格式設定工具,用於將數字格式設定為貨幣。
  • 您可以使用字串參數 (例如 %s) 來建立動態字串,這些字串仍然可以輕鬆翻譯成其他語言。
  • 測試非常重要!
  • 您可以使用 Android Studio 中的 Logcat 來排解應用程式當機等問題。
  • 堆疊追蹤會顯示呼叫的方法清單。如果程式碼會產生例外狀況,這項功能就能派上用場。
  • 例外狀況表示系統存在程式碼沒有預期到的問題。
  • Null 表示「無任何值」。
  • 並非所有程式碼都可以處理 null 值,因此請謹慎使用。
  • 使用「Analyze」>「Inspect Code」取得改善程式碼的建議。

9. 更多程式碼研究室可用於改善 UI

您讓小費計算機正常運作,太棒了!您會發現,仍有許多方式可以改善 UI,讓應用程式看起來更精美。如果您有興趣,請查閱這些額外的程式碼研究室,進一步瞭解如何變更應用程式主題和應用程式圖示,以及如何遵循 Tip Time 應用程式的質感設計指南中的最佳做法!

10. 瞭解詳情

11. 自行練習

  • 使用上一個練習中的烹飪單位轉換器應用程式,針對邏輯和計算項目新增程式碼,在毫升和液體盎司等單位之間來回轉換。