Firebase Database の addListenerForSingleValueEvent に渡すリスナーが Activity や Fragment より長生きして困った

一回だけ値を取りに行ってくれるやつなんですが、すっかり勘違いしてそのまま使っていたら Activity や Fragment が destroy された後にもコールバックを呼んできて困りました。

ホラー画像

詳細は省きますが、たとえばこういう Fragment です。

class Main2Fragment : Fragment() {
lateinit var testRef: DatabaseReference
lateinit var testValueEventListener: TestValueEventListener

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

testRef = FirebaseDatabase.getInstance().reference.child("testd")
testValueEventListener = TestValueEventListener(this)
}

class TestValueEventListener(val fragment: Fragment): ValueEventListener {
override fun onCancelled(error: DatabaseError?) {
println("onCancelled")
}

override fun onDataChange(dataSnapshot: DataSnapshot?) {
println("onDataChange")

// Crash!
fragment.activity.title = "No!"
}
}

override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

testRef.addValueEventListener(testValueEventListener)
}
}

Firebase Database の返事が来る前に、 Fragment ごと別の Activity に遷移するような処理が起きると怪しいです。実際に仕事で作っているアプリだと、戻るボタン連打してるととっても怪しくなりがちでした。

このサンプルアプリのコミットをビルドし、「アクティビティを保持しない」設定で走らせると間違いなくクラッシュします。

Firebase のいつものおじさんこと Puffelen さんも「コールバックが1度も呼ばれていない状態だと remove されていないけど、レアだよね」と言っていてそりゃレアですけど、一回実アプリでクラッシュを見ちゃうと気になってしまいます。

じゃあ onStop とかで removeListenerしたらいいよねって思うのですが、これができないんです。普通の addValueEventListener がこうなのに対し

public ValueEventListener addValueEventListener(ValueEventListener var1) {
this.zzb((qi)(new to(this.zzbYY, var1, this.zzFr())));
return var1;
}

addListenerForSingleValueEvent はこう。

public void addListenerForSingleValueEvent(ValueEventListener var1) {
this.zzb((qi)(new to(this.zzbYY, new zzp(this, var1), this.zzFr())));
}

戻りが void です。ちなみにこの zzp ってのが

final class zzp implements ValueEventListener {
zzp(Query var1, ValueEventListener var2) {
this.zzbZm = var1;
this.zzbZl = var2;
super();
}

public final void onDataChange(DataSnapshot var1) {
this.zzbZm.removeEventListener(this);
this.zzbZl.onDataChange(var1);
}

public final void onCancelled(DatabaseError var1) {
this.zzbZl.onCancelled(var1);
}
}

これなので、 addListenerForSingleValueEvent に渡した ValueEventListener のインスタンスを removeListener に渡してもしようがないという……。

結局、この zzp をまねた extension を書くことにしました。

private class Listener(val query: Query, val listener: ValueEventListener ): ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
query.removeEventListener(this)
listener.onDataChange(dataSnapshot)
}

override fun onCancelled(databaseError: DatabaseError) {
listener.onCancelled(databaseError)
}
}

fun DatabaseReference.addSingleValueEventListener(listener: ValueEventListener): ValueEventListener {
return addValueEventListener(Listener(this, listener))
}

ただ return するようにしただけです。 Firebase のオープンソースリポジトリってじわじわ追加されていて、まだ Android SDK が無いように見えます。出てきたらこれくらい追加してって、あのおじさんに伝えたいな!

おじさん(右)
One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.