うきっとラボ~中卒から始めるプログラミング~

中卒のポンコツ太郎が立派なプログラマになるまでの道のり

【Kotlin】プロパティをJavaのフィールドっぽく使う方法

はじめに

この間サラっと勉強したKotlinを使いAndroid Studioで簡単なアプリを作成していたところ、プロパティの初期化・アクセス方法で軽く詰んでしまったため、解決策とそれまでの試行錯誤も含めてまとめてみました。

この記事でいう「Javaのフィールドっぽく」とは、主に初期化を遅らせるやり方(遅延初期化)の話になります。

それではレッツゴーー!


やりたかったこと

TextViewオブジェクトを、
複数のライフサイクルメソッド(onCreate()、onPause()など)からアクセスできる変数として定義したい

↓こんなイメージ↓

class MainActivity : AppCompatActivity() {

    //TextViewプロパティ

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

        /*
        ボタンを押すとEditText内容がTextViewに反映される処理
         */

    }

    override fun onPause() {
        //TextView内容を保存する処理(SharedPreferences)
        super.onPause()
    }

    override fun onResume() {
        super.onResume()
        //保存した内容をTextViewに反映させる処理
    }
}

試行①:Javaのフィールドのように、プロパティとして定義してみた

とりあえず知ってる書き方で。

class MainActivity : AppCompatActivity() {

    private val tv: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        //~省略~

【結果】
「Property must be initialized or be abstract」というコンパイルエラー。

どうやらKotlinでは必ず何かしらの値で初期化されていないとダメらしいです。
Null Safetyは伊達じゃねぇな!!(´;ω;`)


試行②:それなら初期化してやる

嫌な予感。。。

class MainActivity : AppCompatActivity() {

    private val tv: TextView = findViewById(R.id.tv_Output)

    override fun onCreate(savedInstanceState: Bundle?) {
        //~省略~

【結果】
NullPointerException」がでた。

うん。なんとなく予想はしてた。setContentView()してないのにR.id.〇〇はできないよなぁ、、、


試行③:Nullable型で宣言して「!!」演算子でNon-Null型に変換

今度は!?どう!?

class MainActivity : AppCompatActivity() {

    private var tv: TextView? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        //~省略~

        tv = findViewById(R.id.tv_Output)

        //~省略~
        bt.setOnClickListener{
            tv!!.text = et.text.toString()
        }

    }
    ~省略~

【結果】
一応できたけど不細工&毎回書くのが面倒

変換されるのはその場限りなので、参照するたびに書くのはスタイリッシュじゃない(??)
というより、求めてたのはこれじゃない!


てことで、詰みました( ;∀;)。。。

Twitterにて愚痴(笑)を吐いていたところ、とあるAndroidプログラマの方から助言をいただきました!(ありがとうございます<(_ _)>)
おかげさまでスパっと解決しましたので、以降はその方法についてまとめてみました!

これらの方法は遅延初期化というそうです!

解決策①:「lateinit」修飾子を使う

varの前に”lateinit”を記述します。

class MainActivity : AppCompatActivity() {

    private lateinit var tv: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        //~省略~
        tv = findViewById(R.id.tv_output)

これで他のライフサイクルメソッドから自由にアクセス可能になりました。

lateinit”は
「プロパティーの初期化をコンストラクタより後に遅らせられる機能」
だそうです。

ただし以下のような場合は使用できません
val変数への使用

private lateinit val name: String  //NG
private lateinit var name: String  //OK

Nullable型への使用

private lateinit var name: String?  //NG
private lateinit var name: String  //OK

プリミティブ型への使用

private lateinit var name: Int  //NG
private lateinit var name: String  //OK

今回は
「Non-Null型」
「TextView型」
という条件だったのでひとまずはOKでした
ただ、できればvalとして定義したいところ。。。

参考:
lateinitの行儀の良い使い方 - Takuji->find;


解決策②:「lazy」関数を使う

変数の後に”by lazy”を呼び出して、
引数のラムダ式に初期化を実装します。

class MainActivity : AppCompatActivity() {

    private val tv: TextView by lazy {
        findViewById<TextView>(R.id.tv_Output)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
    //~省略~

この方法の特徴としては以下のものがあげられます

・val変数に対して使える
・プリミティブ型でもOK
・初期化を1度行い、以降は同じ値を返す

この場合、lateinitと違いonCreate内で初期化する必要はありません
最初に呼び出した時点で、lazy関数の引数に渡したラムダ式で初期化されるようです。

委譲プロパティとは「プロパティの実装を外部のクラスに委ねる」ことです。
この場合では、tv変数の実装をLazyインタフェースのSAMであるlazy関数に委ねていることになります。
参考:Lazy - Kotlin Programming Language


大きなメリットとしては、「val変数に対して使うことができる」というところですね。

また参考サイトによると、
”常に同じ値を返す”という性質のため、findViewByIdで取得したViewなどを入れると、textView.text = newValueなどとしても変更が反映されない
とのことだったので、実際にやってみました。

//~省略(上記コードの続き)~
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val et = findViewById<EditText>(R.id.et_Name)
        val bt = findViewById<Button>(R.id.bt_Output)
        //ボタンを押すと変更を反映させる
        bt.setOnClickListener{
            tv.text = et.text.toString()
        }
    }
//~省略~

変更が反映されるか検証

f:id:ukiuki0518:20191113111849j:plain
画質が悪くてすみません。。。
あれ??
しっかり反映されました。なぜだ...?
条件が違っているのか、それとも参考にした記事が書かれた頃から、アップデートか何かで改善されたのかな?。。。
とにかく今のところはOK。もし不具合があったらまた書きますね。

参考:
Kotlinで初期化を遅延する | RE:ENGINES


解決策③:「Delegates.notNull」を使う

変数の後に”by Delegates.notNull()”を呼び出して、
onCreate()内で初期化します。

class MainActivity : AppCompatActivity() {

    private var tv: TextView by Delegates.notNull()

    override fun onCreate(savedInstanceState: Bundle?) {
        //~省略~

        tv = findViewById(R.id.tv_Output)

        //~省略~
    }

この場合はDelegatesのnotNull()関数に委譲しているようです。
(間違っていたらすみません;;)

主な特徴は以下の通りです
・初期化は参照前に行う
・プリミティブ型にも使用可能
・val型には使用できない
・notNullプロパティ毎にオブジェクトが生成されるので多くなると重くなる

参考:
Delegates - Kotlin Programming Language


使い分け

これまで紹介した3つの方法には、同じ「遅延初期化」でも
それぞれ使いどころがあるようですね!φ(..)

・val変数として定義したいとき
→「lazy」を使った方法


・プリミティブ型に対して使用したいとき
→「Delegates.notNull()」を使った方法


・それ以外のとき
→「lateinit」修飾子を使った方法


これで使い分ければ、とりあえずは大丈夫な気がします!

その他の参考リンク:
notNull delegate vs lateinit - Android - Kotlin Discussions
FragmentでKotlinのby lazyを使ってfindViewByIdするとレイアウト反映できない&amp;リークする件 - Qiita
Kotlinアンチパターン