KotlinでCustom Viewを作成するときのコンストラクタの書き方

KotlinでCustom Viewを作成するときのコンストラクタの書き方がよく分からなかったので、これを期にKotlinのコンストラクタとCustom Viewについて調べてみました。

Javaに慣れているエンジニアからすると、Kotlinのコンストラクタの書き方は若干とっつきにくい印象が個人的にはあります。
そのため、まずはJavaライクなKotlinのクラスをKotlinライクに書き換えることでKotlinのコンストラクタの書き方を理解し、その上でCustom Viewの書き方について整理したいと思います。


JavaライクなKotlinのクラス

ここからは、idnameを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つのコンストラクタを持っています。

  1. constructor(context: Context)
  2. constructor(context: Context, attrs: AttributeSet?)
  3. constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
  4. constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int)


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)


なお、@JvmOverloadsN番目の引数から1つずつデフォルト値を持つ引数を減らしてN-1個、N-2個、…、N-M+1個の引数を持つ関数を生成するため、引数の順序に注意する必要があります。
例えば、第3引数のdefStyleAttrを第4引数にすると、3.のコンストラクconstructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int)が生成されないため、エラーの原因となります。


以上となります。何番煎じか分からないくらいありふれた内容ですが、個人的には整理できた気がします。
理解不足で割愛したxmlstyleattr周りについては、別途調べたいと思います。