如果可能需要 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 的引用,因此您可能需要进行大量手动迁移...