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

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

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

# JavaライクなKotlinのクラス
ここからは、`id`と`name`をimmutableなプロパティとして持つ`User`クラスを例に話を進めていきます。
この`User`クラスをJavaで書くと以下のようになります。初期化処理はコンストラクタで行います。

```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でコンストラクタを宣言するときは<b>`constructor`キーワード</b>を利用します。

```kotlin
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. プライマリコンストラクタの利用
先ほどのクラスのように<b>クラス本体に宣言する</b>コンストラクタを、Kotlinでは<b>セカンダリコンストラクタ</b>と呼びます。

Kotlinではもう一つ、<b>プライマリコンストラクタ</b>と呼ばれるコンストラクタがあります。
プライマリコンストラクタは<b>クラスヘッダに宣言します</b>。
また、プライマリコンストラクタを利用するときの初期化処理は<b>`init`ブロック</b>で行います。

先ほどのクラスをプライマリコンストラクタで書き換えると以下のようになります。

```kotlin
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`キーワードは、<b>修飾子やアノテーションを付与する必要がない場合</b>は省略できます。
なお、セカンダリコンストラクタでは`constructor`キーワードは省略できません。

先ほどのクラスの`constructor`キーワードを省略すると以下のようになります。

```kotlin
class User(id: Long, name: String) {
  private val id: Long
  private val name: String

  init {
    this.id = id
    this.name = name
  }

}
```

#### 3. プロパティの初期化処理のワンライン化
プロパティの初期化処理はプライマリコンストラクタの中で宣言とともにワンラインで記述できます。
なお、セカンダリコンストラクタではプロパティの初期化処理はワンライン化できません。

先ほどのクラスのプロパティの初期化処理をワンライン化すると以下のようになります。

```kotlin
class User(private val id: Long, private val name: String) {

  init {
  }

}
```

#### 4. `init`ブロックとクラスのブレースの省略
初期化処理がない場合、`init`ブロックは省略できます。また、クラス本体がない場合、クラスのブレースも省略できます。

先ほどのクラスの`init`ブロックとクラスのブレースを省略すると以下のようになります。

```kotlin
class User(private val id: Long, private val name: String)
```

最終的にここまで簡潔になりました。

# プロパティのデフォルト値の設定
クラスのプロパティにデフォルト値を設定するとき、Javaではコンストラクタをオーバーロードすることが一般的だと思います。
自クラスのコンストラクタを呼び出すには`this`キーワードを利用します。例えば、`name`のデフォルト値を`Unknown`にする場合、Javaでは以下のようになります。

```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では、プライマリコンストラクタの中でデフォルト値を設定できます。

```kotlin
class User(private val id: Long, private val name = "Unknown")
```

このため、Kotlinではデフォルト値を設定するためにコンストラクタをオーバーロードする必要がないので、セカンダリコンストラクタを宣言する機会はあまりありません。

# KotlinでCustom Viewを作成するときのコンストラクタの書き方
上記の通り、Kotlinではセカンダリコンストラクタを宣言する機会はあまりありませんが、`View`クラスを拡張したいわゆる`CustomView`クラスを作成する場合はセカンダリコンストラクタが必要になります。
[公式ドキュメント](https://developer.android.com/reference/android/view/View.html#public-constructors_1)によると、`View`クラスは以下の4つのコンストラクタを持っています。

1. `constructor(context: Context)`
    - <b>kt/javaファイルから</b>インスタンスを生成する際に呼び出される
2. `constructor(context: Context, attrs: AttributeSet?)`
    - xmlファイルからインスタンスを生成する際に呼び出される
3. `constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int)`
    - xmlファイルからインスタンスを生成する際に呼び出される
4. `constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int)`
    - xmlファイルからインスタンスを生成する際に呼び出される
    - <b>APIレベル21から追加</b>

2.-4.のコンストラクタがどう呼び出し分けられるのかは正直理解しきれていないのですが(すみません🙇‍♂️)、`CustomView`クラスを通常のコンポーネントと同様にkt/java/xmlファイルから扱うためには、`CustomView`クラスから`View`クラスの各コンストラクタを呼び出す必要があります。

スーパークラスのコンストラクタを呼び出すには<b>`super`キーワード</b>を利用します。具体的には以下のようになります。

```kotlin
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)
}
```

上記であれば先ほどのデフォルト値を利用して以下のように書くことができそうなのですが、<b>Java側からは全てのプロパティを含んだコンストラクタのみが見える状態</b>となるため、上手くいきません。

```kotlin
// ※これは上手くいかない例です。
class CustomView(
  context: Context,
  attrs: AttributeSet? = null,
  defStyleAttr: Int = 0,
  defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes)
```

このため、Kotlinでは`@JvmOverloads`というアノテーションが用意されています。
N個の引数とM個のデフォルト値を持つ関数に`@JvmOverloads`を付与すると、M個の関数がオーバーロードされます。
<b>コンストラクタ＝インスタンス生成時に呼び出される関数</b>なので、`@JvmOverloads`を利用して書き換えると以下のようになります。

```kotlin
class CustomView
@JvmOverloads
constructor(
  context: Context,
  attrs: AttributeSet? = null,
  defStyleAttr: Int = 0,
  defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes)
```

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