こんにちは。 @ryugoo_ です。先日、 Google から Android KTX というライブラリが公開されました。チャットワークの Android アプリでも早速利用を開始しています。
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 には show
や showNow
メソッドが用意されていますが、これらのメソッドはそれぞれ内部で 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#onSaveInstanceState
で Fragment の状態を 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
な拡張関数を定義して使うなど、チーム開発において混乱を生じさせないようにキチンと話し合いをして使いましょう。
拡張関数は、用法用量を守って適切に:)