チップを計算する

1. 始める前に

この Codelab では、チップ計算ツール用のコードを記述し、前の Codelab(Android 用の XML レイアウトを作成する)で作成した UI で使用します。

前提条件

学習内容

  • Android アプリの基本構造。
  • 値を UI からコードに読み込んで操作する方法。
  • findViewById() の代わりにビュー バインディングを使用して、ビューを操作するコードを簡単に記述する方法。
  • Kotlin で Double データ型を使用して 10 進数を扱う方法。
  • 数値を通貨として書式設定する方法。
  • 文字列パラメータを使用して文字列を動的に作成する方法。
  • Android Studio の Logcat を使用して、アプリ内の問題を見つける方法。

作成するアプリの概要

  • 機能的な [Calculate] ボタンを備えたチップ計算アプリ。

必要なもの

  • Android Studio の最新の安定版がインストールされているパソコン。
  • チップ計算ツールのレイアウトを格納する Tip Time アプリのスターター コード。

2. スターター アプリの概要

最後の CodelabTip Time アプリには、チップ計算ツールに必要な UI がすべて用意されていますが、チップを計算するためのコードは含まれていません。[Calculate] ボタンもありますが、まだ動作しません。[Cost of Service] EditText では、ユーザーがサービス料金を入力できます。RadioButtons のリストを使用すると、ユーザーがチップの割合を選択できます。Switch を使用すると、ユーザーがチップの端数を切り上げるかどうかを選択できます。チップ金額は TextView に表示され、最後に [計算] Button で、他のフィールドからデータを取得してチップ金額を計算するようにアプリに指示します。そこで、この Codelab を使用します。

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 は変更の内容を特定し、アプリの再作成に必要な手順を実施します。また、エミュレータまたは物理デバイスにアプリをインストールして、その実行を制御します。

アプリの作成に関連する他のフォルダやファイルもありますが、これらの場所は主に、この Codelab と次の Codelab での作業に使用します。

3. ビュー バインディング

チップを計算するには、コードですべての UI 要素にアクセスして、ユーザーからの入力を読み取る必要があります。前の Codelab で説明したとおり、コードが View に対するメソッドを呼び出したり、その属性にアクセスしたりする前に、コードで ButtonTextView などの View への参照を調べる必要があります。Android フレームワークには、findViewById() というメソッドが用意されており、View の ID を指定してその参照を返すという、まさにここで必要な操作を行えます。このアプローチは効果的ですが、アプリにビューを追加すると UI が複雑になるため、findViewById() の使用が難しくなる可能性があります。

また、Android にはビュー バインディングと呼ばれる機能もあります。最初は少し作業が必要ですが、ビュー バインディングを使用すると、UI のビューに対してメソッドを呼び出すのがはるかに簡単かつ迅速になります。Gradle でアプリのビュー バインディングを有効にして、コードの一部を変更する必要があります。

ビュー バインディングを有効にする

  1. アプリの build.gradle ファイルを開きます([Gradle Scripts] > [build.gradle (Module: Tip_Time.app)])。
  2. android セクションに、次の行を追加します。
buildFeatures {
    viewBinding = true
}
  1. Gradle files has been changed after last project sync」というメッセージが表示されます。
  2. [Sync Now] をクリックします。

349d99c67c2f40f1.png

しばらくすると、Android Studio ウィンドウの下部に「Gradle sync finished」というメッセージが表示されます。必要に応じて build.gradle ファイルを閉じてもかまいません。

バインディング オブジェクトを初期化する

前の Codelab では、MainActivity クラスに onCreate() メソッドが表示されていました。これは、アプリが起動して、MainActivity が初期化されたときに最初に呼び出されるメソッドです。アプリ内の View ごとに findViewById() を呼び出すのではなく、バインディング オブジェクトの作成と初期化を 1 回だけ行います。

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. 次の行は、activity_main.xml レイアウトの Views へのアクセスに使用する binding オブジェクトを初期化します。
binding = ActivityMainBinding.inflate(layoutInflater)
  1. アクティビティのコンテンツ ビューを設定します。レイアウトのリソース ID(R.layout.activity_main)を渡す代わりに、アプリのビュー階層のルート(binding.root)を指定します。
setContentView(binding.root)

親ビューと子ビューのセクションで説明したとおり、このルートはこれらのビューの両方に接続します。

アプリで View への参照が必要な場合は、findViewById() を呼び出す代わりに binding オブジェクトから取得できます。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 に保存されますが、計算に使用するには数値である必要があります。他の Codelab で Int 型について説明したとおり、Int には整数しか含めることができません。アプリで 10 進数を使用するには、Int ではなく、Double というデータ型を使用します。詳しくは、ドキュメントの Kotlin の数値データ型をご覧ください。Kotlin には、StringDouble に変換するメソッド(toDouble() と呼ばれます)が用意されています。

  1. まず、サービス料金のテキストを取得します。calculateTip() メソッドで、Cost of Service EditText のテキスト属性を取得し、stringInTextField という変数に代入します。UI 要素には binding オブジェクトを使用してアクセスできます。また、キャメルケースではリソース ID 名に基づいて UI 要素を参照できます。
val stringInTextField = binding.costOfService.text

末尾に .text があります。最初の部分の binding.costOfService は、サービス料金の UI 要素を参照します。末尾に .text を追加すると、その結果(EditText オブジェクト)が取得され、そこから text プロパティが取得されます。これはチェーンと呼ばれ、Kotlin ではごく一般的なパターンです。

  1. 次に、テキストを 10 進数に変換します。stringInTextField に対して toDouble() を呼び出し、cost という変数に格納します。
val cost = stringInTextField.toDouble()

ただし、これは機能しません。toDouble()String に対して呼び出す必要があります。EditTexttext 属性は変更可能なテキストを表すため、Editable になっていることがわかります。幸い、この型に対して toString() を呼び出すことで、EditableString に変換できます。

  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

R.id.option_twenty_percentR.id.option_eighteen_percentR.id.fifteen_percent のうち、どの RadioButton が選択されたかがわかりましたが、それに対応する割合が必要です。一連の 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() で、追加した他のコードの後で、tipPercentagecost を掛けて tip という変数に代入します。
var tip = tipPercentage * cost

val ではなく var を使用していることに注意してください。これは、ユーザーが端数切り上げを選択した場合に、端数が切り上げられることで値が変わる可能性があるためです。

Switch 要素については、isChecked 属性をチェックすると、スイッチが「オン」かどうかを確認できます。

  1. 端数切り上げ切り替えの isChecked 属性を roundUp という変数に代入します。
val roundUp = binding.roundUpSwitch.isChecked

端数切り上げとは、小数部分を最も近い整数になるように増減することを指しますが、この例では単に端数を切り上げるか、上限を見つけることが目的です。この場合には ceil() 関数を使用できます。これと同じ名前の関数がいくつかありますが、ここで指定する関数は kotlin.math で定義されています。import ステートメントを追加することもできますが、この場合、kotlin.math.ceil() を使用して必要な動作を Android Studio に指示する方が簡単です。

32c29f73a3f20f93.png

使用したい数学関数が複数ある場合は、import ステートメントを追加すると簡単です。

  1. roundUp が true の場合、チップの上限を tip 変数に代入する if ステートメントを追加します。
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 デベロッパー向けドキュメントの NumberFormat をご覧ください。

  1. calculateTip() で、他のコードの後に NumberFormat.getCurrencyInstance() を呼び出します。
NumberFormat.getCurrencyInstance()

これにより、数値を通貨として書式設定するために使用できる数値書式設定ツールが提供されます。

  1. 数値書式設定ツールを使用して、format() メソッドへの呼び出しを tip でチェーン接続し、その結果を formattedTip という変数に代入します。
val formattedTip = NumberFormat.getCurrencyInstance().format(tip)
  1. NumberFormat は赤色で表示されます。これは、使用する NumberFormat のバージョンを Android Studio が自動で判断できないためです。
  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.ktcalculateTip() メソッドに戻り、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: $10 に設定された tools:text 属性の行を追加します。
tools:text="Tip Amount: $10"

これは単なるプレースホルダであるため、文字列をリソースに抽出する必要はありません。また、アプリの実行時には表示されません。

  1. Layout Editor にツールのテキストが表示されます。
  2. アプリを実行します。料金の値を入力して金額を選択し、[Calculate] ボタンをクリックします。

42fd6cd5e24ca433.png

以上で完了です。チップ金額が正確にわからない場合は、このセクションの手順 1 に戻って、必要なコードの変更をすべて完了していることを確認してください。

5. テストとデバッグ

テストをいくつか行って、アプリが意図したとおりに動作することを確認しました。次に他のテストを行います。

ここでは、calculateTip() メソッドで情報がアプリに伝達される仕組み、各手順で発生し得る問題について考えてみましょう。

たとえば、次の行があるとします。

val cost = stringInTextField.toDouble()

stringInTextField が数値を表していない場合やユーザーがテキストを入力せず、stringInTextField が空だった場合について考えます。

  1. エミュレータでアプリを実行します。なお、[Run] > [Run 'app'] ではなく [Run] > [Debug 'app'] を使用します。
  2. [Calculate] をタップするときは、料金、チップ金額、端数切り上げの有無をさまざまな組み合わせで使用し、それぞれのケースで期待どおりの結果が得られるか確認してください。
  3. 次に、[Cost of Service] フィールドのすべてのテキストを削除して [Calculate] をタップします。プログラムがクラッシュしました。

クラッシュをデバッグする

バグに対処するにはまず、状況を確認します。Android Studio では、システムで発生した現象をログに記録しています。このログを使用して、問題を特定できます。

  1. Android Studio の下部にある [Logcat] ボタンをクリックするか、メニューから [View] > [Tool Windows] > [Logcat] を選択します。

1b68ee5190018c8a.png

  1. [Logcat] ウィンドウが Android Studio の下部に表示され、その中に見慣れないテキストがいくつか表示されます。22139575476ae9d.png

テキストはスタック トレースであり、クラッシュが発生したときに呼び出されたメソッドのリストです。

  1. FATAL EXCEPTION というテキストを含む行が見つかるまで、[Logcat] テキストを上にスクロールします。
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 行目)を確認できます(別のコードを入力した場合は、別の番号が表示される場合があります)。この行は、StringDouble に変換し、その結果を cost 変数に代入します。
val cost = stringInTextField.toDouble()
  1. String で動作する toDouble() メソッドに関する Kotlin ドキュメントをご覧ください。このメソッドは String.toDouble() と呼ばれます。
  2. そのページでは「例外: NumberFormatException - if the string is not a valid representation of a number」と記載されています。

例外とは、問題が存在することをシステム側から表した言葉です。この場合、問題は toDouble() が空の StringDouble に変換できなかったことです。EditTextinputType=numberDecimal がある場合でも、toDouble() が処理できない値(空の文字列など)を入力することができます。

null の詳細

空の文字列や、有効な 10 進数ではない文字列に対して toDouble() を呼び出すことはできません。幸い、Kotlin では toDoubleOrNull() というメソッドを使用して、これらの問題に対処できます。可能な場合は 10 進数を返します。問題が発生した場合は null を返します。

Null は「値なし」を意味する特別な値です。これは、値が 0.0 である Double や、値が 0 文字 "" である空の String などとは異なります。Null は、値がない、Double がない、または String がないことを意味します。多くのメソッドは値を想定しており、null の処理方法を知らなければ動作を停止する場合があります。このため、Kotlin は null が使用される場所制限しようとします。これについては後のレッスンで詳しく説明します。

アプリは nulltoDoubleOrNull() から返されるかどうかを確認し、返された場合は、アプリがクラッシュしないように別の方法で処理することができます。

  1. calculateTip() で、cost 変数を宣言する行を変更して、toDouble() を呼び出す代わりに toDoubleOrNull() を呼び出すようにします。
val cost = stringInTextField.toDoubleOrNull()
  1. costnull であるかどうかを確認して、null である場合はメソッドから戻るためのステートメントを、その行の後に追加します。return 手順は、残りの手順を実行せずにメソッドを終了することを意味します。メソッドが値を返す必要がある場合は、式を含む return 手順でその値を指定します。
if (cost == null) {
    return
}
  1. アプリを再度実行します。
  2. [Cost of Service] フィールドにテキストを入力せずに、[Calculate] をタップします。今度はアプリがクラッシュしませんでした。これでバグの検出と修正が完了しました。

別のケースを処理する

バグが発生しても常にアプリがクラッシュするわけではなく、このことはユーザーの混乱を招く原因になります。

次に別のケースを検討してみましょう。ユーザーが以下の操作を行うとどうなるかを考えてみます。

  1. 料金の有効な値を入力する
  2. [Calculate] をタップしてチップを計算する
  3. 料金を削除する
  4. もう一度 [Calculate] をタップする

1 回目の計算ではチップが計算され、想定どおり表示されます。2 回目の計算では、先ほど追加したチェックのために、calculateTip() メソッドが早期に返されますが、アプリ側では以前のチップの金額が表示されたままになっています。これにより、ユーザーの混乱を招くことがないように、問題がある場合にチップ金額を消去するためのコードを追加することをおすすめします。

  1. この問題が発生しているか確認するには、有効な料金を入力して [Calculate] をタップし、テキストを削除して [Calculate] をもう一度タップします。最初のチップ値は引き続き表示されます。
  2. 先ほど追加した if の内部で、return ステートメントの前に、tipResulttext 属性を空の文字列に設定するための行を追加します。
if (cost == null) {
    binding.tipResult.text = ""
    return
}

この操作を行うと、calculateTip() から返される前のチップ金額が消去されます。

  1. アプリを再び実行し、上記のケースを繰り返してみます。2 回目に [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

前の Codelab で説明したとおり、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. 2 つのメッセージが表示されるまで、[Kotlin] の横と、[Style issues] の横にあるグレーの三角形をそれぞれ順にクリックします。最初のメッセージには「Class member can have ‘private' visibility」と表示されます。e40a6876f939c0d9.png
  5. Property ‘binding' could be 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 が 2 回だけ使用されていることがわかります。最初は、ハイライト表示されている行で 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
}

これは良い方法です。コードでは、行と変数の数が 1 つずつ減りました。

不要な変数を削除する

Android Studio では、検査の結果はそれ以上出力されません。ただし、コードを注意深く見ると、変更したばかりのパターンと同様のパターンが表示されます。roundUp 変数は 1 つの行に代入され、次の行で使用されます。それ以外の場所では使用されません。

  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 変数を使用して行う操作と同じ操作を行いました。これでまた、コードの行と変数の数が 1 つずつ減りました。これらは小さな変更ですが、コードを簡潔で読みやすくするのに役立ちます。

(省略可)繰り返しのコードを排除する

アプリが正しく実行されると、コードをクリーンアップしてより簡潔にする他の改善点を探すことができます。たとえば、サービス料金の値を入力していない場合、アプリは、tipResult を空の文字列 "" になるように更新します。値が入力されている場合は、NumberFormat を使用して、この値を書式設定します。この機能は、アプリの他の場所に適用できます。たとえば、空の文字列の代わりに 0.0 のチップを表示できます。

よく似たコードの重複を減らすために、これら 2 行のコードをその独自の関数に抽出できます。このヘルパー関数では、チップ金額を Double として入力して書式設定し、画面の tipResult TextView を更新できます。

  1. MainActivity.kt で重複しているコードを特定します。これらのコードの行は、calculateTip() 関数で複数回使用できます(0.0 のケースと一般的なケースで各 1 回)。
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)
}

アプリは動作するようになりましたが、製品版とするにはまた不十分です。さらにテストを行う必要があります。また、視覚的な改善を施し、マテリアル デザイン ガイドラインに準拠させる必要があります。次の Codelab では、アプリのテーマとアプリアイコンを変更する方法も学習します。

7. 解答コード

この Codelab の解答コードを以下に示します。

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 データ型は 10 進数を保存できます。
  • RadioGroupcheckedRadioButtonId 属性を使用して、どの RadioButton が選択されているか確認します。
  • NumberFormat.getCurrencyInstance() を使用して、書式設定ツールを使用して数値を通貨として書式設定します。
  • %s などの文字列パラメータを使用して、他の言語に簡単に翻訳できる動的な文字列を作成できます。
  • テストは重要です。
  • Android Studio の Logcat を使用すると、アプリのクラッシュなどの問題のトラブルシューティングを行うことができます。
  • スタック トレースには、呼び出されたメソッドのリストが表示されます。この機能は、コードで例外が発生する場合に便利です。
  • 例外は、コードが想定していなかった問題を表します。
  • Null は「値なし」を意味します。
  • null の値を処理できないコードもあるため、使用するときは注意してください。
  • [Analyze] > [Inspect Code] を使用して、コード改善のための提案を確認します。

9. UI を改善するためのその他の Codelab

これでチップ計算ツールを使用できるようになりました。UI を改善して、アプリをさらに洗練されたものにするめの方法がまだあります。アプリテーマやアプリアイコンを変更する方法、Tip Time アプリのマテリアル デザイン ガイドラインでベスト プラクティスを実践する方法については、以下の追加の Codelab をご覧ください。

10. 関連リンク

11. 自習用練習問題

  • 前の例の調理用の単位変換アプリを使用し、ロジックと計算用のコードを追加して、ミリリットルなどの単位を液量オンスとの間での変換を行います。