12

I'm trying to apply clean-architecture approach to my project (Link: guide I'm currently referencing).

I'm using Room database for local storage and I want it to be the single source of data in the application - this means that all data gathered from network calls first is saved in database and only after is passed to the presenter. Room provides return of LiveData from its DAOs and this is exactly what suits my needs.

However I also want to use repositories as a single way to access data. Here's an example of repository interface in domain layer (the most abstract one):

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

And here I'm running into the problem - I need to get a LiveData from Room's DAO in my ViewModel and I'd like to get it using Repository implementation. But in order to achieve this I need either to:

  1. Change Repository method findByUsername to return LiveData>
  2. Or call Room's DAO directly from ViewModel skipping repository implementation completely

Both of these options have sufficient drawbacks:

  1. If I import android.arch.lifecycle.LiveData into my Repository interface than it would break the abstraction in Domain layer, as it is now depending on android architecture libraries.
  2. If I call Room's DAO directly in the ViewModel as val entities: LiveData<List<Entity>> = database.entityDao.findByUsername(username) then I'm breaking the rule that all data access must be made using Reposiotry and I will need to create some boilerplate code for synchronization with remote storage etc.

How is it possible to achieve single data source approach using LiveData, Room's DAO and Clean architecure patterns?

Sagar
  • 23,903
  • 4
  • 62
  • 62
Alexandr Zhurkov
  • 474
  • 5
  • 19
  • 3
    most of the answers are complicated and overkill. Just look at [this elegance](https://github.com/googlesamples/android-sunflower/blob/master/app/src/main/java/com/google/samples/apps/sunflower/data/PlantRepository.kt#L24) from google's sample. That function is returning `LiveData` but you don't have to import it because of kotlin's language features. Give that repo a good study. It'll probably solve most of your problems – denvercoder9 Jun 10 '18 at 03:16

3 Answers3

6

Technically you are running into trouble because you don't want synchronous data fetching.

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

You want a subscription that returns to you a new List<T> each time there is a change.

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

So now what you might want to do is make your own subscription wrapper that can handle LiveData or Flowable. Of course, LiveData is trickier because you must also give it a LifecycleOwner.

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

    void observe(Observer<T> observer);

    void clear();
}

And then something like

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;
    }
}

And now you can use your repository

val subscription = repository.findByUsernameWithChanges("blah")
if(subscription is LiveDataSubscription) {
    subscription.lifecycleOwner = this
}
subscription.observe { data ->
    // ...
}
EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
5

When similar question is asked about using RxJava, developers usualy answer, that is ok, and RxJava now is a language part, so, you can use it in domain layer. In my opinion - you can do anything, if it helps you, so, if using LiveData don't create problems - use it, or you can use RxJava, or Kotlin coroutines instead.

Mamykin Andrey
  • 1,352
  • 10
  • 13
  • I was considering this approach and other developers also insisted on moving LiveData to domain layer or switching to reactive instead of implementing constant workarounds. I think I will go in suggested direction, thanks for your answer! – Alexandr Zhurkov Jun 08 '18 at 07:01
1

Use Flow as return type in your domain since flow is part of Kotlin language, it's fully acceptable to use this type in your domain. here is an example

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

then in your data package you can implement it

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

Note that

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

from your dao you are receiving data class of data/model package, while ideally your viewmodel should receive data class of domain/model package so that you are mapping it to domain model

here is dao 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>>
}

then in your viewmodel you would convert flow to livedata

   val currencies =
        getCurrenciesUseCase()
            .onStart { _isLoading.value = true }
            .onCompletion { _isLoading.value = false }.asLiveData()
Amr
  • 1,068
  • 12
  • 21