読者です 読者をやめる 読者になる 読者になる

Connehito開発者ブログ

Connehito Engineer's Blog

Kotlinを導入したお話

Kotlin Java
f:id:tommy_kw:20161106193050p:plain

こんにちは!エンジニアの富田(@tommykw)です。 ママリQ Android アプリにKoltinを導入しましたので、導入方法などを紹介します。

ママリQ Android アプリ

家族の毎日の疑問や悩みを解決するママのためのQ&Aアプリです。ストアのレビューが4.5と非常に評価の高いアプリです。

なぜKotlinを導入したのか

  • Javaだと冗長、複雑になるコードもすっきり可読性が上がる
  • Swift使いのiOSエンジニアにもなじみやすい文法
  • JavaのNullPointerExceptionを駆逐できる
  • デメリットが少なかった

簡潔に安全にコードを書けるのはいいですね。Kotlinのスコープ関数やreadonlyや強力なコレクションを言語標準機能として使えるのはとても魅力的なため導入に踏み切りました。

ただメリットだけではなく、懸念点もありました。 DataBindingを利用すると真っ赤な画面での開発になり辛いです。

f:id:tommy_kw:20161107075432p:plain

KotlinとDataBindingを使っている環境であればこのような画面で開発されていると思いますが、JetBrainsのKotlin Issueでversion 1.0.5で修正となっていますね。やったね!! 今後は快適に開発が進められそうです。

導入内容について

それぞれ内容について説明します。

導入方針策定

Kotlin化については以下の基本方針を基に進めました。

  • 無理にKotlin化はしない
  • リファクタリングや新機能を実装する際にKotlin化をする
  • テストはKotlinで書いてみる

コネヒトは開発者の人数も少なくリソースが限られているため、無理にKotlin化するのではなく、リファクタリングなど最低限の部分でKotlinを導入しています。

Kotlin version 1.0.4

Kotlinをプロジェクトに導入するのは非常に簡単です。 Android Studio > Tools > Kotlin > Configure Kotlin in Projectをクリックすればダイアログが出て、OKを押すとセットアップできます。コネヒトでは現在最新版のversion 1.0.4を導入しています。

kotlintest テストフレーム

ユニットテストでkotlintestを使っています。テストフレームはspekなどもありますが、kotlintestはscalatestのような記述でmatcherも豊富でこのライブラリだけで簡単にテストが書けるため採用しました。以下はkotlintestを使ったサンプルになります。

/**
 * 日付に関するテスト
 */
class DateTest : StringSpec() {
    init {
        "Date型からyyyy年MM月dd日形式に変換する" {
            DateUtil.toDisplayString(Calendar.getInstance().apply {
                set(2016, 11, 2)
            }.time) shouldBe "2016年11月2日"

        }
        "Date型からyyyy-MM-dd形式に変換する" {
            DateUtil.toPostString(Calendar.getInstance().apply {
                set(2016, 11, 2)
            }.time) shouldBe "2016-11-2"
        }
    }
}

アノテーションを使う

JavaとKotlinが共存するプロダクトなのでKotlinからJavaを呼び出すこともありますし、その逆も然りです。クラッシュ予防や簡潔に呼び出すためのアノテーションを使いました。

  • @Nullableアノテーション
class TestActivity : AppCompatActivity(), TextView.OnEditorActionListener {
    override fun onEditorAction(v: TextView, actionId: Int, event: KeyEvent): Boolean {  // java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter event
    }
}

上記のコードはKeyEventがNonnullのため実行時にクラッシュします。これはNonnullな型に対してnullがセットされたため発生するクラッシュです。このようなクラッシュが起きた時はエラーだけを見てもどこでエラーが発生したか分かりにくいです。 この不具合を回避するためにはJava側では@Nullableアノテーション、Kotlin側では?を使ってNullableであることを宣言する必要があります

  • @JvmStaticアノテーション
// Kotlin
TestFragment : Fragment() {
    companion object {
        @JvmStatic
        fun newInstance(): TestFragment {
            return TestFragment()
        }
    }
}

// Java @JvmStaticあり
TestFragment.newInstance()
// Java @JvmStaticなし
TestFragment.Companion.newInstance()

シングルトンオブジェクトをクラス内に定義するには、companionキーワードを使い、 companion付きのオブジェクトをcompanionオブジェクトと言います。 通常はcompanionオブジェクトを介して呼び出しますが、 上記のコードはアノテーションを使うことによって直接メソッドを呼ぶことができます。

  • @JvmFieldアノテーション
// Kotlin
class Member {
    @JvmField
    var id = 0
    @JvmField
    var name = ""
}

// Java
Member member = new Member();
member.id
member.name

上記のコードはPOJOにgetter/setterを生やしてプロパティを呼び出しますが、@JvmFieldを使うことによってgetter/setterではなく直接プロパティにアクセスできます。

Kotlin/Javaでそれぞれ問題なくアクセスできるので安心して導入できますね

簡単に導入結果について

  • 1ヶ月で10%くらいはKotlin化できた
  • Kotlinリファクタリングで1、2割くらいコード行数が減った
  • コレクション操作が直感的で扱いやすかった
  • val(readonly)、var(mutable)があり、readonlyがあるので安全だった
  • 遅延初期化(lateinit/lazy)が便利だった

導入が本当に楽でした。コード行数が減ったのはスコープ関数やSAM変換を使うことによって無駄なimportも減り短く記述できたからです。 valはJavaでいうfinalに近い存在だと思いますが、valはimmutableではなくあくまでreadonlyになるのが興味深かったです。実際に使ってみてreadonlyとスコープ関数と遅延初期化は非常に便利でしたし、可読性も上がり安心してコードを書くことができました。lateinit/lazyについては以下の通りです。

  • lateinit
class TestActivity : AppCompatActivity() {
    private lateinit var binding: ActivityTestBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_test)
    }
}

lateinitは必ず初期化するけどクラス生成時に初期化できないようなケースで使用します。必ず初期化するのでNonnullであることを保証し、面倒くさいNullチェックが不要となります。

  • lazy
class TestActivity: AppCompatActivity() {
    private lateinit var binding: ActivityTestBinding
    private val header by lazy {
        DataBindingUtil.inflate<HeaderBinding>(
            layoutInflater,
            R.layout.header,
            binding.headerList,
            false
        )
    }
}

lazyは対象の変数にアクセスされた際に初期化されます。遅延初期化によって効率的に初期化され、readonlyにできるのが素敵ですね

まとめ

ママリQ AndroidにKotlinを導入したお話でした。まだ導入して1ヶ月のため理解も浅く知見も少ないですが、今後KotlinやOSSに対して何かしら貢献できればと考えています。

最近のKotlin界隈では、TornadoFXRxKotlinFXAnkoなどAndroidに限らず面白いフレームが多く刺激的ではないでしょうか。version 1.1からasync、await、yieldも導入されてますます楽しみですね!