はじめに
仕事中、タイムトラッキングツールの「Toggl」を使っています。どんな作業にどれぐらいの時間をかけたのかを簡単に記録できるツールで、仕事終わりに Toggl の記録から業務報告的なやつを ChatWork に載っけていました。同僚の iOS エンジニアがこの報告を簡単にできるアプリを作っていたので、自分も Android アプリを作ろうと思い、せっかくだからと Kotlin で書いてみました。小さく、一般公開はしていませんが 1 本のアプリを Kotlin で書いてみた感想を紹介します。
スクリーンショット
記録が表示される画面 | 年月日の選択 | トークン入力画面 | 送信先の選択画面 |
---|---|---|---|
使用したライブラリ
compile 'com.android.support:appcompat-v7:23.1.1'
compile 'com.android.support:design:23.1.1'
compile 'com.squareup.moshi:moshi:1.0.0'
compile 'com.squareup.okhttp3:okhttp:3.0.1'
compile 'com.squareup.okhttp3:logging-interceptor:3.0.1'
compile 'com.squareup.retrofit2:retrofit:2.0.0-beta3'
compile 'com.squareup.retrofit2:converter-moshi:2.0.0-beta3'
compile 'com.squareup.retrofit2:adapter-rxjava:2.0.0-beta3'
compile 'com.jakewharton.threetenabp:threetenabp:1.0.3'
compile 'io.reactivex:rxjava:1.1.0'
compile 'io.reactivex:rxandroid:1.1.0'
compile 'io.reactivex:rxkotlin:0.30.1'
// Kotlin
kapt 'com.android.databinding:compiler:1.0-rc5'
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
// Deploygate
compile 'com.deploygate:sdk:3.1'
驚きの Square / Jake 率です。Retrofit は現在 2.0-beta3 がリリースされていますが、 アプリの構築時点では beta3 に対応した RxJavaCallAdapterFactory が登場していなかった ので、 2.0-beta2 を使っています。それに伴って、 OkHttp も 2.7.2 を使いました。 Retrofit 2.0-beta3 からパッケージ名が変わったことを失念していて、 RxJavaCallAdapterFactory だけ build.gradle 中の記述を間違えるというポカが原因でした。現在は Retrofit 2.0-beta3 と OkHttp 3.0.1 を使っています。 Kotlin 的なところでいうと、 Data Binding を使うために kapt
で Data Binding Compiler 1.0-rc5 と、リフレクション対応のための kotlin-reflect
を使用しています。
自動生成されたコードに対応するために、 build.gradle
には
kapt {
generateStubs = true
}
を追加しています。
また、 Data Binding Compiler は現在 1.1 が最新のようなのですが、 1.1 を使用すると以下の例外が発生してコンパイルが通りませんでした。その為、 1.0-rc5 を使用しています。
Error:Execution failed for task ':app:compileDebugJavaWithJavac'.
> java.lang.StringIndexOutOfBoundsException: String index out of range: -1
Kotlin と Data Binding
ButterKnife や Adapter の ViewHolder パターンの代わりに使えると専ら評判の Data Binding ですが、 Kotlin で使うときは
class MainActivity : AppCompatActivity() {
lateinit var mBinding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
}
}
このように、 lateinit
キーワードを付ける必要がありました。これはインスタンス変数に Binding オブジェクトを持たせておく場合にだけ必要で、ローカル変数として定義する場合は必要ありません。
Kotlin と Adapter
ListView を使うときになどにセットする Adapter では良く ViewHolder パターンが使われます。このときにも ButterKnife が活躍するのですが、 Data Binding を使うと一気にスッキリとさせることができます。
@Override
public View getView(int position, View containerView, ViewGroup parent) {
if (containerView == null) {
LayoutInflater inflater = LayoutInflater.from(mContext);
SampleLayoutBinding binding = DataBindingUtil.inflate(inflater, R.layout.sample_layout, parent, false);
containerView = binding.getRoot();
}
SampleLayoutBinding binding = DataBindingUtil.getBinding(containerView);
// ここで binding.hoge などを参照して View を組み立てる
return containerView;
}
これを Kotlin では
override fun getView(position: Int, containerView: View?, parent: ViewGroup?) {
val inflater = LayoutInflater.from(mContext)
val view = containerView ?: let {
val binding = DataBindingUtil.inflate<SampleLayoutBinding>(inflater, R.layout.sample_layout, parent, false)
// ここで binding.hoge などを参照して View を組み立てる
return binding.root
}
val binding = DataBindingUtil.getBinding<SampleLayoutBinding>(view)
// ここで binding.hoge などを参照して View を組み立てる
return view
}
このように書いてみました。 Binding あとに View を組み立てる箇所が 2 箇所に散らばっていますが、これは別のメソッドにまとめるなどすれば良いと思います。?:
演算子を使うと JavaScript の var hoge = a || b;
のような感覚で、左辺が null だったら右辺という書き方ができます。 Kotlin を使うと a == null
のような書き方を極力使いたくない気持ちになります。
Kotlin と Moshi
Moshi は軽量な JSON ライブラリです。今回初めて使ってみたのですが、 POJO と JSON の単純な変換だったら十分そうでした。軽量 Gson といった面持ちです。
さて、 Kotlin には素晴らしい data class
というものがあります。 toString
の自動生成やなど、いくつかの嬉しいメリットを享受することができます。これを使って POJO を定義します。
data class Room(
@Json(name = "room_id") val roomId: Long,
val name: String,
val type: String,
val role: String,
val sticky: Boolean,
@Json(name = "unread_num") val unreadNum: Long,
@Json(name = "mention_num") val mentionNum: Long,
@Json(name = "mytask_num") val mytaskNum: Long,
@Json(name = "message_num") val messageNum: Long,
@Json(name = "file_num") val fileNum: Long,
@Json(name = "task_num") val taskNum: Long,
@Json(name = "icon_path") val iconPath: String,
@Json(name = "last_update_time") val lastUpdateTime: Long
)
ChatWork のチャットルーム情報を取得するエンドポイントを叩くと、スネークケースのフィールド名を持つ JSON が返ってきます。今回の Kotlin コードでは、変数名はローワーキャメルケースで定義していたので、 Moshi の Json
アノテーションを使ってフィールド名と変数名が異なる場合に対応させます。 Kotlin では上のような書き方で全く問題無く動作しました。
Toggl のエントリに対応する data class
は
data class TogglEntry(
val at: String,
val billable: Boolean,
val description: String,
val duration: Long,
val duronly: Boolean,
val guid: String,
val id: Long,
val pid: Long,
val start: String,
val stop: String,
val uid: Long,
val wid: Long
) {
fun getFormatText(): String {
val hours = (duration / 3600).toInt().format(2)
val minutes = (duration % 3600 / 60).toInt().format(2)
return "- $description ($hours:$minutes)"
}
}
このように書きました。 data class
とはいえ、普通のクラスのようにメソッドを定義することも可能です。上のコードでは、作業の記録が秒数で記録されている duration
フィールドから HH:mm
の形式で文字列を取得するための getFormatText
メソッドを定義しています。また、このメソッドの中では Int
型の拡張関数を使用しています。
Kotlin と拡張関数
Kotlin の素晴らしい機能の 1 つに拡張関数があります。 Objective-C のクラス拡張みたいなやつです。
fun Context.toast(@StringRes resId: Int, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(this, resId, duration).show()
}
例えば上のようなコードを extention
パッケージにでも入れておけば、 Context を継承したクラスであれば
toast(R.string.hello_world)
のように、簡単に Toast
を表示することができます。この拡張関数を使って Int
型の値を指定の桁数で 0 埋めし、文字列で取得する拡張関数を書きました。
fun Int.format(len: Int) = java.lang.String.format("%0${len}d", this)
Java 側の String#format
メソッドを使って簡単に定義することができます。もしも Java で書いていれば、 IntUtils のようなユーティリティクラスを用意して、そこに static メソッドを書くかもしれません。
public class IntUtils {
public static String format(int len, int num) {
return String.format("%0" + len + "d", num);
}
}
オブジェクトに紐付くメソッドとして定義できた方がスッキリしますね。また、何気なく使っていますが、文字列中に式を書くことができるのも Kotlin の嬉しいところです。
val a = "Hello"
val expressionString = "${a}, Kotlin!" // Hello, Kotlin!
使いすぎ注意かもしれませんが、拡張関数は非常に強力だと思いました。
Kotlin と RxJava (RxKotlin)
とても相性が良い組み合わせだと思います。 Android アプリを Java で書く場合、 Retrolambda に頼らなければ、匿名クラスだらけで非常にインデントが深く、記述量も多くなります。特に RxJava を使う場合は凄いことになりますが、 Retrolambda を使えばスッキリさせることができます。
Kotlin はデフォルトで SAM 変換の機能を持っているため、 Retrolambda と同じようなメリットを言語レベルで享受することができます。さらに it
キーワードを使えるので Retrolambda 以上にコードがスッキリします (it
が何を指しているのか分からなくなることがあるので、ちゃんと考えて使いたいです) 。
val subscription = apiClient.getTogglEntries(start, end)
.flatMap { it.toObservable() }
.filterNotNull()
.map { it.getFormatText() }
.reduce { s1: String?, s2: String? -> "$s1\n$s2" }
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
Log.d(TAG, it);
// UI への反映
})
一例ですが、こんなコードを書きました。 Retrofit から Observable<List<TogglEntry>>
で取得できた結果を Observable<TogglEntry>
に flatMap
で変換し、さらに map
で TogglEntry#getFormatText
を用いて文字列を取得、 reduce
で文字列を結合していった結果を subscribe
という流れです。
it
キーワードをガシガシ使っています。コンテキストによって何ものかが適切に変化してくれる魔法のキーワードですね。上にも書きましたが、何ものか分からなくなることがあるので、ちゃんと考えて使いましょう。
他に便利だと思った機能
when
式が非常に魅力的でした。
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
return when (item?.itemId) {
R.id.main_menu_calendar -> selectDate()
R.id.main_menu_refresh -> startRefresh()
R.id.main_menu_edit -> editApiToken()
else -> super.onOptionsItemSelected(item)
}
}
ActionBar においてメニューを選択したときのハンドリングは Java では if
または switch
で行うしかないですが、 Kotlin では when
式を使って非常に明確に、短く書くことができました。式なので、値を返すことができるのが強いですね。上の例の他にも、 RxJava の subscribe
中のエラー句の中で、例外の種類に応じた処理を書くのにも使いました。
.subscribe({
// onNext
}, {
// onError
when (it) {
is NoSuchElementException -> {
// NoSuchElementException のときの処理
}
else -> {
// どれにもマッチしないときの処理
}
}
})
最後に
自分で書くコードは全て Kotlin という縛りで小さいながらも 1 本のアプリを書いてみました。結果としては非常に魅力的な言語で、 Android アプリを書くときの良い選択肢であると思いました。もっとこう書けたらを実現してくれる言語です。今回は行っていませんが、何かあっても Java で書いてしまえるという退避路が用意されているのも安心できるポイントだと思います。
個人的には魅力的であると感じたものの、これをチーム開発で取り入れるには少し考えるかもしれません。ワンショットのアプリであれば良いかもしれませんが、例えばサービスとしての中核を担う、息の長いアプリであれば考えます。
また、 Java で書く場合よりも少し Android Studio のコードヒントの表示が遅かったり、 Android Studio 自体がレインボーカーソルを表示してハングアップする場面が多く見られました。言語自体はストレスないものの、環境側に少しストレスがある感じです。もうそろそろ Kotlin も 1.0 ですし、時間が解決してくれるかもしれません。