Room DB 와 RoomTrackingLiveData
Room 의 구성요소인 Dao 에서 getAll() 과 같은 메서드에서는 리턴타입을 LiveData 형태로하면 연결된 테이블의 데이터가 변경되면 옵저버들에게 통지가 가도록 만들 수 있습니다. 그런데 생각을 해보다 좀 이상한 부분이 있었습니다. LiveData 는 관찰 가능한 데이터 홀더 클래스로 값이 변경될 때 setValue 또는 postValue 메서드를 호출하여 옵저버들의 onChanged 메서드를 호출하여 통지를 해주는 방식으로 동작합니다. 그렇게 하려면 데이터소스에서 데이터를 읽어온 후 LiveData 에 값을 설정하는 메서드에서 setValue 를 호출해줘야지만 옵저버들에게 통지가 가게됩니다. 즉 아래와 같이 Activity 또는 Fragment 에서 먼저 LiveData 옵저버를 등록한 후 서버에서 데이터를 받아오는 메서드를 호출하여 LiveData 의 데이터가 갱신되도록 하여 통지를 받는 방식으로 구현됩니다.
//ViewModel class
private val _data = MutableLiveData<List<Data>>()
val data: LiveData<List<Data>> get() = _data
fun loadData() {
viewModelScope.launch {
val response = RemoteRepository.load()
_data.value = response
}
}
//Activity or Fragment
fun initObservers() {
viewModel.data.observe(this) { data ->
//View Update
}
viewModel.loadData()
}
하지만 Room 에서 LiveData 를 사용할 때는 데이터가 변경되었을 때(Table 이 변경되었을 때) 명시적으로 setValue 를 호출하는 곳이 보이지 않았습니다. 그래서 도대체 어떻게 테이블이 변경되었을 때 LiveData 에서 관찰중인 옵저버들에게 통지를 해줄 수 있는지 도저히 이해가 가지 않았습니다. 그래서 생성된 DaoImpl 클래스를 확인해보니 반환타입을 LiveData 로 하였을 때 그냥 LiveData 를 반환하는 것이 아닌 createLiveData 메서드를 호출하여 LiveData 를 상속받은 RoomTrackingLiveData 라는 클래스의 객체를 반환하는 것을 확인하였습니다. RoomTrackingLiveData 클래스는 Entity Table 내의 데이터 변경을 관찰하여 데이터가 변경되었을 때 변경된 리스트를 읽어와서 postValue 메서드를 호출하는 방식으로 동작하였습니다. 아래의 코드는 RoomTrackingLiveData 의 동작 방식을 간략히 발췌한 것입니다.
//Dao Impl 클래스
@Override
public LiveData<List<UserEntity>> getAll() {
final String _sql = "SELECT * FROM userentity";
final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
return __db.getInvalidationTracker().createLiveData(new String[]{"userentity"}, false, new Callable<List<UserEntity>>() {
//관찰중인 테이블이 변경되었을 때 call 메서드가 호출되어 변경된 데이터를 모두 읽어옵니다.
@Override
public List<UserEntity> call() throws Exception {
final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
try {
final int _cursorIndexOfUid = CursorUtil.getColumnIndexOrThrow(_cursor, "uid");
final int _cursorIndexOfUserName = CursorUtil.getColumnIndexOrThrow(_cursor, "userName");
final int _cursorIndexOfAge = CursorUtil.getColumnIndexOrThrow(_cursor, "age");
final List<UserEntity> _result = new ArrayList<UserEntity>(_cursor.getCount());
while(_cursor.moveToNext()) {
final UserEntity _item;
final String _tmpUid;
if (_cursor.isNull(_cursorIndexOfUid)) {
_tmpUid = null;
} else {
_tmpUid = _cursor.getString(_cursorIndexOfUid);
}
final String _tmpUserName;
if (_cursor.isNull(_cursorIndexOfUserName)) {
_tmpUserName = null;
} else {
_tmpUserName = _cursor.getString(_cursorIndexOfUserName);
}
final int _tmpAge;
_tmpAge = _cursor.getInt(_cursorIndexOfAge);
_item = new UserEntity(_tmpUid,_tmpUserName,_tmpAge);
_result.add(_item);
}
return _result;
} finally {
_cursor.close();
}
}
@Override
protected void finalize() {
_statement.release();
}
});
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public <T> LiveData<T> createLiveData(String[] tableNames, boolean inTransaction,
Callable<T> computeFunction) {
//LiveData 를 상속받은 RoomTrackingLiveData 객체를 생성하여 반환해줍니다.
return mInvalidationLiveDataContainer.create(
validateAndResolveTableNames(tableNames), inTransaction, computeFunction);
}
//RoomTrackingLiveData 클래스
@SuppressLint("RestrictedApi")
RoomTrackingLiveData(
RoomDatabase database,
InvalidationLiveDataContainer container,
boolean inTransaction,
Callable<T> computeFunction,
String[] tableNames) {
mDatabase = database;
mInTransaction = inTransaction;
mComputeFunction = computeFunction;
mContainer = container;
mObserver = new InvalidationTracker.Observer(tableNames) {
@Override
public void onInvalidated(@NonNull Set<String> tables) {
ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
}
};
}
@SuppressWarnings("WeakerAccess")
final Runnable mInvalidationRunnable = new Runnable() {
@MainThread
@Override
public void run() {
boolean isActive = hasActiveObservers();
if (mInvalid.compareAndSet(false, true)) {
if (isActive) {
getQueryExecutor().execute(mRefreshRunnable);
}
}
}
};
final Runnable mRefreshRunnable = new Runnable() {
@WorkerThread
@Override
public void run() {
if (mRegisteredObserver.compareAndSet(false, true)) {
mDatabase.getInvalidationTracker().addWeakObserver(mObserver);
}
boolean computed;
do {
computed = false;
// compute can happen only in 1 thread but no reason to lock others.
if (mComputing.compareAndSet(false, true)) {
// as long as it is invalid, keep computing.
try {
T value = null;
while (mInvalid.compareAndSet(true, false)) {
computed = true;
try {
//public LiveData<List<UserEntity>> getAll() 메서드에서 등록한 Callable 객체의 call 메서드를 호출합니다.
value = mComputeFunction.call();
} catch (Exception e) {
throw new RuntimeException("Exception while computing database"
+ " live data.", e);
}
}
if (computed) {
//call 메서드에서 가져온 변경된 데이터들을 postValue 를 통해 LiveData 에 바인딩하여 옵저버들에게 통지가 가도록 합니다.
postValue(value);
}
} finally {
// release compute lock
mComputing.set(false);
}
}
} while (computed && mInvalid.get());
}
};