Javaの匿名インナークラスの代わりをラムダ式で書くとき、別インスタンスになると思い込んでいて、インスタンス単位での管理で問題となったメモです。
Android Architecture Components の LiveData を使って、1つの LiveData が管理しているデータソースを別の LiveData としても提供するということを実現するために、空の Observer を LiveData.observer に渡す以下のような実装でハマりました。
サンプルコード
// observeされている間だけ1秒おきにインクリメントしていくカウンターLiveData
class Counter : LiveData<Int>() {
private var timer: Timer? = null
// アウタークラスのカウンターに依存しつつ、偶数だけを配信するLiveData
var oddCounter: MutableLiveData<Int> = object : MutableLiveData<Int>() {
override fun observe(owner: LifecycleOwner, observer: Observer<Int>) {
super.observe(owner, observer)
// アウタークラスにカウンターの実体があるため、
// そちらも observe することで active にし、カウンターを開始する
this@Counter.observe(owner, Observer<Int> { })
}
}
override fun onActive() {
val task = object : TimerTask() {
override fun run() {
var nextCount = (value ?: 0) + 1
postValue(nextCount)
if (nextCount % 2 != 0) {
oddCounter.postValue(nextCount)
}
}
}
timer = Timer()
timer?.scheduleAtFixedRate(task, 0, 1000)
}
override fun onInactive() {
timer?.cancel()
}
}
class MainKotlinActivity : AppCompatActivity() {
private val counter = Counter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 複数のownerがobserveする実装のサンプルで、アクティビティとプロセスがobserveするコード
counter.oddCounter.observe(this, Observer<Int> { value ->
value?.let { println("activity got $it") }
})
counter.oddCounter.observe(ProcessLifecycleOwner.get(), Observer<Int> { value ->
value?.let { println("process got $it") }
})
}
}
これを実行すると、 this@Counter.observe(owner, Observer<Int> {})
というコードが、異なる owner
インスタンスに対して2回呼び出されますが、2回目で java.lang.IllegalArgumentException: Cannot add the same observer with different lifecycles
という例外が発生します。
原因は、次の通り。
-
Observer<Int> { }
は、何度実行しても(同じクラス内では)同じインスタンスを返す -
LiveData.observe
の[仕様] (https://developer.android.com/reference/androidx/lifecycle/LiveData.html#observe(androidx.lifecycle.LifecycleOwner,%2520androidx.lifecycle.Observer%3C?%2520super%2520T%3E)) で、1つのobserverを異なるownerでは使えない
おそらく、 Observer<Int> { }
だとクロージャが呼び出し元のスコープに依存しておらず固定であるため、1つのインスタンスを流用しているんだろう。
実際、 Observer<Int> { }
を Observer<Int> { print(owner.toString()) }
とする、つまり呼び出しのたびに変わる owner
に依存する実装になっていれば、別インスタンスを返しました。
そもそもバイトコードとしては匿名インナークラスとラムダ式だと全く異なるんだろうと思い、 http://www.ne.jp/asahi/hishidama/home/tech/java/lambda.html#h_invokedynamic このへんを読んでフムフム。
今回、空実装のObserverインスタンスを作りたくて Observer<Int> { }
と書いてしまっていたわけですが、IDE の出す warning に従ったらそうなってしまったという背景があります。実際、以下のようにいくつかの方法で書けるわけですが。。
もっともオーソドックスなやり方: Java の匿名インナークラス。
Observer<Integer> nullObserver = new Observer<Integer>() {
@Override
public void onChanged(@Nullable Integer integer) {
}
};
observe(owner, nullObserver);
でもこれは、Android Studio に「can be replaced with lambda」と提案され、それに従うと以下のようにラムダ式を使うコードに変換されます。
Observer<Integer> nullObserver = value -> {};
observe(owner, nullObserver);
Kotlinでも同様。
val nullObserver = object : Observer<Int> {
override fun onChanged(t: Int?) {
}
}
observe(owner, nullObserver);
val nullObserver = Observer<Int> { }
observe(owner, nullObserver);
それぞれの違いを意識して使い分けないといかんなと、再認識しました。
単にもとの匿名インナークラスやオブジェクト式のままにしておい場合、別の人がIDEの提案に従って再度同じ変更をしてしまう可能性もあるため、以下のように専用のクラスをインスタンス化するようにしました。
private class NullObserver<T> : Observer<T> {
override fun onChanged(t: T?) {
}
}
observe(owner, NullObserver<Int>())
いまqiita記事をまとめながら、ソースコードコメントでなにか補足しておいても良かったのかなとも思いました。
おわり。
ちなみに、ここに書いたサンプルコードだと Transformations を使えばこんなややこしい空observeみたいなことをしなくてすむ気もしますが、実際のコードはもう少し込み入った事情があったりはします。