2016年、KotlinでAndroid開発する方へ

More than 3 years have passed since last update.

この記事はKotlin Advent Calendar 2015の最終日の投稿です。

前日はyy_yankさんのKotlinのSeleniumライブラリKebabを作ってるでした。

初日はngsw_taroさんの俺とKotlinの馴れ初めと歩み 〜正式リリースに向けて〜でした。


はじめに

現在、私は社内で新規の動画系サービスのAndroid版の開発を担当しています。2015年の4月から開発しているのですが、その約1ヶ月前からKotlin導入のための検証を始め、開発開始と同時に導入を決めて開発を進めて今に至ります。この記事ではそのプロジェクトの8ヶ月間のKotlinでの開発をまとめました。この記事がこれからKotlinでAndroidの開発をする方々に少しでも役立てばと思います。

また同カレンダーにはプロジェクトメンバーによる記事もあります。


現在のプロジェクトについて

Androidのエンジニアは自身も含めて4名です。

Kotlinのversionは1.0.0-beta-4583。minSdkVersionは16、compileSdkVersionは23です。メソッド数は約93000メソッド。全体の85%がKotlin、残りの15%はJavaで記述されています。メソッド数が65kを超えているので、multidexを使っています。

Javaで記述されている15%はDagger2の@Component,@Module,@Scopeがついているクラス群、Javaでかつて利用していた再利用可能なクラスです。

ビルド速度はフルビルドで約70秒です。

(環境 : MacBook Pro Retina, 15-inch, Late 2013 Processor : 2 GHz Intel Core i7 memory : 16 GB 1600 MHz DDR3)

ただし、開発時はminSdkVersionを21にしてbuild速度をあげて開発しています。


kapt

Dagger2のアノテーションはkaptを利用すればKotlinのコードからでも利用可能です。しかし、現在のプロジェクトはCircleCIでビルドをしており、一度Dagger2用の全てのコードをKotlinに移行した際CircleCI上でのみビルドがこけるという現象に直面したため、現在もaptを利用しDagger2用のコードはJavaで記述するという実装になっています。ただ、このkaptへの移行もKotlinのバージョンがM13の時だったので、時間ができればまた再チャレンジをしたいと考えています。


Libraries

build.gradeのdependenciesは以下のようになっています。

def appcompatVersion = "23.1.1"

def playServicesVersion = "8.3.0"
def rxBindingVersion = "0.3.0"
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
// support libs
compile "com.android.support:support-v4:$appcompatVersion"
compile "com.android.support:appcompat-v7:$appcompatVersion"
compile "com.android.support:recyclerview-v7:$appcompatVersion"
compile "com.android.support:design:$appcompatVersion"
compile "com.android.support:cardview-v7:$appcompatVersion"

// multidex
compile 'com.android.support:multidex:1.0.1'

// play services
compile "com.google.android.gms:play-services-base:$playServicesVersion"
compile "com.google.android.gms:play-services-ads:$playServicesVersion"
compile "com.google.android.gms:play-services-gcm:$playServicesVersion"
compile "com.google.android.gms:play-services-analytics:$playServicesVersion"

// kotlin
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
compile 'com.jakewharton:kotterknife:0.1.0-SNAPSHOT'

// Dagger 2
compile 'com.google.dagger:dagger:2.0.2'
apt 'com.google.dagger:dagger-compiler:2.0.2'
provided 'org.glassfish:javax.annotation:10.0-b28'

// data
compile 'com.google.code.gson:gson:2.4'
compile 'com.squareup.okhttp:okhttp:2.5.0'
compile 'com.squareup.retrofit:retrofit:1.9.0'

// cipher
compile 'com.facebook.conceal:conceal:1.0.1@aar'

// date
compile 'com.jakewharton.threetenabp:threetenabp:1.0.2'

// image
compile 'com.github.bumptech.glide:glide:3.6.1'
compile 'com.github.bumptech.glide:okhttp-integration:1.3.1'
compile 'jp.wasabeef:glide-transformations:1.2.1'

// reactive
compile 'io.reactivex:rxjava:1.1.0'
compile 'io.reactivex:rxandroid:1.1.0'
compile 'com.trello:rxlifecycle:0.3.0'
compile 'com.trello:rxlifecycle-components:0.3.0'
compile "com.jakewharton.rxbinding:rxbinding-kotlin:$rxBindingVersion"
compile "com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxBindingVersion"
compile "com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxBindingVersion"
compile "com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:$rxBindingVersion"
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.0'
compile 'com.squareup.sqlbrite:sqlbrite:0.4.0'

// event
compile 'com.squareup:otto:1.3.8'

// ui
compile 'jp.satorufujiwara:recyclerview-binder:1.3.2'
compile 'jp.satorufujiwara:material-scrolling:1.1.0'
compile 'com.github.ksoichiro:android-observablescrollview:1.5.2'
compile 'com.ogaclejapan.smarttablayout:library:1.4.2@aar'
compile 'uk.co.chrisjenx:calligraphy:2.1.0'
compile 'com.github.castorflex.verticalviewpager:library:19.0.1'
compile 'com.github.magiepooh:recycler-itemdecoration:1.1.0@aar'
compile 'com.github.aakira:expandable-layout:1.4.1@aar'

// video
compile 'jp.satorufujiwara:exoplayer-textureview:0.5.1'
compile 'com.google.android.exoplayer:exoplayer:r1.5.3'

// socket
compile 'io.socket:socket.io-client:0.6.1'
}

上記のライブラリ以外にログインのためのSDKや、デバッグ用にTimberやStethoなどを導入しています。

Androidで著名なライブラリは問題なしに使えます。

ただkaptを利用していないため、以下のライブラリは導入を断念しました。

またKotlin独自のライブラリとして以下の2つを採用しています。


KotterKnife

ButterKnifeのkotlin版です。以下のような感じでViewのfindViewByIdを行うことができます。

private val viewPager: ViewPager by bindView(R.id.viewPager)

KotterKnifeは内部でProperty Delegationが使われています。Property Delegationについては2日目の記事、みんな大好きKotlinのDelegationについて #ktac2015で詳しく書かれています。


RxBinding-kotlin

RxBindingをKotlin用に拡張したライブラリで、Kotlinから使いやすようにExtension Functionsが定義されています。

viewPager.pageSelections()

.filter { it > 0 && it >= adapter.count - 1 }
.subscribe { loadNext() }

上記の例はViewPagerが最後のPageまでフリックされたら続きを読み込む例です。RxJavaとKotlinのExtension Functionsを使うことで完結に記述することができます。(実際はload中はブロックする、RxLifecycleを用いてunsubscribeするなどの処理が必要です。)

RxBinding-kotlinについては2016年1月15日のAndroidでKotlin勉強会 @ Sansanにて発表する予定です。

追記 : 発表しました。資料は以下にあります。

https://speakerdeck.com/satorufujiwara/rxbinding-kotlin-number-kotlin-sansan


Sample

Dagger2など上記のライブラリを導入したプロジェクトのサンプルはGitHub上に公開しています。

https://github.com/satorufujiwara/kotlin-android-example


Kotlin Android Snippets

KotlinでAndroidを開発していてKotlinらしくAndroidを便利に開発できたと感じたコードをいくつか紹介します。


Nullable

var str : String? = null

fun main(){
val str = str ?: return run {
//if str is null
}
//str is not null
Timber.d("str length = ${str.length}")
}

var propertyはSmartCastsが使えませんがlocal variableに代入してあげることでNullable propertyをNon-Null variable に変えることが出来ます。上の例ではnull時に早期リターンしています。


Property Delegate


MainFragment.kt

private val pagerAdapter: MainPagerAdapter by lazy { MainPagerAdapter(activity) }


上記はContext(Activity)が必要なPagerAdapterFragment内に記述されています。Property Delegateは初回呼び出し時に初期化されるため、この例の場合はFragment.onViewCreated内の以下のタイミングで初期化されています。


MainFragment.kt_onViewCreated

viewPager.adapter = pagerAdapter


pagerAdaptervalかつNon-Nullに保つことができます。


apply

applyはkotlinのscope functionsの一つです。scope functionsについてはKotlin スコープ関数 用途まとめが非常に参考になります。

class MainFragment : Fragment() {

companion object {
@JvmStatic fun newInstance(dto: Dto) = MainFragment().apply {
arguments = Bundle().apply { putParcelable(EXTRA_DTO, dto) }
}
private const val EXTRA_DTO = "extra_dto"
}
private val dto: Dto by lazy { arguments.getParcelable<Dto>(EXTRA_DTO) }

fun createIntent(url: String) = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply {

addFlags(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) Intent.FLAG_ACTIVITY_NEW_DOCUMENT
else Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET
)
}

Androidではインスタンスを作って値を与えるためにわざわざ変数を用意する場面が多いですが、その時にapplyを使えば変数を用意する必要がなくなりすっきり書けます。

FragmentBundleIntentAppCompatDialogなどに用いています。

fun View.fitLongestWidth(rate: Float) {

val metrics = DisplayMetrics()
(context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay.getMetrics(metrics)
layoutParams = layoutParams.apply { width = (Math.max(metrics.widthPixels, metrics.heightPixels) * rate).toInt() }
}

LayoutParamsのようにgetして値を変更してから再びsetするものにもapplyは使えます。


Property

Kotlinはpropertyのsetter,getterに処理を記述することができます。

private var isAvailableRefresh = false

set(value) {
if (value) {
refreshButton.toVisible()
if (value != field) refreshButton.scaleIn()
} else {
refreshButton.toGone()
}
field = value
}

toVisibile()などはViewのExtension Functionsです。

上記の例ではisAvailableRefreshのフラグの変更とともにrefreshButtonのvisibilityを変更しています。その際value = true && value != fieldの時、つまりfalseからtrue値が変更された時にのみscaleIn()でアニメーションをさせています。

setter,getterに処理を書くことによってpropertyごとに必要な処理がブロック化され、可読性が向上します。

class CustomView : FrameLayout {

var dto: Dto? = null
set(value) {
field = value
value ?: return toInvisible()
// bind dto to views.
}

CustomViewにオブジェクトを与えて、それによってViewの表示を変える時にも使っています。


Conclusion


Cons

Kotlinを採用するにあたり、多くの罠を踏む覚悟をしましたが、大きくつまづいた場所は上記のkaptだけだったように思います。

バージョンアップ時には言語仕様が変更され、コードの変更が必要でしたが、CodeCleanupを利用するなどすればそこまで苦ではありませんでした。現在は1.0.0-betaが出ているので以前ほどの大きな言語仕様の変更はなさそうに思います。

また、multidexを採用できないプロジェクトにとってはメソッド数は問題になるかもしれません。しかし、Kotlinを採用するためのメソッド数は誤差かと考えています。

参考 : AndroidをKotlinとRetrolamda+Lombokとで作る場合の比較


Pros

Kotlinを採用して最高でした。もうJavaには戻れないというのが正直なところです。

利点は既に例を挙げたとおりですが、それ以外も簡単に列挙しておきます。


  • Nullable / Non-Null


    • NullPointerExceptionを長らく見てません。




  • when


    • 特に文字列比較。



  • ラムダ式 & 拡張関数


    • RxJava/RxBindingとの相性が抜群です。拡張関数と同様のことをJavaでやろうとするとUtil地獄になると思います。




  • Data Class



    • toString()が実装されているのが地味に嬉しい。




  • Named Arguments


    • Data Classのコンストラクタでは必ず使っています。




最後に

8ヶ月間のKotlinでの開発でチームメンバーとKotlinについて議論をしていく中で、最初に比べるとずいぶんとKotlinらしくコードも洗練されてきたように思います。また、Kotlinの1.0.0ももうすぐ出ると思うので、これからもいろいろと試行錯誤しながらKotlinで開発していくつもりです。

今もしくはこれからKotlinでAndroid開発をする方がいたら、KotlinSlack日本語や勉強会( AndroidでKotlin勉強会shibuya.apk )などでぜひ情報交換をしましょう!

今年も一年お疲れさまでした。


Links

最後にリンク集を貼っておきます。