このトピックでは、Android 向け開発に役立つ Kotlin 言語の特徴に焦点を当てて説明します。
フラグメントを処理する
次のセクションでは、Fragment
の例を使って、Kotlin の優れた機能をいくつか紹介します。
継承
Kotlin では class
キーワードでクラスを宣言できます。次の例では、LoginFragment
が Fragment
のサブクラスになっています。サブクラスとその親の間で :
演算子を使用して継承を示すことができます。
class LoginFragment : Fragment()
このクラス宣言では、LoginFragment
が、そのスーパークラスのコンストラクタである Fragment
を呼び出す役割を担っています。
LoginFragment
内では、多くのライフサイクル コールバックをオーバーライドして Fragment
の状態変化に対応できます。関数をオーバーライドするには、次の例に示すように override
キーワードを使います。
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.login_fragment, container, false)
}
親クラスの関数を参照するには、次の例に示すように super
キーワードを使います。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
null 値許容と初期化
前の例では、オーバーライドされたメソッドのパラメータの一部に、末尾に疑問符 ?
の付いた型が含まれています。これは、これらのパラメータに渡される引数が null になり得ることを示しています。null 値許容は安全に取り扱ってください。
Kotlin では、オブジェクト宣言時にオブジェクトのプロパティを初期化する必要があります。つまり、クラスのインスタンスを取得すると、そのアクセス可能なプロパティをすぐに参照できるようになります。ただし、Fragment
の View
オブジェクトは、Fragment#onCreateView
を呼び出すまではインフレーションの準備ができていません。そのため、View
のプロパティ初期化を遅らせる手段が必要になります。
lateinit
を利用すると、プロパティ初期化が遅延します。lateinit
を使用する際は、プロパティをできるだけ早く初期化する必要があります。
lateinit
を使って onViewCreated
の View
オブジェクトを割り当てる例を次に示します。
class LoginFragment : Fragment() {
private lateinit var usernameEditText: EditText
private lateinit var passwordEditText: EditText
private lateinit var loginButton: Button
private lateinit var statusTextView: TextView
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
usernameEditText = view.findViewById(R.id.username_edit_text)
passwordEditText = view.findViewById(R.id.password_edit_text)
loginButton = view.findViewById(R.id.login_button)
statusTextView = view.findViewById(R.id.status_text_view)
}
...
}
SAM 変換
OnClickListener
インターフェースを実装することにより、Android 内クリック イベントをリッスンできます。Button
オブジェクトには、OnClickListener
の実装を受け取る setOnClickListener()
関数が含まれています。
OnClickListener
には単一抽象メソッド onClick()
があり、それを実装する必要があります。setOnClickListener()
は引数として常に OnClickListener
を取り、OnClickListener
は常に同じ単一抽象メソッドを持っているので、この実装は Kotlin で匿名関数を使用して表現できます。このプロセスを、単一抽象メソッド変換または SAM 変換と呼びます。
SAM 変換を使用すると、コードが大幅に簡素化されます。次の例は、SAM 変換を使用して Button
に OnClickListener
を実装する方法を示しています。
loginButton.setOnClickListener {
val authSuccessful: Boolean = viewModel.authenticate(
usernameEditText.text.toString(),
passwordEditText.text.toString()
)
if (authSuccessful) {
// Navigate to next screen
} else {
statusTextView.text = requireContext().getString(R.string.auth_failed)
}
}
setOnClickListener()
に渡される匿名関数内のコードは、ユーザーが loginButton
をクリックすると実行されます。
コンパニオン オブジェクト
コンパニオン オブジェクト
リンクされた変数または関数を定義するメカニズムが
概念的には型に紐付けられますが、特定のオブジェクトには関連付けられません。コンパニオン オブジェクトは、変数とメソッドに Java の static
キーワードを使用するのに似ています。
次の例では、TAG
は String
の定数です。LoginFragment
のインスタンスそれぞれに固有の String
インスタンスを設ける必要はないため、コンパニオン オブジェクトで定義するとよいでしょう。
class LoginFragment : Fragment() {
...
companion object {
private const val TAG = "LoginFragment"
}
}
ファイルのトップレベルで TAG
を定義することもできますが、ファイルには、やはりトップレベルで定義されている変数、関数、クラスが多数含まれている可能性があります。コンパニオン オブジェクトは、クラスの特定のインスタンスを参照せずに変数、関数、クラスの定義を結び付けるのに役立ちます。
プロパティ委任
プロパティを初期化するときには、Fragment
内の ViewModel
へのアクセスなど、Android の一般的パターンを繰り返し使用する場合があります。過剰な重複コードを回避するために、Kotlin のプロパティ委任構文を使用できます。
private val viewModel: LoginViewModel by viewModels()
プロパティ委任では、アプリ全体で再利用できる共通の実装が提供されます。Android KTX はいくつかのプロパティ委任を提供します。たとえば、viewModels
は、現在の Fragment
を対象範囲とする ViewModel
を取得します。
プロパティ委任ではリフレクションが使用され、それによってパフォーマンスのオーバーヘッドが多少増加します。それと引き換えに、開発時間を短縮する簡潔な構文が得られます。
null 値許容
Kotlin は、アプリ全体で型の安全性を維持する厳格な null 値許容ルールを提供しています。Kotlin では、デフォルトではオブジェクトへの参照に null 値を含めることはできません。変数に null 値を割り当てるには、ベースタイプの末尾に ?
を追加して null 値許容変数型を宣言する必要があります。
たとえば、Kotlin では次の式は不正となります。name
が String
型であり、null 値が許容されていません。
val name: String = null
null 値を許可するには、次の例に示すように、null 値を許容する String
型、String?
を使用する必要があります。
val name: String? = null
相互運用性
Kotlin の厳格なルールによって、コードの安全性と簡潔性が向上します。これらのルールは、アプリのクラッシュの原因となる NullPointerException
の可能性を低下させます。また、コード内で行う必要のある null チェックの回数が減ります。
ほとんどの Android API が Java プログラミング言語で記述されているため、Android アプリを記述するときは、多くの場合、Kotlin 以外のコードも呼び出す必要があります。
Java と Kotlin の動作が異なる場合は、null 値許容が重要になります。null 値許容構文に関しては、Java のほうが規則が緩やかです。
たとえば、Account
クラスには、name
と呼ばれる String
プロパティなどの、少数のプロパティがあります。Java では、Kotlin のような null 値許容ルールはありません。その代わり、オプションの null 値許容アノテーションを利用して、null 値割り当ての可否を明示的に宣言します。
Android フレームワークは主に Java で記述されているため、null 値許容アノテーションなしで API を呼び出すときには、このようなシナリオに遭遇する可能性があります。
プラットフォーム型
Java Account
クラスで定義されているアノテーションなしの name
メンバーを、Kotlin を使用して参照すると、Kotlin で String
が String
または String?
にマッピングされるかどうかが、コンパイラにはわかりません。この曖昧さは、プラットフォーム型 String!
によって表現されます。
Kotlin コンパイラにとって、String!
には特別な意味はありません。String!
は String
または String?
を表現でき、コンパイラでどちらの型の値も割り当てられます。ただし、型を String
と表現して null 値を割り当てると NullPointerException
がスローされるおそれがあるので、注意してください。
この問題に対処するには、Java でコードを記述するときには常に null 値許容アノテーションを使用する必要があります。これらのアノテーションは、Java と Kotlin 双方のデベロッパーにとって役立ちます。
たとえば、Account
クラスを Java で定義すると以下のようになります。
public class Account implements Parcelable {
public final String name;
public final String type;
private final @Nullable String accessId;
...
}
メンバー変数の 1 つである accessId
に @Nullable
のアノテーションが付加され、null 値を保持できることを示しています。その場合、Kotlin は accessId
を String?
として扱います。
変数に null を許可しないことを示すには、@NonNull
アノテーションを使います。
public class Account implements Parcelable {
public final @NonNull String name;
...
}
このシナリオでは、name
は Kotlin における null 値非許容 String
と見なされます。
null 値許容アノテーションは、新しいすべての Android API および既存の多くの Android API に含まれています。Kotlin と Java 双方のデベロッパーのサポートを強化するために、多くの Java ライブラリに null 値許容アノテーションが追加されています。
null 値許容の処理
Java 型が不明な場合は、null 値許容とみなす必要があります。たとえば、Account
クラスの name
メンバーにはアノテーションがないので、null 値許容 String
とみなす必要があります。
値の先頭または末尾に空白が含まれないように name
をカットする場合、Kotlin の trim
関数を使用できます。String?
を安全にカットするには数種類の方法があります。その 1 つに、非 null アサーション演算子、!!
を使用する方法があります。次の例をご覧ください。
val account = Account("name", "type")
val accountName = account.name!!.trim()
!!
演算子は、その左側にあるものすべてを非 null として扱います。そのため、この場合、name
は非 null の String
として扱われます。左側にある式の結果が null の場合、アプリは NullPointerException
をスローします。
この演算子は簡潔ですが、NullPointerException
のインスタンスがコードに再導入される可能性があるので、慎重に使用する必要があります。
より安全な選択肢として、safe-call 演算子、?.
を使用する方法があります。次の例をご覧ください。
val account = Account("name", "type")
val accountName = account.name?.trim()
safe-call 演算子を使用すると、name
が非 null の場合、name?.trim()
の結果は、行頭または末尾に空白のない名前値になります。name
が null の場合、name?.trim()
の結果は null
になります。つまり、このステートメントを実行するときにアプリから NullPointerException
がスローされることはあり得ません。
safe-call 演算子を使用すると、NullPointerException
の可能性はなくなりますが、その次のステートメントに null 値が渡されます。代わりに、次の例に示すように、Elvis 演算子(?:
)を使用して null ケースを直ちに処理することが可能です。
val account = Account("name", "type")
val accountName = account.name?.trim() ?: "Default name"
Elvis 演算子の左側の式の結果が null の場合、右側の値が accountName
に割り当てられます。このように null になるような場合でもデフォルト値を提供できるので、この手法は有用です。
また、次の例のように、Elvis 演算子を使用して早期に関数から戻ることもできます。
fun validateAccount(account: Account?) {
val accountName = account?.name?.trim() ?: "Default name"
// account cannot be null beyond this point
account ?: return
...
}
Android API の変更
Android API と Kotlin の相性は、ますます向上しています。AppCompatActivity
や Fragment
など、Android の一般的 API の多くに null 値許容アノテーションが含まれ、Fragment#getContext
のような特定の呼び出しには、Kotlin とさらに相性のよい代替方法が存在します。
たとえば、Fragment
の Context
へのアクセスは、ほとんど null になりません。それは、Fragment
での呼び出しの大半が、Fragment
が Activity
(Context
のサブクラス)に関連付けられている間に行われるためです。とは言え、Fragment#getContext
から常に非 null 値が返されるとは限りません。Fragment
が Activity
に関連付けられていないシナリオもあるからです。このように、Fragment#getContext
の戻り値の型は null 値許容となります。
Fragment#getContext
から返される Context
は null 値許容(かつ @Nullable というアノテーションが付いている)なので、Kotlin コードでは Context?
として扱う必要があります。
つまり、プロパティおよび関数にアクセスする前に、上記の演算子のいずれかを適用して null 値許容に対処することになります。これらのシナリオの一部では、このように便利な代替 API が Android に含まれています。
たとえば、Context
が null になるような場合に呼び出しを行うと、Fragment#requireContext
は非 null Context
を返し、IllegalStateException
をスローします。このようにして、結果となる Context
を、safe-call 演算子または回避策を必要とせずに非 null として扱うことができます。
プロパティの初期化
Kotlin のプロパティはデフォルトでは初期化されません。含まれるクラスが初期化されるときに、プロパティを初期化する必要があります。
プロパティを初期化するには数種類の方法があります。次の例は、クラス宣言で値を割り当てることで index
変数を初期化する方法を示しています。
class LoginFragment : Fragment() {
val index: Int = 12
}
この初期化はイニシャライザ ブロックでも定義できます。
class LoginFragment : Fragment() {
val index: Int
init {
index = 12
}
}
上記の例では、index
は、LoginFragment
が構築されるときに初期化されます。
ただし、オブジェクト構築中に初期化できないプロパティもあります。たとえば、Fragment
内から View
を参照する場合、最初にレイアウトをインフレーションする必要があります。Fragment
構築時にはインフレーションは行われません。そうではなく、Fragment#onCreateView
呼び出し時にインフレーションされます。
このシナリオへの対処策として、次の例に示すように、ビューを null 値許容として宣言し、できるだけ早く初期化する方法があります。
class LoginFragment : Fragment() {
private var statusTextView: TextView? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
statusTextView = view.findViewById(R.id.status_text_view)
statusTextView?.setText(R.string.auth_failed)
}
}
この方法は想定どおりに機能しますが、View
を参照するときには常にその null 値許容を管理する必要が生じます。さらに良いソリューションとして、次の例に示すように、lateinit
を使用して View
を初期化する方法があります。
class LoginFragment : Fragment() {
private lateinit var statusTextView: TextView
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
statusTextView = view.findViewById(R.id.status_text_view)
statusTextView.setText(R.string.auth_failed)
}
}
lateinit
キーワードにより、オブジェクト構築時にプロパティ初期化を回避できます。初期化前にプロパティが参照されると、Kotlin は UninitializedPropertyAccessException
をスローします。そのため、プロパティの初期化はできるだけ早く行ってください。