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 实现来获取它。但为了实现这一点,我需要:
- 将存储库方法 findByUsername 更改为 return LiveData>
- 或者直接从 ViewModel 调用 Room 的 DAO,完全跳过存储库实现
这两个选项都有足够的缺点:
- 如果我将
android.arch.lifecycle.LiveData
导入到我的 Repository 接口中,它会破坏域层中的抽象,因为它现在依赖于 android 架构库。
- 如果我在 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>>
所以现在您可能想要做的是制作您自己的可以处理 LiveData
或 Flowable
的订阅包装器。当然,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()
我正在尝试将干净的架构方法应用于我的项目 (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 实现来获取它。但为了实现这一点,我需要:
- 将存储库方法 findByUsername 更改为 return LiveData>
- 或者直接从 ViewModel 调用 Room 的 DAO,完全跳过存储库实现
这两个选项都有足够的缺点:
- 如果我将
android.arch.lifecycle.LiveData
导入到我的 Repository 接口中,它会破坏域层中的抽象,因为它现在依赖于 android 架构库。 - 如果我在 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>>
所以现在您可能想要做的是制作您自己的可以处理 LiveData
或 Flowable
的订阅包装器。当然,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()