【正社員】還元率83%【フリーランス】マージン一律5万円で案件をご紹介させていただきます。 詳細はこちら

【Android】<Kotlin>プロパティの遅延初期化とは?lateinitやby lazyの使い方の基本を完全マスターする

【Android】<Kotlin>プロパティの遅延初期化とは?lateinitやby lazyの使い方の基本を完全マスターする
すだ

みなさまこんにちは〜!
メモリアインクのすだです。

今回は、Kotlinでの開発における遅延初期化について、その必要性と使い方をわかりやすく解説していきます。

この記事を読んでわかること…
・遅延初期化とは?
・lateinit var の使い方
・by lazy の使い方

目次

環境

  • Kotlin (ver 1.9.0)
  • Android Studio (Giraffe | 2022.3.1 Patch 3)

遅延初期化とは?

遅延初期化とは、「プロパティの初期化を、宣言と同時にではなく、必要なタイミングまで“あとに回す”こと」です。

Kotlinはnull安全を重視している言語なので、
「値がまだ設定されてない状態で使われる」ことを避けたい。

そのためvalvar のプロパティは、
以下のように宣言と同時に初期化(= 最初の値を代入すること)が必要です▼

val name: String = "Taro" // OK
val age: Int              // エラー!初期化されていない

しかし、「このプロパティは、最初でなくあとで初期化したい」というタイミングがいくつかあります。
たとえば以下のような場合です▼

  • ActivityのonCreate() 内で初期化したい
    • (→画面が生成されたタイミングでしか使えない値や部品。findViewById() で取得する UI 部品など)
  • 最初に使われたときにだけ計算したい
    • (→宣言時に初期化した場合、常に起動時に処理が実行されてしまうため、
      初回アクセス時にだけ重たい処理を行って、あとは結果を使い回したい。)
  • 初期値が非nullだけど今すぐ決められない

→このようなときに使えるのが「遅延初期化」です。

Kotlinにおける遅延初期化の方法

遅延初期化を行う方法は、以下の2種類があります。

lateinitあとから代入することを前提にした var(再代入可能な変数) に使える
by lazy初めて使われたときに初期化する val(再代入不可能な変数) に使える

lateinit

lateinitは以下の特徴があります。

  • var(変更可能)のみ使える
  • 非null型(String など)のみ使える
  • Int, Boolean など プリミティブ型には使えない
  • 初期化前にアクセスするとエラーになる(UninitializedPropertyAccessException)
class MainActivity : AppCompatActivity() {

    // lateinit を使って後から初期化するTextView
    private lateinit var textView: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // ここで初期化
        textView = findViewById(R.id.myTextView)

        // 初期化したTextViewを使う
        textView.text = "こんにちは!"
    }
}

このように、
nullにしたくないけど、あとで初期化したい」といったようなプロパティの宣言に使えるのがlateinitです。

isInitializedで初期化済みか調べる

Kotlinでは、lateinit で宣言されたプロパティがすでに初期化されているかどうかを判定するために、isInitializedを使用することができます。

if (::プロパティ名.isInitialized) {
    // 初期化済みの場合の処理
}

by lazy

by lazyは以下の特徴があります。

  • val に使う(読み取り専用)
  • 初めて使われたときだけ初期化される
  • それ以降はキャッシュされる(再計算されない)
  • スレッドセーフ(複数スレッドでも安全)
class MainActivity : AppCompatActivity() {

    // 初回アクセス時にだけ初期化されるユーザー情報
    private val currentUser: User by lazy {
        User(id = 1, name = "Hanako")
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // この時点で初めて currentUser にアクセス → lazyが実行される
        println("ようこそ ${currentUser.name} さん!")

        // もう一度アクセスしても、再計算されずキャッシュされた値が使われる
        greetUser()
    }

    private fun greetUser() {
        Toast.makeText(this, "こんにちは、${currentUser.name} さん", Toast.LENGTH_SHORT).show()
    }
}

このように、
アクセスされるまでは何もしないけど、初回アクセス時にだけ処理が行われて、以降は使い回される」といったようなプロパティの宣言に使えるのがlateinitです。

実践的な実装例

シンプルなAdapterとリスナー注入パターン

たとえば、リストの中のアイテムをタップしたときに「Activity側で処理したい」とします。
そのとき、「Adapter内でリスナーを用意してあとからActivity側から渡す」ことで、
画面によってクリック動作を変えたいときに便利になります。

Adapter:

class MyAdapter(private val items: List<String>) : RecyclerView.Adapter<MyViewHolder>() {

    private lateinit var listener: OnItemClickListener

    // 外部からリスナーをセットする関数
    fun setOnItemClickListener(itemClickListener: OnItemClickListener) {
        listener = itemClickListener
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(android.R.layout.simple_list_item_1, parent, false)
        return MyViewHolder(view)
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        val item = items[position]
        holder.textView.text = item

        holder.itemView.setOnClickListener {
            if (::listener.isInitialized) {
                listener.onItemClick(item)
            }
        }
    }

    override fun getItemCount(): Int = items.size
}

interface OnItemClickListener {
    fun onItemClick(item: String)
}

呼び出し側:

val adapter = MyAdapter(listOf("A", "B", "C"))

adapter.setOnItemClickListener(object : OnItemClickListener {
    override fun onItemClick(item: String) {
        Toast.makeText(this@MainActivity, "$item がクリックされました", Toast.LENGTH_SHORT).show()
    }
})

recyclerView.adapter = adapter

ViewModelの初期化

ViewModel は画面が再生成されるたびに必要だが、初回アクセス時にだけ作ればOKなので、
by lazy の使用が適しています。

class MainActivity : AppCompatActivity() {

    // ViewModel を遅延初期化(初回アクセス時に1回だけ生成)
    private val viewModel by lazy {
        ViewModelProvider(this)[MainViewModel::class.java]
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // ↓ 初回アクセス時にViewModelが初期化される
        viewModel.loadData()

        viewModel.message.observe(this) { message ->
            Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
        }
    }
}

まとめ

おつかれさまでした。いかがでしたでしょうか!

Kotlinの遅延初期化は、「初期化の自由度を高める」「nullを使わず安全に扱う」という目的に非常に役立ちます。
UIのライフサイクルや非同期処理と組み合わせて、状況に合った方法を選びましょう。

すだ

技術者としてのキャリアパスを次のレベルへと進めたい皆様、
未経験からIT・Webエンジニアを目指すなら【ユニゾンキャリア】
を通じて、
自分の市場価値をさらに向上させてみませんか?

それではまた次回の記事でお会いしましょう!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

弊社テックブログをご愛読いただきありがとうございます。
当テックブログを運用している株式会社メモリアインクは、
【正社員】還元率83%
【フリーランス】マージン一律5万円で案件のご紹介
と、エンジニアの皆様に分かりやすい形で稼げる仕組みを構築し提供させていただいております。

コメント

コメントする

目次