Kotlin

自分なりのKotlin Coding Tips

食べログのアプリDevOpsチームでAndroidエンジニアやっている @sadashi です。

最近はアプリアーキテクチャ設計やコーディング規約を考えたりしています。

食べログ Advent Calendar 2018 6日目の記事ではKotlinでのCoding Tipsについて書きたいと思います。


はじめに

Kotlinでコーディングする際に気をつけた方がいいところであったり、ちょっとしたTipsを書いていきたいと思います。

私自身Androidメインなので、内容がAndroidになっているところもありますが、その点はご了承ください。


環境

IDEはAndroidStudioを使っていて、Kotlinは1.3.10をベースに考えています。

あと、単純にKotlinを書いてみたいならKotlin Playgroundを使うとお手軽で良いですよ!


Tips


Lint

lintは必ず使いましょう。自分も気づかなかった不具合を見つけてくれるかもしれません。

また合わせてformatterとしても使うことをお勧めします。コーディングの効率が上がり、書き方も統一されるので、可読性も高くなります。

Kotlinに対応しているものは以下のようなものがあります。


トップレベル関数, 拡張関数

頻繁には使わない方が良いと思います。ルールなしで使うとカオスになります。

使いたい場合はスコープを限定するなどを検討すると良いかもしれません。

私としては、明らかに汎用性が高いようなものはあっても良いと思っています。

Androidですが一例としては以下のようなものです。

// Picassoを利用した、URLからImageViewへの反映

fun ImageView.imageUrl(url: String?) {
if (url.isNullOrEmpty()) {
setImageDrawable(null)
return
}
Picasso.with(context).load(url).into(this)
}

// ViewGroupのinflate
fun ViewGroup.inflate(@LayoutRes layoutRes: Int): View {
return LayoutInflater.from(context).inflate(layoutRes, this, false)
}


型の明記

型は基本的には書いた方がわかりやすいと思います。

省略するとしたら、プリミティブな型かコンストラクタを直接呼ぶ場合のみがおすすめです。

// NG

val huga = factory.create() // 型がよくわからない
val hugaInterface = HugaImpl() // 抽象化したいのに略すと実装したクラスになってしまう

// OK
val primitiveInt = 0
val primitiveString = ""
val directConstractor = Hoge()
val huga: Huga = factory.create()
val hugaInterface: HugaInterface = HugaImple()


Null Check

nullチェックしてearly returnする場合はエルビス演算子(?:)を使うと良いです

fun dummy(hoge: Hoge?) {

val huga = hoge ?: return
// ...
}

何か処理をさせたい場合はスコープ関数(run or let)を使います

fun dummy(hoge: Hoge?) {

val huga = hoge ?: run {
// do something
return
}

// ...
}

逆にnullではない場合に処理させたければこんな感じです。

fun dummy(hoge: Hoge?) {

hoge?.let {
it.hogehoge()
}

// or

hoge?.let {
// smart cast
hoge.hogehoge()
}
}

こういった場合はスマートキャストの方がわかりやすいと思います。


!! (not-null assertion operator, force unwrap)

nullに対して行うと即クラッシュしてしまうので、基本的には利用しないようにした方が良いと思います。

early returnを意識して、smart castをするのが一番わかりやすいです。

どうしても必要なときはrequireNotNullでメッセージをつけると、意思が伝わります。

val a: Int? = null

val b: Int = requireNotNull(a, {"ここでnullになってはいけないよ"}) // 第二引数の文字列は任意


スコープ関数(let,run,apply,also

ここは好みがでるところかもしれません。

個人的にはalsoletのみを使います。

理由としてはapply/runはレシーバでthisをとりますが、クラス内のメンバに同じ名前が存在すると意図しない挙動となるためです。

// NGパターン

class Hoge {
var text = ""
}

fun init() {
var text = ""
val hoge = Hoge().apply {
text = "hoge"
}
hoge.text // "hoge"になっていない
}

thisをつければいいんじゃね?って意見もあると思いますが、それだとitつけるのと変わらないので、明示的に使わないという方向に倒しているだけです。

ただし、以下の例のように、初期化時の設定はapplyの方が意味がわかりやすいとか、エルビス演算子(?:)の後の処理はrunの方が直感的とか、人によって意見は色々あると思いますが、この辺りはどういったときに問題が起こるかを把握した上で議論し、しっかりと「共通認識」を持つことが大事かなと思っています。

// initialize

val intent = Intent().apply { // <- 自分ルールだとalso
it.putExtra("key1", "value1")
it.putExtra("key2", "value2")
it.putExtra("key3", "value3")
}

// null check
val hoge = str ?: run { // <- 自分ルールだとlet
// なんか処理する
return
}


Singlton

DaggerなどのDIライブラリを利用されている方はあまり使わないかもしれませんが、シングルトンは以下のように書くといいです。

// Singleton

class SingeltonHoge {
companion object {
private var INSTANCE: SingeltonHoge? = null

fun getInstance(): SingeltonHoge {
return INSTANCE ?: SingeltonHoge().also { INSTANCE = it }
}
}
}

Android StudioやIntelliJならLive Templateに以下を登録しておくと便利です。

companion object {

private var INSTANCE: $CLASS$? = null

fun getInstance(): $CLASS$ {
return INSTANCE ?: $CLASS$().also { INSTANCE = it }
}
}


Custom Setter

Custom Setterは変数の委譲のみに留め、他の用途ではあまり使わない方が良いかと思います。

private val user = User()

var userName: String
get() = user.name
set(value) {
user.name = value
}

もし、以下のように設定ついでに更新や通知処理などを行うと以下の問題が起こります。


  • 値の更新だけをすることができない

  • 他の値と一緒に更新することができないなど

// NG

private val user = User()
var userName: String
get() = user.name
set(value) {
user.name = value
update()
notify()
}


Custom Getter

処理に時間がかかったり、何か状態を変えるような副作用がある場合には、Custom Getterを使わずに通常の関数を利用した方がよいでしょう。

処理に時間がかからず、副作用も特にない場合はCustom Getterを使っても問題ないと思います。

この辺の話は公式のKotlinコーディング規約(Functions vs Properties)にも記載があります。


メンバ変数の初期化方法

メンバ変数を初期化する方法として以下の4種類があります。



  • lazy


    • 最初にアクセスがあった時に値が計算されて、それ以降同じ値を返す

    • 不変のため、valでしか宣言できない




  • custom getter


    • アクセスがあるたびに計算される



  • 通常の代入


    • インスタンス生成時に計算される




  • lateinit


    • 初期化されていない状態をもち、どこかで初期化される前提となる宣言

    • 初期化されていない状態でアクセスすると例外が発生する

    • 必ず初期化以外の代入が行われる前提なのでvarでしか宣言できない


    • Nullableは認めない



これらを理解せずに曖昧に使ってしまうと痛い目にあうので気をつけましょう。

使う場合には以下のような観点でまとめると良いと思います。



  • lazy


    • 初期化時には値が確定しない、不変




  • custom getter


    • 状態が変わると値も変わる



  • 通常の代入


    • 初期化時に値が確定、不変




  • lateinit


    • 初期化時には値が確定しない、利用シーンは要検討(後述します)



不変でない値についてはcustom getterか通常の代入になるかと思います。

lateinitについては後述するルール次第になります。


lateinit

lateinitは利用箇所をルール化することをオススメします。

例えば以下のようなケースです。



  • Activity/FragmentなどのonCreate()などで確実に代入するメンバ変数


  • DaggerなどのDIライブラリを利用してInjectionするメンバ変数

lateinitの変数が初期化されているかチェックするisInitalizedがありますが、これを使うということは未初期化の状態を管理しなければならないため、Nullableで初期値にnullを代入していることと変わりません。

上記のようなルール外では、Nullableを利用した方が混乱が少なく済むでしょう。


lazylateinitの違い

どちらも遅延初期化という点は同じですが、動作は大きくことなります。

初期化のタイミング
var / val
Nullable

lazy
初回アクセスされたとき
val
OK

lateinit
明示的に初期化する
var
NG

注意点としてはViewlazyを使うとFragmentで渡すViewが更新されないなどの不具合に繋がるため、Viewではlazyではなくlateinitを使ってください。


最後に

ここにまとめたのは色々な情報収集をした上で、自分なりに整理したものですので、まったく違ったご意見もあるかとは思います。

そういったところがあれば是非コメントいただけると幸いです。

明日の7日目は@weakbosonさんより「Rails の trusted proxy めんどいところとワークアラウンド」です。お楽しみに!