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

时间:2018-06-08 02:08:35

标签: android kotlin android-room clean-architecture android-livedata

我试图将清洁架构方法应用于我的项目(Link: guide I'm currently referencing)。

我使用Room数据库进行本地存储,我希望它成为应用程序中的单一数据源 - 这意味着首先从网络调用收集的所有数据都保存在数据库中,并且只有在传递给主持人。房间提供从其DAO返回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以返回LiveData&gt;
  2. 或者直接从ViewModel跳过存储库实现
  3. 调用Room的DAO

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

    1. 如果我将android.arch.lifecycle.LiveData导入我的Repository界面,而不是破坏Domain层中的抽象,因为它现在依赖于android架构库。
    2. 如果我直接在ViewModel中调用Room的DAO作为val entities: LiveData<List<Entity>> = database.entityDao.findByUsername(username),那么我违反了必须使用Reposiotry进行所有数据访问的规则我会需要创建一些样板代码以与远程存储等同步。
    3. 如何使用LiveData,Room的DAO和Clean architecure模式实现单一数据源方法?

3 个答案:

答案 0 :(得分:2)

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

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

您希望每次有更改时都会向您返回新的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 ->
    // ...
}

答案 1 :(得分:1)

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

答案 2 :(得分:0)

在您的域中使用 Flow 作为返回类型 由于 flow 是 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 你接收数据/模型包的数据类,而理想情况下你的视图模型应该接收域/模型包的数据类,以便你将它映射到域模型

这里是dao类

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()