1. 始める前に
この Codelab では、チップ計算ツール用のコードを記述し、前の Codelab(Android 用の XML レイアウトを作成する)で作成した UI で使用します。
前提条件
- Android 用の XML レイアウトを作成する Codelab のコード。
- エミュレータまたはデバイスで Android Studio から Android アプリを実行する方法。
学習内容
- Android アプリの基本構造。
- 値を UI からコードに読み込んで操作する方法。
findViewById()
の代わりにビュー バインディングを使用して、ビューを操作するコードを簡単に記述する方法。- Kotlin で
Double
データ型を使用して 10 進数を扱う方法。 - 数値を通貨として書式設定する方法。
- 文字列パラメータを使用して文字列を動的に作成する方法。
- Android Studio の Logcat を使用して、アプリ内の問題を見つける方法。
作成するアプリの概要
- 機能的な [Calculate] ボタンを備えたチップ計算アプリ。
必要なもの
- Android Studio の最新の安定版がインストールされているパソコン。
- チップ計算ツールのレイアウトを格納する Tip Time アプリのスターター コード。
2. スターター アプリの概要
最後の Codelab の Tip Time アプリには、チップ計算ツールに必要な UI がすべて用意されていますが、チップを計算するためのコードは含まれていません。[Calculate] ボタンもありますが、まだ動作しません。[Cost of Service] EditText
では、ユーザーがサービス料金を入力できます。RadioButtons
のリストを使用すると、ユーザーがチップの割合を選択できます。Switch
を使用すると、ユーザーがチップの端数を切り上げるかどうかを選択できます。チップ金額は TextView
に表示され、最後に [計算] Button
で、他のフィールドからデータを取得してチップ金額を計算するようにアプリに指示します。そこで、この Codelab を使用します。
アプリ プロジェクトの構造
IDE のアプリ プロジェクトは、Kotlin コード、XML レイアウト、文字列や画像などのリソースなど、さまざまな要素から構成されています。アプリに変更を加える前に、操作方法を確認しておくことが重要です。
- Android Studio で [Tip Time] プロジェクトを開きます。
- [Project] ウィンドウが表示されない場合は、Android Studio の左側にある [Project] タブを選択します。
- プルダウンから [Android] ビューを選択します(まだ選択されていない場合)。
- 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
に対するメソッドを呼び出したり、その属性にアクセスしたりする前に、コードで Button
や TextView
などの View
への参照を調べる必要があります。Android フレームワークには、findViewById()
というメソッドが用意されており、View
の ID を指定してその参照を返すという、まさにここで必要な操作を行えます。このアプローチは効果的ですが、アプリにビューを追加すると UI が複雑になるため、findViewById()
の使用が難しくなる可能性があります。
また、Android にはビュー バインディングと呼ばれる機能もあります。最初は少し作業が必要ですが、ビュー バインディングを使用すると、UI のビューに対してメソッドを呼び出すのがはるかに簡単かつ迅速になります。Gradle でアプリのビュー バインディングを有効にして、コードの一部を変更する必要があります。
ビュー バインディングを有効にする
- アプリの
build.gradle
ファイルを開きます([Gradle Scripts] > [build.gradle (Module: Tip_Time.app)])。 android
セクションに、次の行を追加します。
buildFeatures { viewBinding = true }
- 「Gradle files has been changed after last project sync」というメッセージが表示されます。
- [Sync Now] をクリックします。
しばらくすると、Android Studio ウィンドウの下部に「Gradle sync finished」というメッセージが表示されます。必要に応じて build.gradle
ファイルを閉じてもかまいません。
バインディング オブジェクトを初期化する
前の Codelab では、MainActivity
クラスに onCreate()
メソッドが表示されていました。これは、アプリが起動して、MainActivity
が初期化されたときに最初に呼び出されるメソッドです。アプリ内の View
ごとに findViewById()
を呼び出すのではなく、バインディング オブジェクトの作成と初期化を 1 回だけ行います。
MainActivity.kt
を開きます([app] > [java] > [com.example.tiptime] > [MainActivity])。MainActivity
クラスの既存のコードをすべて次のコードに置き換えて、ビュー バインディングを使用するようにMainActivity
を設定します。
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
- 次の行で、バインディング オブジェクトのクラス内の最上位変数を宣言します。この変数は
MainActivity
クラスの複数のメソッドで使用されるため、このレベルで定義します。
lateinit var binding: ActivityMainBinding
lateinit
キーワードは新しい機能です。コードが変数を使用する前に初期化することを保証します。先に初期化しない場合、アプリがクラッシュします。
- 次の行は、
activity_main.xml
レイアウトのViews
へのアクセスに使用するbinding
オブジェクトを初期化します。
binding = ActivityMainBinding.inflate(layoutInflater)
- アクティビティのコンテンツ ビューを設定します。レイアウトのリソース 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] ボタンの動作を指定します。
onCreate()
のMainActivity.kt
で、setContentView()
を呼び出した後に、[Calculate] ボタンにクリック リスナーを設定し、calculateTip()
を呼び出すようにします。
binding.calculateButton.setOnClickListener{ calculateTip() }
- 引き続き
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 には、String
を Double
に変換するメソッド(toDouble()
と呼ばれます)が用意されています。
- まず、サービス料金のテキストを取得します。
calculateTip()
メソッドで、Cost of ServiceEditText
のテキスト属性を取得し、stringInTextField
という変数に代入します。UI 要素にはbinding
オブジェクトを使用してアクセスできます。また、キャメルケースではリソース ID 名に基づいて UI 要素を参照できます。
val stringInTextField = binding.costOfService.text
末尾に .text
があります。最初の部分の binding.costOfService
は、サービス料金の UI 要素を参照します。末尾に .text
を追加すると、その結果(EditText
オブジェクト)が取得され、そこから text
プロパティが取得されます。これはチェーンと呼ばれ、Kotlin ではごく一般的なパターンです。
- 次に、テキストを 10 進数に変換します。
stringInTextField
に対してtoDouble()
を呼び出し、cost
という変数に格納します。
val cost = stringInTextField.toDouble()
ただし、これは機能しません。toDouble()
を String
に対して呼び出す必要があります。EditText
の text
属性は変更可能なテキストを表すため、Editable
になっていることがわかります。幸い、この型に対して toString()
を呼び出すことで、Editable
を String
に変換できます。
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()
}
チップの割合を取得する
ここまでで、サービス料金を取得できました。今度は、ユーザーが RadioButtons
の RadioGroup
から選択したチップの割合が必要です。
calculateTip()
で、tipOptions
RadioGroup
のcheckedRadioButtonId
属性を取得して、selectedId
という変数に代入します。
val selectedId = binding.tipOptions.checkedRadioButtonId
R.id.option_twenty_percent
、R.id.option_eighteen_percent
、R.id.fifteen_percent
のうち、どの RadioButton
が選択されたかがわかりましたが、それに対応する割合が必要です。一連の if/else
ステートメントを記述することもできますが、when
式を使用する方がはるかに簡単です。
- チップの割合を取得するには、次の行を追加します。
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
}
}
チップを計算して、その端数を切り上げる
これでサービス料金とチップの割合がわかったので、チップの計算は簡単に行えます。チップはコストにチップの割合を掛けて計算します(チップ = サービス料金 * チップの割合)。必要に応じて、端数を切り上げることができます。
calculateTip()
で、追加した他のコードの後で、tipPercentage
にcost
を掛けてtip
という変数に代入します。
var tip = tipPercentage * cost
val
ではなく var
を使用していることに注意してください。これは、ユーザーが端数切り上げを選択した場合に、端数が切り上げられることで値が変わる可能性があるためです。
Switch
要素については、isChecked
属性をチェックすると、スイッチが「オン」かどうかを確認できます。
- 端数切り上げ切り替えの
isChecked
属性をroundUp
という変数に代入します。
val roundUp = binding.roundUpSwitch.isChecked
端数切り上げとは、小数部分を最も近い整数になるように増減することを指しますが、この例では単に端数を切り上げるか、上限を見つけることが目的です。この場合には ceil()
関数を使用できます。これと同じ名前の関数がいくつかありますが、ここで指定する関数は kotlin.math
で定義されています。import
ステートメントを追加することもできますが、この場合、kotlin.math.ceil()
を使用して必要な動作を Android Studio に指示する方が簡単です。
使用したい数学関数が複数ある場合は、import
ステートメントを追加すると簡単です。
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 をご覧ください。
-
calculateTip()
で、他のコードの後にNumberFormat.getCurrencyInstance()
を呼び出します。
NumberFormat.getCurrencyInstance()
これにより、数値を通貨として書式設定するために使用できる数値書式設定ツールが提供されます。
- 数値書式設定ツールを使用して、
format()
メソッドへの呼び出しをtip
でチェーン接続し、その結果をformattedTip
という変数に代入します。
val formattedTip = NumberFormat.getCurrencyInstance().format(tip)
NumberFormat
は赤色で表示されます。これは、使用するNumberFormat
のバージョンを Android Studio が自動で判断できないためです。- カーソルを
NumberFormat
に合わせ、ポップアップが表示されると [Import] を選択します。 - 使用可能なインポートのリストで、[NumberFormat(java.text)] を選択します。Android Studio では
MainActivity
ファイルの先頭にimport
ステートメントを追加します。NumberFormat
は赤色で表示されません。
チップを表示する
次は、アプリのチップ金額の TextView
要素にチップを表示します。formattedTip
を単に text
属性に代入することもできますが、金額そのものをラベル付けすることをおすすめします。米国では英語で「Tip Amount: $12.34」と表示される場合がありますが、他の言語では文字列の先頭または途中に数字を表示しなければならないことがあります。Android フレームワークでは、このような文字列のためのメカニズム(文字列パラメータと呼ばれます)が用意されているので、アプリを翻訳する際に、必要に応じて数値の表示位置を変更できます。
strings.xml
を開きます([app] > [res] > [values] > [strings.xml])。tip_amount
文字列をTip Amount
からTip Amount: %s
に変更します。
<string name="tip_amount">Tip Amount: %s</string>
%s
は、書式設定された通貨が挿入される場所です。
- 次に、
tipResult
のテキストを設定します。MainActivity.kt
のcalculateTip()
メソッドに戻り、getString(R.string.tip_amount, formattedTip)
を呼び出して、それをチップ結果TextView
のtext
属性に代入します。
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
のプレースホルダを使用すると便利です。
activity_main.xml
を開きます([app] > [res] > [layout] > [activity_main.xml])。tip_result
TextView
を見つけます。android:text
属性を持つ行を削除します。
android:text="@string/tip_amount"
Tip Amount: $10
に設定されたtools:text
属性の行を追加します。
tools:text="Tip Amount: $10"
これは単なるプレースホルダであるため、文字列をリソースに抽出する必要はありません。また、アプリの実行時には表示されません。
- Layout Editor にツールのテキストが表示されます。
- アプリを実行します。料金の値を入力して金額を選択し、[Calculate] ボタンをクリックします。
以上で完了です。チップ金額が正確にわからない場合は、このセクションの手順 1 に戻って、必要なコードの変更をすべて完了していることを確認してください。
5. テストとデバッグ
テストをいくつか行って、アプリが意図したとおりに動作することを確認しました。次に他のテストを行います。
ここでは、calculateTip()
メソッドで情報がアプリに伝達される仕組み、各手順で発生し得る問題について考えてみましょう。
たとえば、次の行があるとします。
val cost = stringInTextField.toDouble()
stringInTextField
が数値を表していない場合やユーザーがテキストを入力せず、stringInTextField
が空だった場合について考えます。
- エミュレータでアプリを実行します。なお、[Run] > [Run 'app'] ではなく [Run] > [Debug 'app'] を使用します。
- [Calculate] をタップするときは、料金、チップ金額、端数切り上げの有無をさまざまな組み合わせで使用し、それぞれのケースで期待どおりの結果が得られるか確認してください。
- 次に、[Cost of Service] フィールドのすべてのテキストを削除して [Calculate] をタップします。プログラムがクラッシュしました。
クラッシュをデバッグする
バグに対処するにはまず、状況を確認します。Android Studio では、システムで発生した現象をログに記録しています。このログを使用して、問題を特定できます。
- Android Studio の下部にある [Logcat] ボタンをクリックするか、メニューから [View] > [Tool Windows] > [Logcat] を選択します。
- [Logcat] ウィンドウが Android Studio の下部に表示され、その中に見慣れないテキストがいくつか表示されます。
テキストはスタック トレースであり、クラッシュが発生したときに呼び出されたメソッドのリストです。
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)
NumberFormatException
を含む行が見つかるまで下へスクロールします。
java.lang.NumberFormatException: empty String
右側に empty String
と表示された行があります。例外の型から、問題が数値形式に関連しているということがわかり、残りの部分から、問題の内容がわかります。型が値を持つ String
である必要がある場合、空の String
が検出されます。
- このまま下にスクロールすると、
parseDouble()
の呼び出しがいくつか表示されます。 - これらの呼び出しの下に、
calculateTip
が含まれる行を見つけます。MainActivity
クラスも含まれています。
at com.example.tiptime.MainActivity.calculateTip(MainActivity.kt:22)
- この行をよく見てみると、コード内で呼び出しが行われた場所(
MainActivity.kt
の 22 行目)を確認できます(別のコードを入力した場合は、別の番号が表示される場合があります)。この行は、String
をDouble
に変換し、その結果をcost
変数に代入します。
val cost = stringInTextField.toDouble()
String
で動作するtoDouble()
メソッドに関する Kotlin ドキュメントをご覧ください。このメソッドはString.toDouble()
と呼ばれます。- そのページでは「例外:
NumberFormatException
- if the string is not a valid representation of a number」と記載されています。
例外とは、問題が存在することをシステム側から表した言葉です。この場合、問題は toDouble()
が空の String
を Double
に変換できなかったことです。EditText
に inputType=numberDecimal
がある場合でも、toDouble()
が処理できない値(空の文字列など)を入力することができます。
null の詳細
空の文字列や、有効な 10 進数ではない文字列に対して toDouble()
を呼び出すことはできません。幸い、Kotlin では toDoubleOrNull()
というメソッドを使用して、これらの問題に対処できます。可能な場合は 10 進数を返します。問題が発生した場合は null
を返します。
Null は「値なし」を意味する特別な値です。これは、値が 0.0
である Double
や、値が 0 文字 ""
である空の String
などとは異なります。Null
は、値がない、Double
がない、または String
がないことを意味します。多くのメソッドは値を想定しており、null
の処理方法を知らなければ動作を停止する場合があります。このため、Kotlin は null
が使用される場所制限しようとします。これについては後のレッスンで詳しく説明します。
アプリは null
が toDoubleOrNull()
から返されるかどうかを確認し、返された場合は、アプリがクラッシュしないように別の方法で処理することができます。
calculateTip()
で、cost
変数を宣言する行を変更して、toDouble()
を呼び出す代わりにtoDoubleOrNull()
を呼び出すようにします。
val cost = stringInTextField.toDoubleOrNull()
cost
がnull
であるかどうかを確認して、null である場合はメソッドから戻るためのステートメントを、その行の後に追加します。return
手順は、残りの手順を実行せずにメソッドを終了することを意味します。メソッドが値を返す必要がある場合は、式を含むreturn
手順でその値を指定します。
if (cost == null) {
return
}
- アプリを再度実行します。
- [Cost of Service] フィールドにテキストを入力せずに、[Calculate] をタップします。今度はアプリがクラッシュしませんでした。これでバグの検出と修正が完了しました。
別のケースを処理する
バグが発生しても常にアプリがクラッシュするわけではなく、このことはユーザーの混乱を招く原因になります。
次に別のケースを検討してみましょう。ユーザーが以下の操作を行うとどうなるかを考えてみます。
- 料金の有効な値を入力する
- [Calculate] をタップしてチップを計算する
- 料金を削除する
- もう一度 [Calculate] をタップする
1 回目の計算ではチップが計算され、想定どおり表示されます。2 回目の計算では、先ほど追加したチェックのために、calculateTip()
メソッドが早期に返されますが、アプリ側では以前のチップの金額が表示されたままになっています。これにより、ユーザーの混乱を招くことがないように、問題がある場合にチップ金額を消去するためのコードを追加することをおすすめします。
- この問題が発生しているか確認するには、有効な料金を入力して [Calculate] をタップし、テキストを削除して [Calculate] をもう一度タップします。最初のチップ値は引き続き表示されます。
- 先ほど追加した
if
の内部で、return
ステートメントの前に、tipResult
のtext
属性を空の文字列に設定するための行を追加します。
if (cost == null) {
binding.tipResult.text = ""
return
}
この操作を行うと、calculateTip()
から返される前のチップ金額が消去されます。
- アプリを再び実行し、上記のケースを繰り返してみます。2 回目に [Calculate] をタップすると、最初のチップ値が非表示になるはずです。
これで、Android 向けの実用的なチップ計算アプリを作成し、特殊なケースを処理できるようになりました。
6. 適切なコーディング慣習を採用する
チップ計算ツールは問題なく機能するようになりましたが、将来的に適切なコーディング手法を取り入れることで、コードを改善し、より使いやすくすることができます。
MainActivity.kt
を開きます([app] > [java] > [com.example.tiptime] > [MainActivity])。calculateTip()
メソッドの先頭を見ると、グレーの波線で下線が引かれているのがわかります。
calculateTip()
にカーソルを合わせると、「Function ‘calculateTip' could be private」というメッセージが表示され、その下に「Make ‘calculateTip' ‘private'」という提案が表示されます。
前の Codelab で説明したとおり、private
は、メソッドまたは変数がそのクラスのコード(この場合は MainActivity
クラス)にのみ表示されることを意味します。MainActivity
以外のコードが calculateTip()
を呼び出す理由はないので、この関数は安全に private
にできます。
- Make 'calculateTip' 'private' を選択するか、
fun calculateTip()
の前にprivate
キーワードを追加します。calculateTip()
の下のグレーの線が消えます。
コードを検査する
グレーの線は非常に細く、見落とされがちです。ファイル全体を調べると、グレーの線が引かれている行がほかにも見つかりますが、すべての提案を確実に見つけるための簡単な方法があります。
MainActivity.kt
を開いたままで、メニューから [Analyze] > [Inspect Code...] を選択します。[Specify Inspection Scope] というダイアログ ボックスが表示されます。- [File] で始まる項目を選択し、[OK] をクリックします。これにより、検査は
MainActivity.kt
のみに制限されます。 - 下部に [Inspection Results] のウィンドウが表示されます。
- 2 つのメッセージが表示されるまで、[Kotlin] の横と、[Style issues] の横にあるグレーの三角形をそれぞれ順にクリックします。最初のメッセージには「Class member can have ‘private' visibility」と表示されます。
- 「Property ‘binding' could be private」というメッセージが表示されるまで、グレーの三角形をクリックします。メッセージが表示されたらそれをクリックします。Android Studio は
MainActivity
にコードの一部を表示し、binding
変数をハイライト表示します。 - [Make 'binding' 'private'] ボタンをクリックします。Android Studio では、[Inspection Results] から問題が削除されます。
- コードの
binding
を確認すると、Android Studio では、宣言の前にprivate
というキーワードが追加されていることがわかります。
private lateinit var binding: ActivityMainBinding
- 「Variable declaration could be inlined」というメッセージが表示されるまで、グレーの三角形をクリックします。Android Studio にコードの一部が再び表示されますが、今回は
selectedId
変数がハイライト表示されます。 - コードを見ると、
selectedId
が 2 回だけ使用されていることがわかります。最初は、ハイライト表示されている行でtipOptions.checkedRadioButtonId
の値が代入されており、次の行ではwhen
に使用されています。 - [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 つの行に代入され、次の行で使用されます。それ以外の場所では使用されません。
roundUp
が代入されている行から、=
の右側の式をコピーします。
val roundUp = binding.roundUpSwitch.isChecked
- 次の行の
roundUp
を、先ほどコピーした式(binding.roundUpSwitch.isChecked
)に置き換えます。
if (binding.roundUpSwitch.isChecked) {
tip = kotlin.math.ceil(tip)
}
roundUp
の行は不要になったので削除します。
Android Studio で selectedId
変数を使用して行う操作と同じ操作を行いました。これでまた、コードの行と変数の数が 1 つずつ減りました。これらは小さな変更ですが、コードを簡潔で読みやすくするのに役立ちます。
(省略可)繰り返しのコードを排除する
アプリが正しく実行されると、コードをクリーンアップしてより簡潔にする他の改善点を探すことができます。たとえば、サービス料金の値を入力していない場合、アプリは、tipResult
を空の文字列 ""
になるように更新します。値が入力されている場合は、NumberFormat
を使用して、この値を書式設定します。この機能は、アプリの他の場所に適用できます。たとえば、空の文字列の代わりに 0.0
のチップを表示できます。
よく似たコードの重複を減らすために、これら 2 行のコードをその独自の関数に抽出できます。このヘルパー関数では、チップ金額を Double
として入力して書式設定し、画面の tipResult
TextView
を更新できます。
MainActivity.kt
で重複しているコードを特定します。これらのコードの行は、calculateTip()
関数で複数回使用できます(0.0
のケースと一般的なケースで各 1 回)。
val formattedTip = NumberFormat.getCurrencyInstance().format(0.0)
binding.tipResult.text = getString(R.string.tip_amount, formattedTip)
- 重複したコードをその独自の関数に移動します。コードに関する変更としては、コードが複数の場所で機能するようにパラメータとしてチップを使用することです。
private fun displayTip(tip : Double) {
val formattedTip = NumberFormat.getCurrencyInstance().format(tip)
binding.tipResult.text = getString(R.string.tip_amount, formattedTip)
}
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 の解答コードを以下に示します。
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 進数を保存できます。 RadioGroup
のcheckedRadioButtonId
属性を使用して、どのRadioButton
が選択されているか確認します。NumberFormat.getCurrencyInstance()
を使用して、書式設定ツールを使用して数値を通貨として書式設定します。%s
などの文字列パラメータを使用して、他の言語に簡単に翻訳できる動的な文字列を作成できます。- テストは重要です。
- Android Studio の Logcat を使用すると、アプリのクラッシュなどの問題のトラブルシューティングを行うことができます。
- スタック トレースには、呼び出されたメソッドのリストが表示されます。この機能は、コードで例外が発生する場合に便利です。
- 例外は、コードが想定していなかった問題を表します。
Null
は「値なし」を意味します。null
の値を処理できないコードもあるため、使用するときは注意してください。- [Analyze] > [Inspect Code] を使用して、コード改善のための提案を確認します。
9. UI を改善するためのその他の Codelab
これでチップ計算ツールを使用できるようになりました。UI を改善して、アプリをさらに洗練されたものにするめの方法がまだあります。アプリテーマやアプリアイコンを変更する方法、Tip Time アプリのマテリアル デザイン ガイドラインでベスト プラクティスを実践する方法については、以下の追加の Codelab をご覧ください。
10. 関連リンク
- Kotlin の
Double
データ型 - Kotlin の数値データ型
- Kotlin の null 安全性
- アプリ マニフェスト
View
バインディングNumberFormat.getCurrencyInstance()
- 文字列パラメータ
- テスト
- Logcat
- スタック トレースの分析
11. 自習用練習問題
- 前の例の調理用の単位変換アプリを使用し、ロジックと計算用のコードを追加して、ミリリットルなどの単位を液量オンスとの間での変換を行います。