【问题标题】:Android: clean architecture with Room database and LiveData in DAOAndroid:在 DAO 中使用 Room 数据库和 LiveData 的干净架构
【发布时间】:2018-06-08 02:08:34
【问题描述】:

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

我正在使用 Room 数据库进行本地存储,我希望它成为应用程序中的单一数据源 - 这意味着首先从网络调用收集的所有数据都保存在数据库中,然后才传递给演示者。 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>
  2. 或者直接从 ViewModel 调用 Room 的 DAO,完全跳过存储库实现

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

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

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

【问题讨论】:

  • 大部分答案都很复杂而且矫枉过正。只需查看 google 示例中的 this elegance 即可。该函数返回 LiveData 但由于 kotlin 的语言特性,您不必导入它。好好研究一下那个 repo。它可能会解决你的大部分问题

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


【解决方案1】:

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

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

您希望订阅能够在每次发生更改时返回给您一个新的List&lt;T&gt;

 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 ->
    // ...
}

【讨论】:

【解决方案2】:

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

【讨论】:

  • 我正在考虑这种方法,其他开发人员也坚持将 LiveData 移动到域层或切换到响应式,而不是实施不断的变通方法。我想我会朝着建议的方向前进,谢谢您的回答!
【解决方案3】:

在您的域中使用 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()

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-09-15
    • 1970-01-01
    • 2016-06-24
    • 2020-02-17
    • 2020-03-16
    • 1970-01-01
    • 2019-05-24
    相关资源
    最近更新 更多