Android:在 DAO 中使用 Room 数据库和 LiveData 的干净架构

Android: clean architecture with Room database and LiveData in DAO

我正在尝试将干净的架构方法应用于我的项目 (Link: guide I'm currently referencing)。

我正在使用 Room 数据库进行本地存储,我希望它成为应用程序中的单一数据源 - 这意味着从网络调用收集的所有数据首先保存在数据库中,然后才传递给主持人。 Room 从其 DAO 中提供了 return 的 LiveData,这正是我需要的。

不过,我也想使用存储库作为访问数据的单一方式。这是领域层(最抽象的一个)中存储库接口的示例:

interface Repository<T>{
    fun findByUsername(username: String) : List<T>    
    fun add(entity: T): Long
    fun remove(entity: T)    
    fun update(entity: T) : Int
}

这里我 运行 遇到了问题 - 我需要在我的 ViewModel 中从 Room 的 DAO 获取 LiveData,并且我想使用 Repository 实现来获取它。但为了实现这一点,我需要:

  1. 将存储库方法 findByUsername 更改为 return LiveData>
  2. 或者直接从 ViewModel 调用 Room 的 DAO,完全跳过存储库实现

这两个选项都有足够的缺点:

  1. 如果我将 android.arch.lifecycle.LiveData 导入到我的 Repository 接口中,它会破坏域层中的抽象,因为它现在依赖于 android 架构库。
  2. 如果我在 ViewModel 中直接调用 Room 的 DAO val entities: LiveData<List<Entity>> = database.entityDao.findByUsername(username) 那么我就违反了所有 数据访问必须使用 Reposiotry 的规则,我将需要创建一些用于与远程存储同步的样板代码等。

如何使用 LiveData、Room 的 DAO 和 Clean 架构模式实现单一数据源方法?

当被问到关于使用 RxJava 的类似问题时,开发人员通常会回答,没关系,而且 RxJava 现在是语言部分,因此,您可以在领域层使用它。在我看来 - 你可以做任何事情,如果它对你有帮助的话,所以,如果使用 LiveData 不会产生问题 - 使用它,或者你可以改用 RxJava 或 Kotlin 协程。

从技术上讲,您 运行 遇到了麻烦,因为您不想同步获取数据。

 fun findByUsername(username: String) : List<T>  

您想要一个 returns 每次有变化时给您一个新的 List<T> 的订阅。

 fun findByUsernameWithChanges(username: String) : Subscription<List<T>>

所以现在您可能想要做的是制作您自己的可以处理 LiveDataFlowable 的订阅包装器。当然,LiveData 比较棘手,因为您还必须给它一个 LifecycleOwner。

public interface Subscription<T> {
    public interface Observer<T> {
        void onChange(T t);
    }

    void observe(Observer<T> observer);

    void clear();
}

然后是

public class LiveDataSubscription<T> implements Subscription<T> {
    private LiveData<T> liveData;
    private LifecycleOwner lifecycleOwner;

    private List<Observer<T>> foreverObservers = new ArrayList<>();

    public LiveDataSubscription(LiveData<T> liveData) {
        this.liveData = liveData;
    }

    @Override
    public void observe(final Observer<T> observer) {
        if(lifecycleOwner != null) {
            liveData.observe(lifecycleOwner, new android.arch.lifecycle.Observer<T>() {
                 @Override
                 public void onChange(@Nullable T t) {
                      observer.onChange(t);
                 }
            });
        } else {
            Observer<T> foreverObserver = new android.arch.lifecycle.Observer<T>() {
                 @Override
                 public void onChange(@Nullable T t) {
                      observer.onChange(t);
                 }
            };
            foreverObservers.add(foreverObserver);
            liveData.observeForever(foreverObserver);
        }
    }

    @Override
    public void clear() {
        if(lifecycleOwner != null) {
            liveData.removeObservers(lifecycleOwner);
        } else {
            for(Observer<T> observer: foreverObservers) {
                liveData.removeObserver(observer);
            }
        }
    }

    public void setLifecycleOwner(LifecycleOwner lifecycleOwner) {
        this.lifecycleOwner = lifecycleOwner;
    }
}

现在您可以使用您的存储库

val subscription = repository.findByUsernameWithChanges("blah")
if(subscription is LiveDataSubscription) {
    subscription.lifecycleOwner = this
}
subscription.observe { data ->
    // ...
}

使用 Flow 作为 return 输入您的域 由于流是 Kotlin 语言的一部分,因此在您的域中使用此类型是完全可以接受的。 这是一个例子

Repository.kt

package com.example.www.myawsomapp.domain

import com.example.www.myawsomapp.domain.model.Currency
import com.example.www.myawsomapp.domain.model.Result
import kotlinx.coroutines.flow.Flow

interface Repository {
    fun getCurrencies(): Flow<List<Currency>>
    suspend fun updateCurrencies(): Result<Unit>
}

那么在你的数据包中就可以实现了

package com.example.www.myawsomapp.data
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class RepositoryImpl @Inject constructor(
    private val currencyDao: CurrencyDao,
    private val api: CurrencyApi,
    private val connectivity: Connectivity
) :
    Repository {


    override fun getCurrencies(): Flow<List<Currency>> =
        currencyDao.getAll().map { list -> list.map { it.toDomain() } }

    override suspend fun updateCurrencies(): Result<Unit> =
        withContext(Dispatchers.IO) {
            val rowsInDataBase = currencyDao.getCount()
            if (rowsInDataBase <= 0) {
                if (connectivity.hasNetworkAccess()) {
                    return@withContext updateDataBaseFromApi()
                } else {
                    return@withContext Failure(HttpError(Throwable(NO_INTERNET_CONNECTION)))
                }
            } else {
                return@withContext Success(Unit)
            }
        }
}

注意

currencyDao.getAll().map { list -> list.map { it.toDomain() } }

从你的 dao 中你正在接收数据 class of data/model package,而理想情况下你的 viewmodel 应该接收数据 class of domain/model package 以便你映射它到领域模型

这里是道class

package com.example.www.myawsomapp.data.database.dao

import com.blogspot.soyamr.cft.data.database.model.Currency
import kotlinx.coroutines.flow.Flow
import com.blogspot.soyamr.cft.data.database.model.Currency

@Dao
interface CurrencyDao {
    @Query("SELECT * FROM currency")
    fun getAll(): Flow<List<Currency>>
}

然后在您的视图模型中将流转换为实时数据

   val currencies =
        getCurrenciesUseCase()
            .onStart { _isLoading.value = true }
            .onCompletion { _isLoading.value = false }.asLiveData()