如果可能需要初始化/更新数据库,如何在Room上提供从数据库转换的liveData



背景

我正在创建一些SDK库,我想提供一些liveData作为函数的返回对象,这将允许监视DB上的数据。

问题

我不想透露DB中的真实对象及其字段(如ID),所以我想使用它们的转换。

所以,假设我有来自数据库的实时数据:

val dbLiveData = Database.getInstance(context).getSomeDao().getAllAsLiveData()

我所做的是让liveData在外部提供:

val resultLiveData: LiveData<List<SomeClass>> = Transformations.map(
dbLiveData) { data ->
data.map { SomeClass(it) }
}

这个效果很好。

然而,问题是第一行(获取dbLiveData)应该在后台线程上工作,因为DB可能需要初始化/更新,而Transformations.map部分应该在UI线程上(不幸的是,包括映射本身)。

我尝试过的

这让我想到了这种丑陋的解决方案,即让一个实时数据的监听器在UI线程上运行:

@UiThread
fun getAsLiveData(someContext: Context,listener: OnLiveDataReadyListener) {
val context = someContext.applicationContext ?: someContext
val handler = Handler(Looper.getMainLooper())
Executors.storageExecutor.execute {
val dbLiveData = Database.getInstance(context).getSomeDao().getAllAsLiveData()
handler.post {
val resultLiveData: LiveData<List<SomeClass>> = Transformations.map(
dbLiveData) { data ->
data.map { SomeClass(it) }
}
listener.onLiveDataReadyListener(resultLiveData)
}
}
}

注意:我使用简单的线程解决方案,因为它是一个SDK,所以我想尽可能避免导入库。另外,无论如何,这是一个非常简单的案例。

问题

有没有办法在UI线程上提供转换后的实时数据,即使还没有准备好,也没有任何监听器?

意思是某种";懒惰;初始化转换后的实时数据。只有当某个观察者处于活动状态时,它才会初始化/更新DB并开始真正的获取&转换(当然都在后台线程中)。

问题

  • 您是一个没有UX/UI或没有上下文来派生生命周期的SDK
  • 您需要提供一些数据,但要以异步方式提供,因为这是您需要从源中获取的数据
  • 您还需要时间来初始化自己的内部依赖关系
  • 您不希望将数据库对象/内部模型暴露给外部世界

您的解决方案

  • 您的数据作为LiveData直接来自您的源(在这种特殊情况下,尽管不相关,但来自房间数据库)

你能做什么

  • 使用Coroutines,这是目前首选的文档化方式(比RxJava这样的野兽更小)
  • 不要提供List<TransformedData>。而是有一个状态:
sealed class SomeClassState {
object NotReady : SomeClassState()
data class DataFetchedSuccessfully(val data: List<TransformedData>): SomeClassState()
// add other states if/as you see fit, e.g.: "Loading" "Error" Etc.
}

然后以不同的方式暴露您的LiveData:

private val _state: MutableLiveData<SomeClassState> = MutableLiveData(SomeClassState.NotReady) // init with a default value
val observeState(): LiveData<SomeClassState) = _state

现在,无论是谁在消费数据,都可以用自己的生命周期来观察它。

然后,您可以继续使用获取公共方法:

SomeClassRepository中的某个位置(您有数据库),接受Dispatcher(或CoroutineScope):

suspend fun fetchSomeClassThingy(val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default) {
return withContext(defaultDispatcher) {
// Notify you're fetching...
_state.postValue(SomeClassState.Loading)         

// get your DB or initialize it (should probably be injected in an already working state, but doesn't matter)
val db = ...

//fetch the data and transform at will
val result = db.dao().doesntmatter().what().you().do()
// Finally, post it.
_state.postValue(SomeClassState.DataFetchedSuccessfully(result))
}   
}

我还能做什么

  • 数据来自数据库这一事实是或应该是绝对无关的
  • 我不会直接从Room返回LiveData(我发现谷歌上的一个非常糟糕的决定违背了他们自己的架构,如果有什么不同的话,那就是让你有能力自己开枪)
  • 我会考虑公开一个flow,它允许emit值N次

最后但并非最不重要的是,我建议你花15分钟阅读谷歌推论最佳实践最近发布的(2021),因为它会给你一个你可能没有的见解(我当然没有做其中的一些)。

请注意,我没有涉及到一个ViewModel,这都是针对体系结构洋葱的较低层。通过注入(通过param或DI)Dispatcher,您可以方便地对此进行测试(稍后在测试中使用Testdispatcher),也不会对线程进行任何假设,也不会施加任何限制;它也是一个suspend函数,所以这里已经介绍过了。

希望这能给你一个新的视角。祝你好运

好吧,我得到了这样的结果:

@UiThread
fun getSavedReportsLiveData(someContext: Context): LiveData<List<SomeClass>> {
val context = someContext.applicationContext ?: someContext
val dbLiveData =
LibraryDatabase.getInstance(context).getSomeDao().getAllAsLiveData()
val result = MediatorLiveData<List<SomeClass>>()
result.addSource(dbLiveData) { list ->
Executors.storageExecutor.execute {
result.postValue(list.map { SomeClass(it) })
}
}
return result
}
internal object Executors {
/**used only for things that are related to storage on the device, including DB */
val storageExecutor: ExecutorService = ForkJoinPool(1)
}

我找到这个解决方案的方式实际上是通过一个非常类似的问题(这里),我认为它是基于Transformations.map()的代码:

@MainThread
public static <X, Y> LiveData<Y> map(
@NonNull LiveData<X> source,
@NonNull final Function<X, Y> mapFunction) {
final MediatorLiveData<Y> result = new MediatorLiveData<>();
result.addSource(source, new Observer<X>() {
@Override
public void onChanged(@Nullable X x) {
result.setValue(mapFunction.apply(x));
}
});
return result;
}

不过,请注意,如果您在Room上有迁移代码(来自其他DB),这可能是一个问题,因为这应该在后台线程上。

对于这个问题,我不知道如何解决,除了尝试尽快进行迁移,或者使用"的回调;onCreate"(此处为docs),但遗憾的是,您不会引用您的类。相反,您将获得对SupportSQLiteDatabase的引用,因此您可能需要进行大量手动迁移。。。

最新更新