Kotlinアンチパターン
Naoto Nakazato @DroidKaigi2018
自己紹介
● Naoto Nakazato
○ Android開発歴 7年くらい
● Recruit Lifestyle
○ 2017年6月〜
○ HOT PEPPER Beauty
● アカウント
○ Twitter: @oxsoft
○ Facebook: naoto.nakazato
○ GitHub: oxsoft
○ Qiita: oxsoft
KotlinとAndroid
普段のアプリ開発でKotlin使ってますか?
KotlinとAndroid
2017年5月にGoogle I/Oで公式サポートが発表されたKotlin
Javaよりもシンプルで安全なのは間違いない!けど……
● 色々な書き方があって悩む
● 使い方次第ではバグが発生するかも
KotlinとAndroid
公式ドキュメントに「文法」は書いてあるが、
それ以外に「アンチパターン」のようなものがありそう
このセッションでは、30分で10個紹介します!
今日お話しすること
その前に……
ホットペッパービューティー
ホットペッパービューティーは、
国内最大級のサロン検索・予約サービス
ヘアサロン以外にも、
ネイル、まつげ、リラクゼーション、エステがあります
Androidアプリ
2010年 Androidアプリリリース
徐々に秘伝のタレ化
2016年05月 フルリニューアルを決意
2017年02月 フルKotlinに方針転換
2017年12月 リニューアル版リリース
今日お話しすること
KotlinでAndroidアプリを開発して感じたアンチパターン
● リリース済みの巨大なアプリのフルリニューアル
● メンバーのAndroid開発経験が様々
● 最大11人という大人数のチーム開発
プロダクトや開発メンバーによって賛否両論ありそう
→ブースでは実際のソースコードを展示しているので、
 開発メンバーとディスカッションしに来てください!
今日お話しすること
今回はアンチパターンを10個話しますが、
それぞれ以下の4つの構成で話していこうと思います
● 言語機能の説明
● アンチパターン
● 何が良くないか
● 解決策の例
Kotlinを普段使っている人は、
「言語機能の説明」は聞かなくても良いかもしれないです
#01 lateinitとnull初期化
lateinitとは
Javaでは、初期値がない変数宣言は null などが入る
Kotlinでは、基本的に初期値を書かないといけない
TextView message; // nullが入る
var message: TextView // errorになる
var message: TextView? = null // OK
言語機能の説明
lateinitとは
Androidでは、onCreate以降で初期化するものが多い
実質NonNullなものが、全部Nullableになってしまう
言語機能の説明
var message: TextView? = null
fun clear() {
message!!.text = "" // 怪しい
message?.text = "" // 面倒
}
lateinitとは
そこでlateinitを使うと、宣言時に初期値を入れなくて良い
ただし、値を代入していない状態で値を参照すると
UninitializedPropertyAccessException が投げられる
lateinit var message: TextView // OK
言語機能の説明
lateinitの使いどころ
インスタンス作成時には値が定まらないが、
onCreateやonCreateViewで代入されるもの
例えば、
● findViewByIdしたView
● DataBindingのbinding
● DaggerなどのDIによってinjectされるもの
ただし、プリミティブ型やNullableには使えない
言語機能の説明
アンチパターン
通信後に得られる情報をlateinitにする
lateinit var profile: Profile
fun init() {
fetchProfile().subscribe { profile ->
this.profile = profile
}
}
何が良くないか
あらかじめリスナーをセットしている場合に、
通信中や通信エラー時にアクセスして、
UninitializedPropertyAccessException
button.setOnClickListener {
textView.text = profile.name
}
解決策の例
Nullableにして、
常に「情報未取得時どうするか」を考えさせる
var profile: Profile? = null
fun init() {
fetchProfile().subscribe { profile ->
this.profile = profile
}
}
解決策の例
onCreate / onCreateView で初期化可能
 →  lateinitで良さそう
それ以降で値が決まる
 → Nullableにする
     or
   メンバ変数にするのを避ける
isInitialized
Kotlin 1.2から、isInitializedが追加された
値が代入されたかどうかを確認することができる
lateinit var str: String
fun foo() {
val before = ::str.isInitialized // false
str = "hello"
val after = ::str.isInitialized // true
}
補足
isInitialized
元々はテストコードでの利用を想定して追加された
これを日常的に使うともはやnull安全じゃなくなるので、
プロダクションコードでの使用は避けたほうが良さそう
補足
参考:https://youtrack.jetbrains.com/issue/KT-9327
private lateinit var file: File
@After
fun tearDown() {
// ファイル作成前にfailするとエラーになる
file.delete()
}
#02 スコープ関数
スコープ関数
let/run/also/apply/withの5つがあるが、
withを除くとザックリ以下のように分類できる
it this
結果 let run
自身 also apply
戻り値
レシーバ
言語機能の説明
戻り値 = 自身.スコープ関数 {
レシーバ.method()
結果
}
使いどころ1
null関連の制御に便利
str?.let {
// strがnullじゃない場合
}
言語機能の説明
val r = str ?: run {
// strがnullの場合
}
使いどころ2
初期化処理をまとめる
言語機能の説明
val intent = Intent().apply {
putExtra("key", "value")
putExtra("key", "value")
putExtra("key", "value")
}
アンチパターン
apply内でプロパティアクセス形式の処理を書く
val button = Button(context)
button.text = "hello" // JavaのsetText(...)が呼ばれる
// ... buttonの設定が続く ...
val button = Button(context).apply {
text = "hello"
// ... buttonの設定が続く ...
}
↓ apply を使って処理をまとめよう!
何が良くないか
ローカル変数を定義すると、アクセス先が変わる
fun init() {
var text = "" // 後々この行が追加されると……
val button = Button(context).apply {
text = "hello"
}
button.text // "hello"にならない!
}
解決策の例1
apply 内ではプロパティアクセス形式を使わず、
通常の関数呼び出しにする
ただし、ローカル関数が定義されると同じ問題が起きる
val button = Button(context).apply {
setText("hello")
}
解決策の例2
this 必須というルールにする
alsoと似たような感じになってしまう
val button = Button(context).apply {
this.text = "hello"
}
解決策の例3
apply 禁止( also を使う)
我々のチームでは、let と also のみに限定
val button = Button(context).also {
it.text = "hello"
}
#03 Nullable と NonNull
Nullable と NonNull
Nullable / NonNull はKotlinの大きな魅力の1つ
言語機能の説明
val nullable: String? = null // OK
val nonNull: String = null // NG
nullable.length // NG
nullable!!.length // OK
nullable?.length // OK
nonNull.length // OK
アンチパターン1
Nullableのままデータを引き回す
data class User(
val id: Long? = null,
val name: String? = null,
val age: Int? = null
)
API
Domain
UI
Nullable
Nullable
何が良くないか1
Nullableのデータを引き回すと、
至る所でnullチェックやsafe callするハメになる
→ 実質的には条件分岐が増え、挙動把握が困難になる
→ 「 null って何だっけ?」を毎回考えることになる
return team?.user?.name?.length
アンチパターン2
全てNonNullにするために無効なデータを入れる
val response = ...
return User(
id = response.id ?: 0L,
name = response.name.orEmpty(),
age = response.age ?: 0
)
何が良くないか2
全てNonNullにするために無効なデータを入れると……
→ 無効なデータかどうかチェックするハメになる
→ チェックを忘れて表示崩れが起きる
if (user.name.isNotEmpty()) {
// ↑しんどい or 忘れる
}
解決策の例
「nullが何を表すか」で処理するレイヤを決める
● APIが返してくれないからnull → APIのレイヤで処理
● ユーザーが設定してないからnull → UIのレイヤで処理
● 自明じゃなくてレイヤをまたぐ → クラスで明確化
データ表現に困った時、安易にnullに頼らない
(例外を投げる、別のクラスにする、など)
#04 data class
data classとは
● equals/hashCodeやtoStringをよしなにoverride
● componentNやcopyなどのメソッドを生成
言語機能の説明
data class User(val name: String, val age: Int)
val alice = User("Alice", 27)
alice == User("Alice", 27) // true
alice.toString() // "User(name=Alice, age=27)"
val (name, age) = alice
val nextYear = alice.copy(age = 28)
アンチパターン
インスタンス生成用のメソッドで制約を保証したい
data class Range(val min: Int, val max: Int) {
companion object {
fun getRange(a: Int, b: Int): Range {
return Range(minOf(a, b), maxOf(a, b))
}
}
}
何が良くないか
data classにはcopyメソッドが生成される
→ 任意のデータを持つインスタンスが生成可能
val range = Range.getRange(3, 5)
val illegal = range.copy(max = 0)
↑ Range(min=3, max=0) となり、制約が壊れる
解決策の例
値がまとまっているからといってdata classにしない
「値の内部表現」=「クラスに期待される振る舞い」
→ 値のまとまりに名前を付けているだけ
→ data class
「値の内部表現」!=「クラスに期待される振る舞い」
→ 通常のクラスが良さそう
#05 interfaceとabstract class
interfaceのデフォルト実装
Kotlinではinterfaceにデフォルト実装ができる
Java8にもある機能だが、Androidで使うためには、
minSdkVersionを24以上にする必要がある
言語機能の説明
interface Downloadable {
fun download() {
// 共通の処理など
}
}
abstract classと何が違う?
interface abstract class
状態 持てない 持てる
継承元 Interfaceのみ classとInterface
多重継承 できる できない
default method class method
final できない できる
protected できない できる
言語機能の説明
アンチパターン
Javaの頃から言われていたアンチパターンで、
処理を共通化するためだけにBaseクラスを肥大化させる
abstract class BaseActivity : AppCompatActivity() {
protected fun showNetworkError() {
// エラー表示(エラーがないページもあるのに……)
}
}
何が良くないか
何千行レベルの親クラスになり、手に負えなくなる
解決策の例
Javaでは「継承よりも委譲」などと言われてきた
Kotlinではインタフェースのデフォルト実装も良さそう
interface NetworkErrorView {
// エラー用のViewとリロード処理は子クラスで用意
val networkErrorView: View
fun onClickReload()
fun showNetworkError() {
// リスナーのセット、エラーの表示などの共通処理
}
}
解決策の例
もちろん「Baseクラス=悪」ではない
abstractクラスが有効な例
● onCreateなどの継承部分で処理を共通化したい
● クラス外から呼ばれたくない
● 子クラスでoverrideされたくない
解決策の例
デフォルト実装が有効な例
● 前述以外で、処理を共通化させたい場合
● 多重継承したい場合
状態を持ちたい場合も、class delegationを使うとできる
class UserModel() : DefaultImpl by State() { ... }
class UserModel(s: State) : DefaultImpl by s { ... }
状態を持ちつつも、一部の処理は子クラスで実装させたい場合
interface IState {
var state: String
}
interface DefaultImpl : IState {
fun abstract(): String
fun default() { ... }
}
class State : IState {
override var state: String = ""
}
class UserModel : DefaultImpl, IState by State() {
override fun abstract(): String {
return "concrete"
}
}
IState
State
DefaultImpl
UserModel
状態をインタフェースとして切り出し、
状態を実装したクラスを用意する
#06 トップレベル関数と拡張関数
トップレベル関数
ファイルに直接関数を書くことができる
そうすると、どこからでも呼び出すことができる
fun throwOrLog(t: Throwable) {
// 開発はクラッシュ、本番はログ送信
}
fun foo() {
throwOrLog(e)
}
言語機能の説明
拡張関数
クラスに関数を追加する(ように見せる)ことができる
こちらも、どこからでも呼び出せるようになる
fun Any?.log() {
Log.d("DEBUG", this.toString())
}
fun foo() {
123.log()
"hello".log()
}
言語機能の説明
アンチパターン1
一般性がない・局所的にしか使わないメソッドを追加する
fun String.decorate() =
"_人人人人人_n" +
"> $this <n" +
" ̄Y^Y^Y^Y^Y ̄"
↑”decorate”の意味が広く、
 文字幅も考慮されていない
アンチパターン2
公式でありそうな名前なのに雑に実装する
fun String.isInteger() =
this.all { it in '0'..'9' }
↑空文字や負の数が考慮されていない
何が良くないか
拡張関数が便利なので乱発しがち
ライブラリよりもバグが混入している可能性が高い
JavaでUtilクラスを作りまくるのと同種の問題だが、
Kotlinだと通常の関数っぽく見えるので被害が大きい
開発メンバーが多いと特に問題になる
解決策の例
チーム内で拡張関数を作る基準・感覚を揃える
Interfaceのデフォルト実装でスコープを限定する
interface BookmarkableActivity {
fun bookmark() {
// 共通のブックマーク処理
}
fun String.toLabel() = "★ $this"
}
#07 lazyとcustom getter
lazyとは
最初にアクセスがあった時に値が計算され、
以降はその値を返す、delegated propertyの1つ
言語機能の説明
private val userId by lazy {
intent.getStringExtra("USER_ID")
}
通常の代入とcustom getter
● 通常の代入は、インスタンス生成時に計算される
● lazyは、最初にアクセスがあった時に計算される
● custom getterは、アクセスがあるたびに計算される
言語機能の説明
val isAdmin = userId == ADMIN_ID
val isAdmin by lazy { userId == ADMIN_ID }
val isAdmin get() = userId == ADMIN_ID
アンチパターン
通常の代入、lazy、custom getterを、曖昧に使い分ける
何が良くないか
クラッシュしたり、
古い値が返ってきたり、
無駄な計算が何度も走ったりする
private val button = findViewById<Button>(R.id.button)
val area by lazy { width * height }
val user: User get() = User(userId)
解決策の例
値の性質応じて、適切に使い分ける
クラス初期化時に値が確定し、不変 → 通常の代入
ある時点を過ぎるまで値が取れないが、不変 → lazy
状態が変わると値が変わる → custom getter
delegated property
プロパティのget(とset)を別のクラスに委譲できる
→ 普通の値のように見えて分かりやすい
● Intentのextra
● Fragmentのarguments
● savedInstanceState
● SharedPreferences
● FirebaseRemoteConfig   などに使える
ついでに紹介
var userId: String by MyClass()
delegated propertyの使用例
ついでに紹介
class Extra<out T> : ReadOnlyProperty<Activity, T> {
override fun getValue(
thisRef: Activity,
property: KProperty<*>
): T = thisRef.intent.extras.get(property.name) as T
}
fun Intent.put(
prop: KProperty1<*, String>, value: String
): Intent = this.putExtra(prop.name, value)
delegated propertyの使用例
ついでに紹介
class ProfileActivity : AppCompatActivity() {
val userId: String by Extra()
companion object {
fun createIntent(context: Context, userId: String): Intent {
return Intent(context, ProfileActivity::class.java)
.put(ProfileActivity::userId, userId)
}
}
}
かなりの部分を隠蔽することができる
参考:https://speakerdeck.com/sakuna63/kotlins-delegated-properties-x-android
#08 Fragmentとlazy
lazyとは(再掲)
最初にアクセスがあった時に値が計算され、
以降はその値をキャッシュして返す
private val userId by lazy {
intent.getStringExtra("USER_ID")
}
言語機能の説明
アンチパターン
(アンチパターンとして有名ですが)
FragmentのViewをlazyにする
val button by lazy {
view!!.findViewById<Button>(R.id.button)
}
何が良くないか
Fragmentでは同じインスタンスに対して
onCreateViewが再び呼ばれる
          ↓
lazyが初回のviewをずっと保持するため、
再びonCreateViewされても値が更新されない
出典:https://developer.android.com/guide/components/fragments.html
解決策の例
lateinit を使い、onCreateView で代入する
Data Bindingの場合も、binding自体はlateinitが良い
lateinit var button: Button
override fun onCreateView(...): View? {
val view = ...
button = view.findViewById(R.id.button)
return view
}
#09 custom getter
custom getterとは
valやvarの返り値は、カスタマイズすることができる
文法上は、引数がない関数は全てcustom getterにできる
var userId: String = ""
val isAdmin get() = userId == ADMIN_ID
言語機能の説明
アンチパターン1
値を取得する過程で副作用がある
private val itemCount: Int
get() {
if (recyclerView.adapter == null) {
initXXX() // 副作用
}
return recyclerView.adapter.itemCount
}
アンチパターン2
計算量が多い
private val total: Int
get() = countView(root)
fun countView(view: View): Int = if (view is ViewGroup) {
(0 until view.childCount).map {
countView(view.getChildAt(it))
}.sum()
} else {
1
}
何が良くないか
呼び出す側からは通常の変数と同じように見える
そのため、状態が変化したり、計算が重いことが予期できず、
予想外のバグやパフォーマンス低下を引き起こす
if (this.itemCount > 20) {
// ↑のように無邪気にアクセスしてしまう
}
解決策の例
副作用がなくて、計算量が少ない → custom getter
副作用があるか、計算量が多い  → 関数
上記の分類で実装していけば、関数名よりも明確に、
呼び出し側が副作用の有無や計算量を予想することができる
公式のリファレンスにも同様の記述がある
https://kotlinlang.org/docs/reference/coding-conventions.html#functions-vs-properties
#10 custom setter
custom setterとは
Kotlinでは通常のvarをカスタマイズすることができる
var text: String = ""
set(value) {
field = value
notifyDataSetChanged()
}
言語機能の説明
custom setterの使いどころ
● 値をセットするついでに更新処理などを行う
● 変数の委譲
言語機能の説明
変数の委譲
メンバー変数への委譲を簡単に書くことができる
(この場合はバッキングフィールドも生成されない)
private val owner = Person()
var ownerName: String
get() = owner.name
set(value) {
owner.name = value
}
言語機能の説明
※バッキングフィールド:Javaに変換された時に生成されるメンバ変数
変数の委譲
もちろん以下のように書くこともできるが、やや冗長
private val owner = Person()
var ownerName: String by object : ReadWriteProperty<Any, String> {
override fun getValue(thisRef: Any, property: KProperty<*>): String = owner.name
override fun setValue(thisRef: Any, property: KProperty<*>, value: String) {
owner.name = value
}
}
言語機能の説明
アンチパターン
常に必要とは限らない処理をしている
例:値が更新されたら、XXXを更新して、YYYに通知して、ZZZ
の値も更新して……
var person: Person = Person()
set(value) {
field = value
updateXXX()
notifyYYY()
ZZZ = value.name
}
何が良くないか
値だけ変えることができない(たとえクラス内からでも)
反映や計算を後でまとめてやるとかができない
値を代入しただけだと思ったら予想外の挙動になる
解決策の例
当たり前だけど、varの責務は値の保持にとどめておくべき
varは普通にprivateで持っておいて、更新用の関数を公開する
というJavaのパターンの方が混乱が少ないケースも多い
最後にもう1つ
最後にもう1つだけ……
Kotlinの機能を無理やり使わないように注意が必要かも
● operator overload
● infix
● tailrec etc...
キレイに書けると超気持ちいいが、
実際のプロダクトで有効な例はそんなに多くないはず
特にチーム開発では、分かりやすさも大事
まとめ
Kotlinは最高!だけど書き方の自由度が高い
→チーム開発の場合は、基準・感覚をこまめに話し合う
まずは今回話した10個のテーマで
ブースにて実際のソースコードを展示しているので、
開発メンバーとディスカッションしに来てください!

Kotlinアンチパターン