Our team are building a project with benefits from Android Jetpack.
There are demo code showing the question we are facing. These code can be found at https://github.com/viseator/TestRoomLivedata
I create a UserDao
:
@Dao
interface UserDao {
@Query("SELECT * FROM user WHERE uid = :uid LIMIT 1")
fun findUserById(uid: String?): Single<User>
@Query("SELECT * FROM user WHERE state = 1 LIMIT 1")
fun findLoginUserWithObserve(): LiveData<User>
@Query("SELECT * FROM user WHERE state =1 LIMIT 1")
fun findLoginUser(): Single<User>
@Update
fun update(vararg user: User)
}
I also created a kotlin object to manage user's state.
I'm observing the livedata returned by findLoginUserWithObserve()
to get notified when login user changed:
object AccountManager {
private const val DATA_BASE_NAME = "users"
val TAG = "AccountManager"
fun init(context: Application) {
sDb = databaseBuilder(context, UserDataBase::class.java, DATA_BASE_NAME).build()
sDao = sDb.userDao()
sDao.findLoginUserWithObserve().observeForever {
Log.d(TAG, "notified: $it")
}
}
private lateinit var sDb: UserDataBase
private lateinit var sDao: UserDao
fun findLoginUserWithObserve() = sDao.findLoginUserWithObserve()
fun logoutFlowable(): Single<Boolean> = sDao.findLoginUser().subscribeOn(
Schedulers.io()).map { user ->
user.state = User.STATE_NOT_LOGIN
sDao.update(user)
true
}
fun login(user: User) = logoutFlowable().subscribe({ doLogin(user) }, { doLogin(user) })
private fun doLogin(user: User) = sDao.findUserById(user.uid).subscribeOn(
Schedulers.io()).subscribe({ origin ->
origin.userName = user.userName
origin.state = User.STATE_HAVE_LOGIN
sDao.update(origin)
user.state = User.STATE_HAVE_LOGIN
}, {
user.state = User.STATE_HAVE_LOGIN
sDao.insert(user)
})
}
I initialize the AccountManager
in the Applicaiton
by calling it's init
method and create a demo activity:
class MainActivity : AppCompatActivity() {
private var i = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
AccountManager.login(User().apply {
userName = "user1"
uid = System.currentTimeMillis().toString()
})
button.setOnClickListener {
AccountManager.login(User().apply {
userName = "user${i++}"
uid = System.currentTimeMillis().toString()
})
}
}
}
I suppose once AccountManager.login()
be called, I will get notified and it will print a log message. But we found that we won't be notified anymore after GC. (We trigger GC by Android Studio Profiler)
After exploring the UserDao_Impl
class generated by room, we found it create a observer and link with database by calling addWeakObserver()
:
@Override
public LiveData<User> findLoginUserWithObserve() {
final String _sql = "SELECT * FROM user WHERE state = 1 LIMIT 1";
final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
return new ComputableLiveData<User>(__db.getQueryExecutor()) {
private Observer _observer;
@Override
protected User compute() {
if (_observer == null) {
_observer = new Observer("user") {
@Override
public void onInvalidated(@NonNull Set<String> tables) {
invalidate();
}
};
__db.getInvalidationTracker().addWeakObserver(_observer);
}
So We wonder why room using WeakObserver
here, which makes livedata returned by room unreliable?
PS: We are using Flowable
to emit livedata in it's onNext()
now to work around this, onNext()
will be triggered every time as expected.
After post this issue to the google issue tracker(https://issuetracker.google.com/issues/114833188), I got reply:
We don't want to leak the LiveData if it is not used anymore. We could technically keep adding and removing the observer when LiveData is in use / not in use; but that might mean missing some events that happens when LiveData is inactive. We used to do that in the initial prototypes but became harder to maintain. You should keep a reference to the LiveData to keep using it. This is the patter we use in all examples.
So just keep a reference to the livedata returned by room instead of just observing it, everything works well now.