如果可能需要 initialization/update 的数据库,如何在 Room 上提供来自数据库的转换后的 liveData?
How to offer a transformed liveData from DB on Room, if initialization/update of the DB might be needed?
背景
我正在创建一些 SDK 库,我想提供一些 liveData 作为函数的返回对象,这将允许监视数据库上的数据。
问题
我不想透露 DB 中的真实对象及其字段(如 ID),因此我想对它们进行转换。
所以,假设我有来自数据库的这个 liveData:
val dbLiveData = Database.getInstance(context).getSomeDao().getAllAsLiveData()
为了让 liveData 提供给外部,我所做的是:
val resultLiveData: LiveData<List<SomeClass>> = Transformations.map(
dbLiveData) { data ->
data.map { SomeClass(it) }
}
这个效果很好。
但是,问题在于第一行(获取 dbLiveData
)应该在后台线程上工作,因为数据库可能需要 initialize/update,而 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 线程上提供转换后的实时数据,即使它还没有准备好,没有任何侦听器?
意味着对转换后的实时数据进行某种“惰性”初始化。只有当某个观察者处于活动状态时,它才会 initialize/update 数据库并开始真正的获取和转换(当然,两者都在后台线程中)。
问题
- 你是一个SDK,没有UX/UI,或者没有派生生命周期的上下文。
- 您需要提供一些数据,但以异步方式提供,因为您需要从源中获取这些数据。
- 您还需要时间来初始化您自己的内部依赖项。
- 您不想向外界公开您的数据库 objects/internal 模型。
您的解决方案
- 您的数据作为 LiveData 直接 来自您的源(在这种特殊情况下,尽管不相关,但来自房间数据库)。
你可以做什么
- 使用协程,这是当今首选的记录方式(并且比 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
现在,无论谁在消费数据,都可以用自己的生命周期观察它。
然后,您可以继续获取 public 方法:
在你的 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))
}
}
我还能做什么。
- 数据来自数据库这一事实是或者应该是绝对不相关的。
- 我会不会 return 直接从 Room 获取 LiveData(我发现 Google 上的一个非常糟糕的决定违背了他们自己的架构,如果有的话, 让你有能力射自己的脚)。
- 我会考虑公开一个
flow
,它允许您 emit
值 N 次。
最后但同样重要的是,我建议您花 15 分钟阅读 Google Coroutines Best Practices 最近发布的 (2021),因为它会给您一个您可能没有的见解(我当然其中一些没有做)。
请注意,我没有涉及单个 ViewModel,这都是针对架构洋葱的较低层。通过注入(通过参数或 DI)Dispatcher,您可以方便地对此进行测试(稍后在测试中使用 Testdispatcher),也不会对 Threading 做出任何假设,也不会施加任何限制;它也是一个 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 上有迁移代码(来自其他数据库),这可能是个问题,因为这应该在后台线程上。
为此我不知道如何解决,除了尝试尽快进行迁移,或者以某种方式使用数据库的“onCreate”(文档here)的回调,但遗憾的是不过,您不会参考 class。相反,您将获得对 SupportSQLiteDatabase 的引用,因此您可能需要进行大量手动迁移...
背景
我正在创建一些 SDK 库,我想提供一些 liveData 作为函数的返回对象,这将允许监视数据库上的数据。
问题
我不想透露 DB 中的真实对象及其字段(如 ID),因此我想对它们进行转换。
所以,假设我有来自数据库的这个 liveData:
val dbLiveData = Database.getInstance(context).getSomeDao().getAllAsLiveData()
为了让 liveData 提供给外部,我所做的是:
val resultLiveData: LiveData<List<SomeClass>> = Transformations.map(
dbLiveData) { data ->
data.map { SomeClass(it) }
}
这个效果很好。
但是,问题在于第一行(获取 dbLiveData
)应该在后台线程上工作,因为数据库可能需要 initialize/update,而 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 线程上提供转换后的实时数据,即使它还没有准备好,没有任何侦听器?
意味着对转换后的实时数据进行某种“惰性”初始化。只有当某个观察者处于活动状态时,它才会 initialize/update 数据库并开始真正的获取和转换(当然,两者都在后台线程中)。
问题
- 你是一个SDK,没有UX/UI,或者没有派生生命周期的上下文。
- 您需要提供一些数据,但以异步方式提供,因为您需要从源中获取这些数据。
- 您还需要时间来初始化您自己的内部依赖项。
- 您不想向外界公开您的数据库 objects/internal 模型。
您的解决方案
- 您的数据作为 LiveData 直接 来自您的源(在这种特殊情况下,尽管不相关,但来自房间数据库)。
你可以做什么
- 使用协程,这是当今首选的记录方式(并且比 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
现在,无论谁在消费数据,都可以用自己的生命周期观察它。
然后,您可以继续获取 public 方法:
在你的 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))
}
}
我还能做什么。
- 数据来自数据库这一事实是或者应该是绝对不相关的。
- 我会不会 return 直接从 Room 获取 LiveData(我发现 Google 上的一个非常糟糕的决定违背了他们自己的架构,如果有的话, 让你有能力射自己的脚)。
- 我会考虑公开一个
flow
,它允许您emit
值 N 次。
最后但同样重要的是,我建议您花 15 分钟阅读 Google Coroutines Best Practices 最近发布的 (2021),因为它会给您一个您可能没有的见解(我当然其中一些没有做)。
请注意,我没有涉及单个 ViewModel,这都是针对架构洋葱的较低层。通过注入(通过参数或 DI)Dispatcher,您可以方便地对此进行测试(稍后在测试中使用 Testdispatcher),也不会对 Threading 做出任何假设,也不会施加任何限制;它也是一个 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)
}
我找到这个解决方案的方式实际上是通过一个非常相似的问题 (
@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 上有迁移代码(来自其他数据库),这可能是个问题,因为这应该在后台线程上。
为此我不知道如何解决,除了尝试尽快进行迁移,或者以某种方式使用数据库的“onCreate”(文档here)的回调,但遗憾的是不过,您不会参考 class。相反,您将获得对 SupportSQLiteDatabase 的引用,因此您可能需要进行大量手动迁移...