コネヒト開発者ブログ

コネヒト開発者ブログ

Kotlin1.1の新機能について

f:id:tommy_kw:20161106193050p:plain

こんにちは!@tommykwです。先日のDroidKaigiとても楽しかったですね。2日間参加させていただき、どのセッションも素敵なセッションで学びのある時間を過ごせました。スタッフ、スピーカー、参加者の皆さん、ありがとうございました!

さて、Kotlin1.1が3/1にリリースされました。早いものでもう3週間ほど経ちますが、すでに利用されている方も多くいらっしゃるのではないでしょうか。Kotlin1.1では多くの機能がリリースされましたが、今回はKotlin1.1で導入された便利な新機能をいくつか情報共有したいと思います。

今回紹介する新機能

  • Alt+Enter
  • _キーワード
  • 型エイリアス
  • ::キーワード
  • データクラス継承

Alt+Enter

Alt+EnterとはIntentionと呼ばれるもので、Android Studio上でAlt+Enterすることによって簡単にクイックフィックスできる機能です。 高速コーディングを支える技術で、Kotlin1.1で新しく追加されたクイックフィックスについて紹介したいと思います。

  • Add Android View constructors using @JvmOverloads

Android開発でカスタムViewを作る場合はコンストラクタを複数定義する必要があり、とても冗長になります。 それをKotlinの@JvmOverloadsを使うことによって簡潔に記述できます。 f:id:tommy_kw:20170317080730g:plain

簡単ですね。しかし、以下のように既にコンストラクタを定義した場合はクイックフィックスできません。 この状態からコード最適化できるとリファクタリングが容易です。

class CustomView: View {
    constructor(context: Context) : this(context, null) {
    }
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
    }
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
    }
}
  • Convert to range check

範囲指定しているif文をRange型に変換します。 f:id:tommy_kw:20170317082139g:plain

  • Merge 'if's

ネストしたif文をマージします。 f:id:tommy_kw:20170317082603g:plain

  • Use destructuring declaration

ラムダで分解宣言を使うことによって、オブジェクトからデータを取り出して別の変数に代入します。 f:id:tommy_kw:20170317224853g:plain

よく見ると、fun main(args: Array<String>) {} と記述するとKotlinアイコンが表示されますね。 エラー、警告表示があった時に取り敢えずこのショートカットを使えば役に立ちますし、特にカスタムViewのクイックフィックスは Kotlinっぽく記述できてコーディングが捗りそうです。

_キーワード

Swiftでもお馴染み_キーワードで、パラメータを省略するためのキーワードです。 ママリQにて、Kotlin1.1にアップグレードした際にLintで未使用な変数である旨の警告が大量に表示されました。そんな時に利用したのが_キーワードになります。使用用途としては以下の通り、ラムダの引数、無名関数の引数、分解引数で利用できます。

  • ラムダの引数
// v, hasFocusパラメータを定義する
view.setOnFocusChangeListener { v, hasFocus ->
}

// v, hasFocusパラメータを省略する
view.setOnFocusChangeListener { _, _ ->
}
  • 無名関数の引数
// xパラメータを定義する
val function = fun(x: Int) {}

// xパラメータを省略する
val function = fun(_: Int) {}
  • 分解引数 その1
// a, b, cパラメータを定義する
val (a, b, c) = listOf(1,2,3)

// bパラメータを省略する
val (a, _, c) = listOf(1,2,3)
  • 分解引数 その2
// a, b, cパラメータを定義する
listOf(Triple(1, 2, 3)).forEach { (a, b, c) -> }

// a, cパラメータを省略する
listOf(Triple(1, 2, 3)).forEach { (_, b, _) -> }
  • onClickイベントのunusedなView引数

AndroidでよくあるパターンのonClickイベントのView引数がunusedになる問題ですが、

fun onPhotoClick(@Suppress("UNUSED_PARAMETER") view: View) {
    // do something
}

残念ながら以下の様にメソッドの引数には適用できません。unusedを回避するには@Suppress("UNUSED_PARAMETER") アノテーションをつけるのが良さそうです。

fun onPhotoClick(_: View) {
    // do something
}
  • _キーワードに型を明示的に定義

Function型の引数が複数ある場合に{}{ _ -> }で記述するか悩むケースがあります。 以下はRxJavaを用いたサンプルになります。

// RxJava1の非同期Extension
fun <T> Observable<T>.async(onNext: (T) -> Unit, onError: (Throwable) -> Unit) {
    this.subscribeOn(Schedulers.newThread())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(onNext, onError)
}

// APIクライアントからメンバーカウントを取得する
apiClient.memberCount()
    .async(
        { result ->
            if (result.count > 0) {
                // do something
            }
        }, 
        { e ->  } // 未使用のeが未使用となる
    )

上記の{ e -> }{}に変えても良いですし、{ _: Thowable -> }と型指定した宣言をしても良いのではないでしょうか。 多少冗長になりますが、個人的には可読性が上がる場合もあるかなと思います。

apiClient.memberCount()
    .async(
        { result ->
            if (result.count > 0) {
                // do something
            }
        }, 
        { _: Thowable ->  }
    )

型エイリアス

型エイリアスは既存の型を別の型として提供でき、関数、コレクション、コンパニオンオブジェクト、ネストしたクラスなどに適用できます。 トップレベル宣言、メンバー宣言、ローカル宣言をすることができますが、Kotlin1.1ではトップレベル宣言のみサポートされています。

以下はKotlinのstdlibのコード内で型エイリアスを利用しているケースですが、本来importする必要があるライブラリを型エイリアスを使うことによって、利用する側はimport不要で利用出来るのが良いです。

TypeAliases.kt

@file:kotlin.jvm.JvmVersion

package kotlin.collections

@SinceKotlin("1.1") public typealias RandomAccess = java.util.RandomAccess


@SinceKotlin("1.1") public typealias ArrayList<E> = java.util.ArrayList<E>
@SinceKotlin("1.1") public typealias LinkedHashMap<K, V> = java.util.LinkedHashMap<K, V>
@SinceKotlin("1.1") public typealias HashMap<K, V> = java.util.HashMap<K, V>
@SinceKotlin("1.1") public typealias LinkedHashSet<E> = java.util.LinkedHashSet<E>
@SinceKotlin("1.1") public typealias HashSet<E> = java.util.HashSet<E>


// also @SinceKotlin("1.1")
internal typealias SortedSet<E> = java.util.SortedSet<E>
internal typealias TreeSet<E> = java.util.TreeSet<E>

さらに、以下のAnnotations.ktを見ていただくといくつかのアノテーションをサポートしていることがわかります。 型エイリアスは@Deprecated, @SinceKotlin, @Suppressアノテーションを許可されていて、別名の型として利用する機会はあると思いますが、アノテーション付きで定義できるところが良いなという印象です。

Annotations.kt


@Target(CLASS, FUNCTION, PROPERTY, ANNOTATION_CLASS, CONSTRUCTOR, PROPERTY_SETTER, PROPERTY_GETTER, TYPEALIAS)
@MustBeDocumented
public annotation class Deprecated(
        val message: String,
        val replaceWith: ReplaceWith = ReplaceWith(""),
        val level: DeprecationLevel = DeprecationLevel.WARNING
)

@Target(CLASS, ANNOTATION_CLASS, PROPERTY, FIELD, LOCAL_VARIABLE, VALUE_PARAMETER,
        CONSTRUCTOR, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER, TYPE, EXPRESSION, FILE, TYPEALIAS)
@Retention(SOURCE)
public annotation class Suppress(vararg val names: String)

@Target(CLASS, PROPERTY, CONSTRUCTOR, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER, TYPEALIAS)
@Retention(AnnotationRetention.BINARY)
@MustBeDocumented
public annotation class SinceKotlin(val version: String)

このような未定義を表したカスタムViewクラスも定義できますし、@Suppressアノテーションもうまく利用できるかもしれません。

@Deprecated("unused view")
typealias DeprecatedView = View

::キーワード

::キーワードが追加されました。これはJavaの<expression>::classメソッド参照と<expression>::<member name>メンバー参照をサポートしています。 毎回ラムダで少し冗長に記述するのではなく、::キーワードを使ってより簡潔に記述できます。

// ラムダ
listOf("a","b","c").map { it.toUpperCase() }

// メソッド参照
listOf("a","b","c").map(String::toUpperCase)
data class Member(val id: Int)
// ラムダ
intArrayOf(1,2,3).map { Member(it) }

// メソッド参照
intArrayOf(1,2,3).map(::Member)

また、KClassを取得するのに、.javaClass.kotlinがありましたが、::classを使ったほうがよさそうです。 以下のコードからもわかるように、.javaClass.kotlin::classでは動作が異なるため、.javaClassはDeprecatedになっていく予定のようです。

class Foo {
   fun bar() {
      val a: KClass = this.javaClass.kotlin
      val b: KClass = this::class
 
      a.memberProperties.forEach { property: KProperty1<Foo, *> ->
          property.get(this)
      }
 
      b.memberProperties.forEach { property: KProperty1<out Foo, *> ->
          property.get(this) //Error: Kotlin: Out-projected type 'KProperty1<out Foo,*>' prohibits the use of 'public abstract fun get(receiver: T): R defined in kotlin.reflect.KProperty1'
      }
   }
}

データクラス継承

データを保持するクラスとして便利なdata classがありますが、これは継承することができませんでした。以下の通り、Kotlin1.1からsealed classやopen classを継承できるようになり、柔軟なデータクラス設計ができるようになりました。

open class Base(
    open val id: Int, 
    open val name: String
)

data class Member(
    override val id: Int,
    override val name: String,
    val age: Int
) : Base(id, name)

val member = Member(1, "foo", 20)

今までは生成されるコードの一貫性を保つために、クラスを継承できない仕様になっていました。インターフェースは実装することができましたが、 sealed classやopen classを継承できるのはとても便利です。

Kotlin 1.0.7、1.1.1リリース

早いですね!1.0.7は現時点でバージョン1.1にアップグレードできないユーザが利用できるように、Gradleおよびアノテーション処理に関連する修正をバックポートできるバージョンです。1.1.1は1.1.0のバグ修正がメインで一部のIDE、Compiler、Librariesの修正を行っています。特に問題ないのであれば、1.1.1の利用をオススメします。

まとめ

Kotlin1.1の新機能について簡単に説明しましたが、いかがでしょうか。本来であれば、Kotlin1.1のメイン機能であるCorutinesについて情報共有できれば良かったのですが、正直まだ使いどころがわかっていません。こちらは引き続き理解を深めていきます。また今回紹介していない新しいスコープ関数provideDelegateオペレータなどすぐに利用出来る便利な機能がたくさんリリースされました。これらをうまく利用することで、Kotlinの言語機能を最大限に活かせそうです。それでは楽しいKotlinライフをお過ごしください!