ChatWork Creator's Note

ビジネスチャット「チャットワーク」のエンジニアとデザイナーのブログです。

Kotlin を使った Android アプリ開発を拡張関数で加速させる!

こんにちは。 @ryugoo_ です。先日、 Google から Android KTX というライブラリが公開されました。チャットワークの Android アプリでも早速利用を開始しています。

developers-jp.googleblog.com

Android KTX は Kotlin で Android アプリを開発する場合に使える拡張関数群です。よく使う処理を簡潔に呼び出して使えるようになります。

// Before
sharedPreferences.edit()
  .putString("Hello", "World")
  .apply()

// After
sharedPreferences.edit {
  putString("Hello", "World")
}

もちろん Kotlin の拡張関数は自分達のコードでも定義して使うことができます。今回は私たちが実際に使っている拡張関数の一部を紹介したいと思います。

RxJava

onXXXIfNotDisposed

Observable#create で作った Observable が既に Dispose されている場合に、イベント通知またはエラー通知が行われないように毎回 isDisposed の値をチェックするのは冗長ですし、チェック漏れを起こす可能性があります。そこで、拡張関数でシンプルなラッパーを用意します。

拡張関数

fun <T> ObservableEmitter<T>.onNextIfNotDisposed(value: T) {
  if (!isDisposed) {
    onNext(value)
  }
}

fun <T> ObservableEmitter<T>.onErrorIfNotDisposed(throwable: Throwable) {
  if (!isDisposed) {
    onError(throwable)
  }
}

利用イメージ

fun rxString(): Observable<String> = Observable.create { emitter ->
  try {
    (0..9).forEach { i ->
      emitter.onNextIfNotDisposed(i.toString())
    }
  } catch (e: Exception) {
    emitter.onErrorIfNotDisposed(e)
  }
}

Single, Maybe, Completable に関しても同様にラップするための拡張関数を用意することで記述を簡単化でき、 Dispose チェックの漏れを無くすことができます。

DialogFragment

showAllowingStateLoss / showNowAllowingStateLoss

Fragment を使っていてしばらくすると出くわすのが IllegalStateException です。画面遷移や端末の回転時など Activity#onSaveInstanceState を通過した後に FragmentTransaction が対象の Fragment を操作してしまうと発生します。 DialogFragment には showshowNow メソッドが用意されていますが、これらのメソッドはそれぞれ内部で FragmentTransaction#add, FragmentTransaction#commitNow を使っているため、 IllegalStateException と出くわす確率が高まります。

これを回避するために FragmentTransaction#commit (Now) AllowingStateLoss を使う逃げ道を用意します。

拡張関数

fun DialogFragment.showAllowingStateLoss(fragmentManager: FragmentManager, tag: String) {
  fragmentManager.beginTransaction()
    .add(this, tag)
    .commitAllowingStateLoss()
}

fun DialogFragment.showNowAllowingStateLoss(fragmentManager: FragmentManager, tag: String) {
  fragmentManager.beginTransaction()
    .add(this, tag)
    .commitNowAllowingStateLoss()
}

利用イメージ

FileInfoDialog.newInstance(file)
  .showAllowingStateLoss(supportFragmentManager, tag)

逃げ道と書いたのは、 FragmentTransation#commit (Now) AllowingStateLoss が何を行っているのかを把握しないまま使うのは危険だからです。

ザックリと言うと Activity は Activity#onSaveInstanceStateFragment の状態を Parcelable にして保存し、 Activity#onCreate保存された状態を復元します。これが期待通りに動かなくなる危険性があります。

状態の保存、復元が不要な DialogFragment でだけ使うようにしましょう。また、そもそも DialogFragment である必要があるのかも検討しても良いでしょう。私たちのアプリでは Kotlin 化を進める段階で DialogFragment である必要がないと判断したものは AlertDialog に置き換えるなどの対応も行っています。

EditText

setAutoFillEnabled

Android DataBinding の BindingAdapter との合わせ技です。 Android 8.0 から Autofill 機能が使えるようになりましたが、 Autofill が動いて欲しくない EditText で無効にするためのものです。

拡張関数

@BindingAdapter("autoFillEnabled")
fun EditText.setAutoFillEnabled(enable: Boolean?) {
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    importantForAutofill = when (enable) {
      true -> View.IMPORTANT_FOR_AUTOFILL_AUTO
      else -> View.IMPORTANT_FOR_AUTOFILL_NO
    }

    customInsertionActionModeCallback = object : ActionMode.Callback {
      override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
          menu?.removeItem(android.R.id.autofill)
        }
        return true
      }

      override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean = false
      override fun onActionItemClicked(mode: ActionMode?, menu: MenuItem?): Boolean = false
      override fun onDestroyActionMode(mode: ActionMode?) {}
    }
  }
}

利用イメージ

binding.editText.setAutoFillEnabled(false)
<layout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto">

  <data>

    <variable
      name="isEnableAutoFill"
      type="boolean"/>
  </data>
~~~~~
  <EditText
    android:id="@+id/edit_text"
    app:autoFillEnabled="@{isEnableAutoFill}"/>
</layout>

こんな感じで使います。 Kotlin の拡張関数は

fun EditText.setAutoFillEnabled(enable: Boolean?) {}

の形で定義しますが、これは Java に展開すると

public static final void setAutoFillEnabled(@NonNull EditText editText, boolean enabled) {}

の形になります。その為、 BindingAdapter アノテーションを付与してバインディングを作ることができます。

乱用厳禁

Kotlin の拡張関数を利用すると、本来用意されていないメソッドをあたかも最初から存在していたかのように定義して呼び出すことができます。非常に便利でカッコイイ機能ではありますが、この機能を乱用すると何が Android SDK や Kotlin が提供しているメソッドで、何が自分達が定義した物なのかが追いづらくなってしまいます。

そのため、私たちは名前から何を行っているのか推測できないような拡張関数は作らないようにしています。単に特定の範囲で処理を共通化したいだけであれば、private メソッドを作るとか、 Kotlin ならばローカルな関数オブジェクト作って呼び出すとか方法は他にも考えられます。

どうしても特定の範囲の処理で、オブジェクトに対してのメソッド呼び出し (のように見える形) で使いたい場合には private な拡張関数を定義して使うなど、チーム開発において混乱を生じさせないようにキチンと話し合いをして使いましょう。

拡張関数は、用法用量を守って適切に:)