文字列中の変数の値を表示するプラグインを作った

プラグインの概要

Kotlinの文字列中の変数が定数値の場合に、その値を文字列中に表示するプラグインです。
↓の例では$tagの表示がtagの値であるtag nameになります。

f:id:scache:20180404100244g:plain  

プラグイン説明

インストール方法

以下の画像のように、IntellijやAndroidStudioの設定から
PluginsBrowse Repositories... をクリックし、ConstStringPlaceHolderで検索することでインストールができます。

f:id:scache:20180404230859p:plain:w300
f:id:scache:20180404230836p:plain:w300

使い方

定数値を表示するには、文字列中の変数にカーソルを移動した後にCollapse(右クリック→FoldingCollapse)をします。

f:id:scache:20180404235225g:plain
f:id:scache:20180404234946g:plain

複数の値を一度に変換する場合は、範囲を選択後にCollapse All(右クリック→FoldingCollapse All)をします。

f:id:scache:20180405001433g:plain

課題

v0.0.1時点では、

  • Javaコードでは使用できない
  • ファイルを開いた時点ではすべてExpandされた状態になる

の課題があるので今後は上記を改善していきたいです。

data classで配列を使う時はequalsをオーバーライドしよう [Kotlin]

data classとは

data classは以下のようにクラスにdataをつけて宣言することで、
コンストラクタで宣言されたプロパティを使ったequals/hashCode/toString関数などを自動生成してくれます。

data class Book(
    val title: String,
    val page: Int
)

val book1 = Book("book", 100)
val book2 = Book("book", 100)
book1.toString()  // => Book(title=book, page=100)
book1 == book2    // => true

toString()で返って来る文字列にプロパティの値が入っていたり、
==(equalsが呼ばれる)を使った比較ができたりします。

data class内で配列を宣言した場合

↓のようにプロパティに配列型を宣言した場合にも、同様にequals()などの関数が自動で生成されます。

data class Book(
    val title: String,
    val author: Array<String>
)
    
val book1 = Book("book", arrayOf("Alice","Bob"))
val book2 = Book("book", arrayOf("Alice","Bob"))
book1.toString() // Book(title=book, author=[Alice, Bob])
book1 == book2   // false

しかし、先ほどとは違いbook1 == book2の結果がfalseになってしまいます。

なぜ配列を使うと比較がうまくできないのか?

配列を使ったdata classをJavaデコンパイルすると、
Array<String>String[]に、data classのequals内の配列の比較はarray1.equals(array2)と同等のコードになります。

Javaの配列型のequals()メソッドは、要素の比較は行わず配列型のインスタンスが同じかどうかを判定するため、配列内の値が等しいだけではtrueになりません。

対処法

equals()メソッドをオーバーライドしてArrays#equlas()を使うことでこの問題を解決することが可能です。

IntellijやAndroidStudioでKotlinPluginを入れている場合(最近のバージョンでは初めから入っています)は、Inspectionが表示され以下のようなequals()/hashCode()を自動生成することが可能になっています。 f:id:scache:20180321002505p:plain

override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (javaClass != other?.javaClass) return false
    other as Book
    if (title != other.title) return false
    if (!Arrays.equals(author, other.author)) return false
    return true
}
override fun hashCode(): Int {
    var result = title.hashCode()
    result = 31 * result + Arrays.hashCode(author)
    return result
}

最後に

data classで配列の比較も良い感じに生成してくれるとありがたいですが、影響がでかいためv2が来るまで変更されることはないと個人的には思います。

Gradle Kotlin DSLを書くための参考になるサイトやページのメモ

Gradle Kotlin DSL

メリット

  • 1回ビルドできる状態にもっていけばあとはコードの補完が効く
  • Groovyを知らなくてもKotlinで書くことができる

デメリット

  • gradle(Groovy版)からKotlinDSLに直すのがつらい
  • あまり使っている人を見ないので思わぬバグを踏む可能性が高い

公式サイト

github.com

Android関係

DroidKaigi2018にいってきた

セッションについて

去年のDroidKaigiでは、MVP, MVVMを使った実装方法、RxJava, Kotlinなどをどう使うか、のようなすぐに役に立つ系の話が多かったイメージがあります。
しかし今年は、なぜそのような設計にしたのか、あるライブラリや技術がどのように実装されているか、という一歩踏み込んだ話が増えた気がします。このような話はすぐには役に立たないかもしれないですが、アプリ開発に限らず良い影響が出ると個人的には思っています。

また、去年はサンプルコードがKotlinの場合は断りを入れている人が多かったですが、今年は当然のようにKotlinやRxJavaが使われていて、技術の移り変わりを感じました。

特によかったセッション

見たセッションはほとんど良かったが、特に良かったもの

  • 詳解 ViewGroupのレイアウト内部実装 by Hiroyuki Seto
  • ConstraintLayout, now and future
    ConstraintLayout完全に理解した(理解したとは言ってない)
    英語は何を言っているかわからなかったが言いたいことは分かった。今年は英語をがんばる

  • AndroidとCPU
    CPU? ARMとか、Snapdragonが強いんでしょ、程度の知識しかなかったがCPUの種類や特徴について知ることができて、想像以上に良いセッションだった。

  • multi-module-no-susume by Shinnosuke Kugimiya
    すごく良く考えて設計しているなと感じたし、設計に銀の弾丸はないなと改めて思った。作るものに適した設計をできる力をつけていきたい。
    次に個人で作るアプリはマルチモジュールやっていく

アフターパーティとか

同級生や前職でお世話になった人、去年のDroidKaigi以来の人と話せて良かった。
去年も評判が良かったコーヒーを飲みたかったが、普段からコーヒーをあまり飲まないので今年も結局飲まずに終わってしまったのが残念。
企業ブースは時間がなくて半分ぐらいしか回れず。

また来年もDroidKaigiがあるのなら発表したいと思う。
DroidKaigi、最高だった。

Kotlinで型パラメータの型情報を取得する

フレームワークやライブラリの仕様で、Classインスタンスが必要になる場合があります。
例: AndroidIntentを作る場合、jsonを特定の型にパースする場合など

JavaとKotlinではこのような場合にどのような違いがあるのか調べました。

Javaの場合

以下のようにAndroidIntentを作る際にActivityClassを渡す場合

public <T> void startActivity(Context context, Class<T> clazz) {
    context.startActivity(new Intent(context, clazz));
}

startActivity(this, MainActivity.class)

上のコードは問題なく動きますがジェネリクスで型が分かっているのに引数でClassを渡しているのは冗長な気がします。 型パラメータからClassを取得しようとするとどうなるのでしょうか

public <T> void startActivity(Context context) {
    context.startActivity(new Intent(context, T.class));
}

これはコンパイルエラーになります。これ以外の構文でもTからClassを取得することはできません。
よってJavaでは型パラメータからClassを取得することは基本的にはできず、引数としてClassを渡さなければいけないのです。

Kotlinの場合

Kotlinの場合ではどうでしょうか?

Kotlinでは多少制約はありますが型パラメータのメソッド呼び出しやフィールドアクセスが可能です。
上のJavaコードをKotlinで書くと以下のようになります。

inline fun <reified T> startActivity(context: Context) {
    context.startActivity(Intent(context, T::class.java))
}

startActivity<MainActivity>(this)

ポイントは

  • inline関数にする必要がある
  • 型パラメータにreifiedをつける
  • 型引数に Nothingや、List<String>のように型パラメータ付きのクラス(Arrayは除く)は使えない。

また、obj is Tのように型チェックも可能です。これもJavaではできないです。

最後に

今回はreifiedの機能や使い方について調べてみました。
reifiedを使うには制約がありますが、それは何故なのかも調べてみたいと思います。

Kotlinで関数の引数などの条件をチェックする関数

kotlinの標準ライブラリに条件式を満たさなければ例外を出すという動作を行う関数が定義されています。
関数内で引数の条件をチェックする場合などに使うと便利なので紹介したいと思います

IllegalArgumentExceptionを発生させる

引数をチェックするための関数は以下の4つがあります

  • fun require(value: Boolean)
  • fun require(value: Boolean, lazyMessage: () -> Any)
  • fun requireNotNull(value: T?): T
  • fun requireNotNull(value: T?, lazyMessage: () -> Any): T

使用例

fun require(value: Boolean)

fun List<String>.second() {
    require(size >= 2)
    this[1]
}

listOf("a").second()
// => java.lang.IllegalArgumentException: Failed requirement.

fun require(value: Boolean, lazyMessage: () -> Any)
こちらは、例外を投げる時のメッセージを指定することが可能です

fun List<String>.second() {
    require(size >= 2){
        if(size==0) "リストの要素が1つも無いよ"
        else "リストの要素が1つしかないよ"
    }
    this[1]
}

listOf("a").second()
// => java.lang.IllegalArgumentException: リストの要素が1つしかないよ

fun requireNotNull(value: T?): T

fun f(v: Any?): String {
    requireNotNull(v)
    return v.toString()
}

f(null)
// => java.lang.IllegalArgumentException: Required value was null.

f("not null")
// => "not null"

fun requireNotNull(value: T?, lazyMessage: () -> Any): T

fun f(v: Any?): String {
    requireNotNull(v){ "nullじゃない値がほしい" }
    return v.toString()
}

f(null)
// => java.lang.IllegalArgumentException: nullじゃない値がほしい

IllegalStateExceptionを発生させる

なんらかの処理を行う前に関係する状態をチェックするための関数が定義されています

  • fun check(value: Boolean)
  • fun check(value: Boolean, lazyMessage: () -> Any)
  • fun checkNotNull(value: T?): T
  • fun checkNotNull(value: T?, lazyMessage: () -> Any): T

使用例

関数の使い方はrequireとほとんど同じなので1つだけ例をあげておきます

var someState: String? = null
fun end(): String {
    val state = checkNotNull(someState) { "Stateがnullになっている" }
    check(state == "start") { "この処理を実行するにはStateがstartの必要があります" }
    // ...
    return state
}

someState="end"
end()
// => java.lang.IllegalStateException: この処理を実行するにはStateがstartの必要があります

まとめ

requireやcheck関数を使用することで特定の状況でのみ処理が実行されることを保証することができるため、バグを生みにくくコードが読みやすくなることが期待できます。
requireとcheck関数の処理や呼び出し方法はほとんど変わらないため、場面に応じて使い分けると良いでしょう。

演算子オーバーロードを使ってFragmentを生成しない方が良いのではないか

少しツイートが古いですが、Kotlin + AndroidのFragmentで下記ツイートの方法を使っているコードを見かけたので自分の考えを書いておきます
※このツイートがどのような文脈で呟かれたのかを確認していないのでこのツイートだけを見て思った感想です。

class MyFragment : Fragment() {
    companion object {
        operator fun invoke(arg: Int): MyFragment {
            val frag = MyFragment()
            frag.arguments = Bundle().also { it.putInt("ARG", arg) }
            return frag
        }
    }
}

Fragmentを生成する際の問題

前提知識として、AndroidのFragmentを生成する際には以下のような問題点があります。

  • コンストラクタで値を渡すだけではFragmentが再生成された時にその値が復元されないためFragment#setArguments(Bundle)を介して値を渡す必要がある
  • 再生成する際にデフォルトコンストラクタ(引数なしのpublicなコンストラクタ)が必要
  • デフォルトコンストラクタ以外のコンストラクタを定義すると警告が出る f:id:scache:20171205020555p:plain

何ができるようになるか

まず、冒頭のコードを書くことで何ができるかというと以下のようにコンストラクタを呼び出すかのようにFragmentを生成できます。さらに問題点であった警告も表示されません。

MyFragment(42)

Fragmentで演算子オーバーロード(invoke)を使う際の問題

Fragmentではデフォルトコンストラクタが必要という制限があるためMyFragment(Int)だけではなく、MyFragment()の呼び出しも可能になるという問題があります。
もちろんデフォルトコンストラクタで正しくFragmentを生成できるよう実装をしていれば問題はないのですが、そのような場面はあまりないのではないでしょうか?

間違えて、正しくFragmentを生成できないコンストラクタを使ってしまう可能性があるため、冒頭のようなコードは書かずにnewInstanceのようなファクトリメソッドを定義するのが良いと個人的には思います。

最後に

今回のFragmentのケースではAndroidFrameworkの制約があるため、あまり良くない演算子オーバーロードの使い方だと感じました。
色々議論の余地はあると思うのでぜひ意見があればコメントなどお願いします。