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の制約があるため、あまり良くない演算子オーバーロードの使い方だと感じました。
色々議論の余地はあると思うのでぜひ意見があればコメントなどお願いします。

Kotlinのデフォルト値を使用した関数やコンストラクタをJavaから使用する際のtips

tl;dr

Kotlinコードで定義したデフォルト値を使用した関数,コンストラクタをJavaコードで使う事を想定する場合には@JvmOverloadsアノテーションをつける

@JvmOverloadsアノテーション

Kotlinの関数やコンストラクタの宣言で、以下のようにデフォルト値を使うと

class MyClass {
    fun myFunc(a: String = "", b: Int = 1, c: Boolean = true) {}
}

JavaからはMyClass#myFunc(String, int, boolean)のみ定義されているように見えます
変数b,cにはデフォルト値を使いつつMyClass#myFunc(String)の呼び出しを行いたいという場面では@JvmOverloadsアノテーションを使うことができます

@JvmOverloads
fun myFunc(a: String = "", b: Int = 1, c: Boolean = true) {}

Javaからは以下の4つが定義されているように見えます

  • myFunc()
  • myFunc(String)
  • myFunc(String, int)
  • myFunc(String, int, boolean)

@JvmOverloadsアノテーションをつける事で生成されるメソッドは、関数定義の後ろに宣言されているデフォルト値付き引数から順番に省略されます
今回の例ではc>b>aの順番に引数が消えた関数が生成されます

使用例: AndroidのCustomView

AndroidでCustomViewに以下のようなコンストラクタを生成する際に使うことができます

  • MyCustomView(Context)
  • MyCustomView(Context, AttributeSet)
  • MyCustomView(Context, AttributeSet, int)
class MyCustomView
@JvmOverloads constructor(
        context: Context,
        attrs: AttributeSet? = null,
        defStyleAttr: Int = 0)
    : View(context, attrs, defStyleAttr)

後ろにあるデフォルト値付き引数から順番に消えるため、リフレクションを使う場合など、引数の順番が大事な場合には定義の順番に気をつける必要があります

公式ドキュメント

JvmOverloads - Kotlin Programming Language

Kotlinレポジトリのidea pluginやJ2Kのテスト実行方法メモ

Kotlin1.1.50あたりからビルドスクリプトがgradle(Gradle Kotlin Script)になった。その影響かテストの実行方法が変わっていたので、メモしておく

※以下の情報は古くなる可能性が高いのでまずはKotlin(github)のReadmeを先に読んでください

Gradleタブから実行

IntellijのGradleタブからtestを実行するタスクを見つけて実行する
だいたいはTasksverificationにあると思う

f:id:scache:20171002231145p:plain:w300

Gradleのタスクを直接実行する

Gradleなのでターミナルから直接テストタスクを実行することができる

テストの実行

例: ideaのテストを実行

./gradlew :idea:cleanTest :idea:test

テスト結果はbuildディレクトリの中に作成される

一部のテストだけ実行する

一部のテストだけ実行したいという場合がよくあると思うが、その場合は--testsオプションを使うと良い

例: ideaのorg.jetbrains.kotlin.addImportパッケージ内のテストのみ実行

./gradlew :idea:cleanTest :idea:test --tests org.jetbrains.kotlin.addImport.*

Projectツリーから実行

Intellijの設定

Readmeにも少し書いてあるが、以下のページを開き
Preferences -> Build, Execution, Deployment ->Build Tools -> Gradle -> Runner

Delegate IDE build/run actions to Gradleをチェックする。書いてある通り、チェックをするとビルドや実行のアクションをGradleに移譲することができる

f:id:scache:20171002225618p:plain:w400

テストの実行

設定が終わったらIntellijでプロジェクトツリーでテストコードがあるディレクトリを開き、右クリックをしてRun Testする

f:id:scache:20171002232833p:plain:w400

一部のテストのみ実行する

RunTestを行うと新しくRun Configurationが作られていると思うので、Edit Configurationsから編集を行う
Gradleタスクの引数で制御する形になるのでArgumentsに条件を書く

f:id:scache:20171002234650p:plain:w400

CamTwistを使ってAndroidEmulatorのカメラソースをPC画面にする

バーコード読み取り機能を作る場合などにいちいち実機でバーコードをスキャンするのが面倒臭かったのでなんとかPCに表示した画像をエミュレータのカメラソースにできないか調べてみました

CamTwist

CamTwistニコニコ生放送やTwitchなどで配信する時に、Webカメラの映像ではなく、PC画面を映像のソースとして配信するためのソフトです。エミュレータではWebカメラや内臓カメラをソースとして使えますが、同じようにCamTwistの映像も使うことができます

以下の方法はCamTwist以外にも同じ機能をもったソフトであれば有効だと思います

設定方法

CamTwist

CamTwistをダウンロードページからダウンロードしてインストールします

CamTwistの設定は、以下のようにします

  • Desktop (範囲は任意)
  • Rotation Translation(任意)
  • Zoom(任意)

f:id:scache:20170927010204p:plain:w400

エミュレータで表示した時に画像が回転した状態で表示されていたらRotationを設定してみてください

AVD

  1. エミュレータの設定画面からShow Advanced Settingsを押す
  2. Cameraの設定をwebcam0にする
  3. 設定を保存

f:id:scache:20170927010812p:plain:w400

f:id:scache:20170927014603p:plain:w200

以上で設定は終了です

エミュレータ起動

エミュレータでカメラアプリを起動して確認してみます

以下のようにCamTwistで指定した範囲の映像が表示されていればOKです

f:id:scache:20170927012641p:plain:w300

おまけ webcam0について

Macではもともと内臓カメラがついている端末がほとんどだと思いますが、エミュレータの設定では内臓カメラやCamTwistなどの映像ソースが複数ある場合でもwebcam0しか選択することができません(AndroidStudioのエミュレータ設定ではカメラがあろうがなかろうがNone, Emulated, Webcam0の3つのみ選択できるようになっているようです)。

そのため、環境によっては内臓カメラの映像が自動的に選択されてしまうかもしれません。

emulatorコマンド Commonly used optionsを見た限りではemulatorコマンドからエミュレータを起動する際にどの映像ソースを使うかを選択できそうですが、カメラ一覧を取得してもwebcam0しか表示されず任意の映像ソースを選択する方法はわかりませんでした。

FCMを使ってAndroid端末へ通知を送るスクリプト

Firebase Cloud Messaging(FCM)を使ってAndroid端末へ通知を送るスクリプトを作りました(iOSは非対応です)
スクリプト内で送られているデータはcurlを使う場合を参照してください

作成したスクリプト

ソースコードhttps://github.com/sckm/fcm-send にあがっています

インストー

あらかじめgoがインストールされている必要があります

$ go get github.com/sckm/fcm-send

使い方

以下のように、はじめにjsonのdataキーで送りたい内容をファイルに書いておきます

{
    "message": "Hello World!",
    "title": "example title"
}

その後、以下のコマンドを実行することで通知が送られます

$ token='registration token'
$ server_key='your server key'
$ fcm-send -t $token -s $server_key -p data.json

curlを使う場合

作成したスクリプトは以下のcurlコマンドと同等のリクエストを送っています

token='registration token'
server_key='your server key'
msg='{"message":"Hello World!", "title": "example title"}'

curl \
    --header "Authorization: key=${server_key}" \
    --header Content-Type:"application/json" \
    https://fcm.googleapis.com/fcm/send \
    -d "{ 
        \"to\" : \"${token}\", 
        \"data\": $msg
    }"