KotlinでCustom Viewを作成するときのコンストラクタの書き方がよく分からなかったので、これを期にKotlinのコンストラクタとCustom Viewについて調べてみました。
Javaに慣れているエンジニアからすると、Kotlinのコンストラクタの書き方は若干とっつきにくい印象が個人的にはあります。
そのため、まずはJavaライクなKotlinのクラスをKotlinライクに書き換えることでKotlinのコンストラクタの書き方を理解し、その上でCustom Viewの書き方について整理したいと思います。
JavaライクなKotlinのクラス
ここからは、id
とname
をimmutableなプロパティとして持つUser
クラスを例に話を進めていきます。
このUser
クラスをJavaで書くと以下のようになります。初期化処理はコンストラクタで行います。
public final class User {
private final long id;
private final String name;
public User(long id, String name) {
this.id = id;
this.name = name;
}
public long getId() { return this.id; }
public String getName() { return this.name; }
}
上記のUser
クラスをJavaライクにKotlinで書くと以下のようになります。
Kotlinでコンストラクタを宣言するときはconstructor
キーワードを利用します。
class User {
private val id: Long
private val name: String
constructor(id: Long, name: String) {
this.id = id
this.name = name
}
}
Kotlinライクなクラスへの書き換え
ここからは、上記のJavaライクなKotlinのクラスをKotlinライクに書き換えるためのいくつかの方法について説明します。
1. プライマリコンストラクタの利用
先ほどのクラスのようにクラス本体に宣言するコンストラクタを、Kotlinではセカンダリコンストラクタと呼びます。
Kotlinではもう一つ、プライマリコンストラクタと呼ばれるコンストラクタがあります。
プライマリコンストラクタはクラスヘッダに宣言します。
また、プライマリコンストラクタを利用するときの初期化処理はinit
ブロックで行います。
先ほどのクラスをプライマリコンストラクタで書き換えると以下のようになります。
class User constructor(id: Long, name: String) {
private val id: Long
private val name: String
init {
this.id = id
this.name = name
}
}
2. constructor
キーワードの省略
プライマリコンストラクタのconstructor
キーワードは、修飾子やアノテーションを付与する必要がない場合は省略できます。
なお、セカンダリコンストラクタではconstructor
キーワードは省略できません。
先ほどのクラスのconstructor
キーワードを省略すると以下のようになります。
class User(id: Long, name: String) {
private val id: Long
private val name: String
init {
this.id = id
this.name = name
}
}
3. プロパティの初期化処理のワンライン化
プロパティの初期化処理はプライマリコンストラクタの中で宣言とともにワンラインで記述できます。
なお、セカンダリコンストラクタではプロパティの初期化処理はワンライン化できません。
先ほどのクラスのプロパティの初期化処理をワンライン化すると以下のようになります。
class User(private val id: Long, private val name: String) {
init {
}
}
4. init
ブロックとクラスのブレースの省略
初期化処理がない場合、init
ブロックは省略できます。また、クラス本体がない場合、クラスのブレースも省略できます。
先ほどのクラスのinit
ブロックとクラスのブレースを省略すると以下のようになります。
class User(private val id: Long, private val name: String)
最終的にここまで簡潔になりました。
プロパティのデフォルト値の設定
クラスのプロパティにデフォルト値を設定するとき、Javaではコンストラクタをオーバーロードすることが一般的だと思います。
自クラスのコンストラクタを呼び出すにはthis
キーワードを利用します。例えば、name
のデフォルト値をUnknown
にする場合、Javaでは以下のようになります。
public final class User {
private final long id;
private final String name;
public User(long id) {
this(id, "Unknown");
}
public User(long id, String name) {
this.id = id;
this.name = name;
}
// Getterは省略。
}
Kotlinでは、プライマリコンストラクタの中でデフォルト値を設定できます。
class User(private val id: Long, private val name = "Unknown")
このため、Kotlinではデフォルト値を設定するためにコンストラクタをオーバーロードする必要がないので、セカンダリコンストラクタを宣言する機会はあまりありません。
KotlinでCustom Viewを作成するときのコンストラクタの書き方
上記の通り、Kotlinではセカンダリコンストラクタを宣言する機会はあまりありませんが、View
クラスを拡張したいわゆるCustomView
クラスを作成する場合はセカンダリコンストラクタが必要になります。
公式ドキュメントによると、View
クラスは以下の4つのコンストラクタを持っています。
-
constructor(context: Context)
- kt/javaファイルからインスタンスを生成する際に呼び出される
-
constructor(context: Context, attrs: AttributeSet?)
- xmlファイルからインスタンスを生成する際に呼び出される
-
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
- xmlファイルからインスタンスを生成する際に呼び出される
-
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int)
- xmlファイルからインスタンスを生成する際に呼び出される
- APIレベル21から追加
2.-4.のコンストラクタがどう呼び出し分けられるのかは正直理解しきれていないのですが(すみません🙇♂️)、CustomView
クラスを通常のコンポーネントと同様にkt/java/xmlファイルから扱うためには、CustomView
クラスからView
クラスの各コンストラクタを呼び出す必要があります。
スーパークラスのコンストラクタを呼び出すにはsuper
キーワードを利用します。具体的には以下のようになります。
class CustomView : View {
// 1.
constructor(context: Context) : super(context)
// 2.
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
// 3.
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
// 4.
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
}
上記であれば先ほどのデフォルト値を利用して以下のように書くことができそうなのですが、Java側からは全てのプロパティを含んだコンストラクタのみが見える状態となるため、上手くいきません。
// ※これは上手くいかない例です。
class CustomView(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes)
このため、Kotlinでは@JvmOverloads
というアノテーションが用意されています。
N個の引数とM個のデフォルト値を持つ関数に@JvmOverloads
を付与すると、M個の関数がオーバーロードされます。
コンストラクタ=インスタンス生成時に呼び出される関数なので、@JvmOverloads
を利用して書き換えると以下のようになります。
class CustomView
@JvmOverloads
constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes)
なお、@JvmOverloads
はN番目の引数から1つずつデフォルト値を持つ引数を減らしてN-1個、N-2個、…、N-M+1個の引数を持つ関数を生成するため、引数の順序に注意する必要があります。
例えば、第3引数のdefStyleAttr
を第4引数にすると、3.のコンストラクタconstructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
が生成されないため、エラーの原因となります。