Android で一般的な Kotlin パターンを使用する

このトピックでは、Android 向け開発に役立つ Kotlin 言語の特徴に焦点を当てて説明します。

フラグメントを処理する

次のセクションでは、Fragment の例を使って、Kotlin の優れた機能をいくつか紹介します。

継承

Kotlin では class キーワードでクラスを宣言できます。次の例では、LoginFragmentFragment のサブクラスになっています。サブクラスとその親の間で : 演算子を使用して継承を示すことができます。

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 では、オブジェクト宣言時にオブジェクトのプロパティを初期化する必要があります。つまり、クラスのインスタンスを取得すると、そのアクセス可能なプロパティをすぐに参照できるようになります。ただし、FragmentView オブジェクトは、Fragment#onCreateView を呼び出すまではインフレーションの準備ができていません。そのため、View のプロパティ初期化を遅らせる手段が必要になります。

lateinit を利用すると、プロパティ初期化が遅延します。lateinit を使用する際は、プロパティをできるだけ早く初期化する必要があります。

lateinit を使って onViewCreatedView オブジェクトを割り当てる例を次に示します。

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 変換を使用して ButtonOnClickListener を実装する方法を示しています。

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 キーワードを使用するのに似ています。

次の例では、TAGString の定数です。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 では次の式は不正となります。nameString 型であり、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 で StringString または 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 は accessIdString? として扱います。

変数に 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 の相性は、ますます向上しています。AppCompatActivityFragment など、Android の一般的 API の多くに null 値許容アノテーションが含まれ、Fragment#getContext のような特定の呼び出しには、Kotlin とさらに相性のよい代替方法が存在します。

たとえば、FragmentContext へのアクセスは、ほとんど null になりません。それは、Fragment での呼び出しの大半が、FragmentActivityContext のサブクラス)に関連付けられている間に行われるためです。とは言え、Fragment#getContext から常に非 null 値が返されるとは限りません。FragmentActivity に関連付けられていないシナリオもあるからです。このように、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 をスローします。そのため、プロパティの初期化はできるだけ早く行ってください。