Let every Observer only receive *new* LiveData upon subscribing/observing - android

Whenever you call .observe() on LiveData, the Observer receives the last value of that LiveData. This may be useful in some cases, but not in mine.
Whenever I call .observe(), I want the Observer to receive only future LiveData changes, but not the value it holds when .observe() is called.
I may have more than one Observer for a LiveData instance. I want them all to receive LiveData updates when they happen.
I want each LiveData update to be consumed only once by each Observer. I think is just a re-phrasing of the first requirement, but my head is spinning already and I'm not sure about it.
While googling this problem, I came upon two common approaches:
Wrap the data in an LiveData<SingleEvent<Data>> and check in this SingleEvent class if it was already consumed.
Extend MediatorLiveData and use a look-up-map if the Observer already got the Event
Examples for these approaches can be found here:
https://gist.github.com/JoseAlcerreca/5b661f1800e1e654f07cc54fe87441af#gistcomment-2783677
https://gist.github.com/hadilq/f095120348a6a14251a02aca329f1845#file-liveevent-kt
https://gist.github.com/JoseAlcerreca/5b661f1800e1e654f07cc54fe87441af#file-event-kt
Unfortunately none of these examples solves all my requirements. Most of the time, the problem is that any new Observer still receives the last LiveData value upon subscribing. That means that a Snackbar which was already shown is displayed again and again whenever the user navigates between screens.
To give you some insights what I am talking about / what I am coding about:
I am following the LiveData MVVM design of the Android Architecture Componentns:
2 ListFragment are showing a list of entries.
They are using 2 instances of the same ViewModel class to observe UI-related LiveData.
The user can delete an entry in such a ListFragment. The deletion is done by the ViewModel calling Repository.delete()
The ViewModel observes the Repository for RepositoryEvents.
So when the deletion is done, the Repository informs the ViewModel about it and the ViewModel inform the ListFragment about it.
Now, when the user switches to the second ListFragment the following happens:
The second Fragment gets created and calls .observe() on its ViewModel
The ViewModel gets created and calls .observe() on the Repository
The Repository sends its current RepositoryEvent to the ViewModel
The ViewModel send the according UI Event to the Fragment
The Fragment shows a confirmation Snackbar for a deletion that happened somewhere else.
Heres some simplified code:
Fragment:
viewModel.dataEvents.observe(viewLifecycleOwner, Observer { showSnackbar() })
viewModel.deleteEntry()
ViewModel:
val dataEvents: LiveData<EntryListEvent> = Transformations.switchMap(repository.events, ::handleRepoEvent)
fun deleteEntry() = repository.deleteEntry()
private fun handleRepoEvent(event: RepositoryEvent): LiveData<EntryListEvent> {
// convert the repository event to an UI event
}
Repository:
private val _events = MutableLiveData<RepositoryEvent>()
val events: LiveData<RepositoryEvent>
get() = _events
fun deleteEntry() {
// delete it from database
_events.postValue(RepositoryEvent.OnDeleteSuccess)
}

UPDATE 2021:
Using the coroutines library and Flow it is now very easy to achieve this by implementing Channels:
MainActivity
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar
import com.plcoding.kotlinchannels.databinding.ActivityMainBinding
import kotlinx.coroutines.flow.collect
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
binding.btnShowSnackbar.setOnClickListener {
viewModel.triggerEvent()
}
lifecycleScope.launchWhenStarted {
viewModel.eventFlow.collect { event ->
when(event) {
is MainViewModel.MyEvent.ErrorEvent -> {
Snackbar.make(binding.root, event.message, Snackbar.LENGTH_LONG).show()
}
}
}
}
}
}
MainViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
class MainViewModel : ViewModel() {
sealed class MyEvent {
data class ErrorEvent(val message: String): MyEvent()
}
private val eventChannel = Channel<MyEvent>()
val eventFlow = eventChannel.receiveAsFlow()
fun triggerEvent() = viewModelScope.launch {
eventChannel.send(MyEvent.ErrorEvent("This is an error"))
}
}

For me problem was solved with this:
Event wrapper class to keep event related data(Copy from google samples)
public class Event<T> {
private T mContent;
private boolean hasBeenHandled = false;
public Event( T content) {
if (content == null) {
throw new IllegalArgumentException("null values in Event are not allowed.");
}
mContent = content;
}
#Nullable
public T getContentIfNotHandled() {
if (hasBeenHandled) {
return null;
} else {
hasBeenHandled = true;
return mContent;
}
}
public boolean hasBeenHandled() {
return hasBeenHandled;
}
}
Next, i create event observer class, that handles data checks(null, etc):
public class EventObserver<T> implements Observer<Event<T>> {
#Override
public void onChanged(Event<T> tEvent) {
if (tEvent != null && !tEvent.hasBeenHandled())
onEvent(tEvent.getContentIfNotHandled());
}
protected void onEvent(#NonNull T content) {}
}
And, event handler class, to simplify access from viewmodel:
public class EventHandler<T> {
private MutableLiveData<Event<T>> liveEvent = new MutableLiveData<>();
public void observe(#NonNull LifecycleOwner owner, #NonNull EventObserver<T> observer){
liveEvent.observe(owner, observer);
}
public void create(T content) {
liveEvent.setValue(new Event<>(content));
}
}
Example:
In ViewModel.class:
private EventHandler<Boolean> swipeEventHandler = new EventHandler<>();
public EventHandler<Boolean> getSwipeEventHandler() {
return swipeEventHandler;
}
In Activity/Fragment:
Start observing:
viewModel
.getSwipeEventHandler()
.observe(
getViewLifecycleOwner(),
new EventObserver<Boolean>() {
#Override
protected void onEvent(#NonNull Boolean content) {
if(content)confirmDelete(modifier);
}
});
Create event:
viewModel.getSwipeEventHandler().create(true);

Created a basic sealed class flag in the need of:
sealed class Event(private var handled: Boolean = false) {
val coldData: Event?
get() {
return if (handled) null else {
handled = true
this
}
}
class ShowLoader() : Event()
class HideLoader() : Event()
class ShowErrorAlert(#StringRes val message: Int) : Event()
}
Then it can be observed at different fragments
viewModel.eventFlow.observe(this) { event ->
val data = event.coldData
when (data) {
is Event.ShowLoader -> {
progressBar.visible = true
}
is Event.HideLoader -> {
progressBar.visible = false
}
is Event.ShowErrorAlert -> {
showAlert(data.message)
}
else -> {
// do nothing
}
}
}
Or use a subclass of MutableLiveDatawith the same purpose to process them individually.

Related

How to prevent data duplication caused by LiveData observation in Fragment?

I'm subscribed to an observable in my Fragment, the observable listens for some user input from three different sources.
The main issue is that once I navigate to another Fragment and return to the one with the subscription, the data is duplicated as the observable is handled twice.
What is the correct way to handle a situation like this?
I've migrated my application to a Single-Activity and before it, the subscription was made in the activity without any problem.
Here is my Fragment code:
#AndroidEntryPoint
class ProductsFragment : Fragment() {
#Inject
lateinit var sharedPreferences: SharedPreferences
private var _binding: FragmentProductsBinding? = null
private val binding get() = _binding!!
private val viewModel: ProductsViewModel by viewModels()
private val scanner: CodeReaderViewModel by activityViewModels()
private fun observeBarcode() {
scanner.barcode.observe(viewLifecycleOwner) { barcode ->
if (barcode.isNotEmpty()) {
if (binding.searchView.isIconified) {
addProduct(barcode) // here if the fragment is resumed from a backstack the data is duplicated.
}
if (!binding.searchView.isIconified) {
binding.searchView.setQuery(barcode, true)
}
}
}
}
private fun addProduct(barcode: String) {
if (barcode.isEmpty()) {
return
}
viewModel.insert(barcode)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.start(args.documentId)
if (args.documentType == "Etichette") {
binding.cvLabels.visibility = View.VISIBLE
}
initUI()
observe()
}
private fun observe() {
observeBarcode()
observeProducts()
observeLoading()
observeLast()
}
}
Unfortunately, LiveData is a terribly bad idea (the way it was designed), Google insisted till they kinda phased it out (but not really since it's still there) that "it's just a value holder"...
Anyway... not to rant too much, the solution you have to use can be:
Use The "SingleLiveEvent" (method is officially "deprecated now" but... you can read more about it here).
Follow the "official guidelines" and use a Flow instead, as described in the official guideline for handling UI Events.
Update: Using StateFlow
The way to collect the flow is, for e.g. in a Fragment:
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { // or RESUMED
viewModel.yourFlow.collectLatest { ... } // or collect { ... }
}
}
For that in your ViewModel you'd expose something like:
Warning: Pseudo-Code
// Imagine your state is represented in this sealed class
sealed class State {
object Idle: State
object Loading: State
data class Success(val name: String): State
data class Failure(val reason: String): State
}
// You need an initial state
private val _yourFlow = MutableStateFlow(State.Idle)
val yourFlow: StateFlow<State> = _yourFlow
Then you can emit using
_yourFlow.emit(State.Loading)
Every time you call
scanner.barcode.observe(viewLifecycleOwner){
}
You are creating a new anonymous observer. So every new call to observe will add another observer that will get onChanged callbacks. You could move this observer out to be a property. With this solution observe won't register new observers.
Try
class property
val observer = Observer<String> { onChanged() }
inside your method
scanner.barcode.observe(viewLifecycleOwner, observer)
Alternatively you could keep your observe code as is but move it to a Fragment's callback that only gets called once fex. onCreate(). onCreate gets called only once per fragment instance whereas onViewCreated gets called every time the fragment's view is created.

Passing errors coming from the API call

I am using 2 separate liveData exposed to show the error coming from the API. I am basically checking if there is an exception with the API call, pass a failure status and serverErrorLiveData will be observed.
So I have serverErrorLiveData for error and creditReportLiveData for result without an error.
I think I am not doing this the right way. Could you please guide me on what is the right way of catching error from the API call. Also, any concerns/recommendation on passing data from repository on to view model.
What is the right way of handing loading state?
CreditScoreFragment
private fun initViewModel() {
viewModel.getCreditReportObserver().observe(viewLifecycleOwner, Observer<CreditReport> {
showScoreUI(true)
binding.score.text = it.creditReportInfo.score.toString()
binding.maxScoreValue.text = "out of ${it.creditReportInfo.maxScoreValue}"
initDonutView(
it.creditReportInfo.score.toFloat(),
it.creditReportInfo.maxScoreValue.toFloat()
)
})
viewModel.getServerErrorLiveDataObserver().observe(viewLifecycleOwner, Observer<Boolean> {
if (it) {
showScoreUI(false)
showToastMessage()
}
})
viewModel.getCreditReport()
}
MainActivityViewModel
class MainActivityViewModel #Inject constructor(
private val dataRepository: DataRepository
) : ViewModel() {
var creditReportLiveData: MutableLiveData<CreditReport>
var serverErrorLiveData: MutableLiveData<Boolean>
init {
creditReportLiveData = MutableLiveData()
serverErrorLiveData = MutableLiveData()
}
fun getCreditReportObserver(): MutableLiveData<CreditReport> {
return creditReportLiveData
}
fun getServerErrorLiveDataObserver(): MutableLiveData<Boolean> {
return serverErrorLiveData
}
fun getCreditReport() {
viewModelScope.launch(Dispatchers.IO) {
val response = dataRepository.getCreditReport()
when(response.status) {
CreditReportResponse.Status.SUCCESS -> creditReportLiveData.postValue(response.creditReport)
CreditReportResponse.Status.FAILURE -> serverErrorLiveData.postValue(true)
}
}
}
}
DataRepository
class DataRepository #Inject constructor(
private val apiServiceInterface: ApiServiceInterface
) {
suspend fun getCreditReport(): CreditReportResponse {
return try {
val creditReport = apiServiceInterface.getDataFromApi()
CreditReportResponse(creditReport, CreditReportResponse.Status.SUCCESS)
} catch (e: Exception) {
CreditReportResponse(null, CreditReportResponse.Status.FAILURE)
}
}
}
ApiServiceInterface
interface ApiServiceInterface {
#GET("endpoint.json")
suspend fun getDataFromApi(): CreditReport
}
CreditScoreResponse
data class CreditReportResponse constructor(val creditReport: CreditReport?, val status: Status) {
enum class Status {
SUCCESS, FAILURE
}
}
It's creates complexity and increased chances for a coding error to have two LiveData channels for success and failure. You should have a single LiveData that can offer up the data or an error so you know it's coming in orderly and you can observe it in one place. Then if you add a retry policy, for example, you won't risk somehow showing an error after a valid value comes in. Kotlin can facilitate this in a type-safe way using a sealed class. But you're already using a wrapper class for success and failure. I think you can go to the source and simplify it. You can even just use Kotlin's own Result class.
(Side note, your getCreditReportObserver() and getServerErrorLiveDataObserver() functions are entirely redundant because they simply return the same thing as a property. You don't need getter functions in Kotlin because properties basically are getter functions, with the exception of suspend getter functions because Kotlin doesn't support suspend properties.)
So, to do this, eliminate your CreditReportResponse class. Change your repo function to:
suspend fun getCreditReport(): Result<CreditReport> = runCatching {
apiServiceInterface.getDataFromApi()
}
If you must use LiveData (I think it's simpler not to for a single retrieved value, see below), your ViewModel can look like:
class MainActivityViewModel #Inject constructor(
private val dataRepository: DataRepository
) : ViewModel() {
val _creditReportLiveData = MutableLiveData<Result<CreditReport>>()
val creditReportLiveData: LiveData<Result<CreditReport>> = _creditReportLiveData
fun fetchCreditReport() { // I changed the name because "get" implies a return value
// but personally I would change this to an init block so it just starts automatically
// without the Fragment having to manually call it.
viewModelScope.launch { // no need to specify dispatcher to call suspend function
_creditReportLiveData.value = dataRepository.getCreditReport()
}
}
}
Then in your fragment:
private fun initViewModel() {
viewModel.creditReportLiveData.observe(viewLifecycleOwner) { result ->
result.onSuccess {
showScoreUI(true)
binding.score.text = it.creditReportInfo.score.toString()
binding.maxScoreValue.text = "out of ${it.creditReportInfo.maxScoreValue}"
initDonutView(
it.creditReportInfo.score.toFloat(),
it.creditReportInfo.maxScoreValue.toFloat()
)
}.onFailure {
showScoreUI(false)
showToastMessage()
}
viewModel.fetchCreditReport()
}
Edit: the below would simplify your current code, but closes you off from being able to easily add a retry policy on failure. It might make better sense to keep the LiveData.
Since you are only retrieving a single value, it would be more concise to expose a suspend function instead of LiveData. You can privately use a Deferred so the fetch doesn't have to be repeated if the screen rotates (the result will still arrive and be cached in the ViewModel). So I would do:
class MainActivityViewModel #Inject constructor(
private val dataRepository: DataRepository
) : ViewModel() {
private creditReportDeferred = viewModelScope.async { dataRepository.getCreditReport() }
suspend fun getCreditReport() = creditReportDeferred.await()
}
// In fragment:
private fun initViewModel() = lifecycleScope.launch {
viewModel.getCreditReport()
.onSuccess {
showScoreUI(true)
binding.score.text = it.creditReportInfo.score.toString()
binding.maxScoreValue.text = "out of ${it.creditReportInfo.maxScoreValue}"
initDonutView(
it.creditReportInfo.score.toFloat(),
it.creditReportInfo.maxScoreValue.toFloat()
)
}.onFailure {
showScoreUI(false)
showToastMessage()
}
}

SharedViewModel between fragment and its host activity by Koin

I want to achieve the communication between fragment and its host activity by using ViewModel(following: Share data using a ViewModel) to update the UI of activity when shared LiveData changed.
Start with declare ViewModel in the module
MainModule.kt
object MainModule {
val module = module {
viewModel {
MainViewModel()
}
}
}
Then inject it to activity and fragment
MainActivity.kt
private val mainViewModel by viewModel<MainViewModel>()
MainFragment.kt
private val mainViewModel by sharedViewModel<MainViewModel>()
Observe the change of LiveData on activity
MainActivity.kt
mainViewModel.drawerState.observe(this, {
// do something when it changed
})
Update the LiveData when the button(on fragment) clicked
MainFragment.kt
mainButton.setOnClickListener {
mainViewModel.toggleDrawerState()
}
The LiveData declare in ViewModel
MainViewModel.kt
private val _drawerState = MutableLiveData<DrawerState>()
val drawerState: LiveData<DrawerState> = _drawerState
fun toggleDrawerState() {
if (_drawerState.value == DrawerState.OPENED) {
_drawerState.value = DrawerState.CLOSED
} else {
_drawerState.value = DrawerState.OPENED
}
}
DrawerState.kt
enum class DrawerState {
CLOSED, OPENED
}
But It does not work as expected which means nothing happens when the button clicked(can guarantee by debugging with breakpoint). I wondering to know where I've gone wrong or misunderstood. Thank you.

Android ViewModel object recreate with kotlin but not with java

Im new to kotlin, and mvvm, but i was able to make it work in java, but when i made a new example mvvm-retrofit-corutines in kotlin, the view model gets called all the time on the OnCreate function is called, (which shouldn't happen according to docs and works fine in java).
MainActivity:
lateinit var viewModel : MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//Here we can see the logs in every orientation changed in the emulator.
viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
viewModel.getMutableLiveDataModel().observe(this, Observer {
Log.d("zzzz","lamda executes onChanged method -> "+ it.otherValues). //element from model
})
}
MyViewModel:
class MyViewModel : ViewModel() {
private lateinit var objectTypeModel: MutableLiveData<MyTestModel>
fun getMutableLiveDataModel():MutableLiveData<MyTestModel>{
//Gets the model from a retrofit service call
objectTypeModel = MyRepository.getModelFromService()
return objectTypeModel
}
}
Am i doing something wrong? already tried convert 'viewModel' into local variable as suggested in other post.
Java Code, MainActivity
MyViewModel model;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
model = new ViewModelProvider(this).get(MyViewModel.class);
model.getUsers().observe(this, new Observer<Integer>() {
#Override
public void onChanged(Integer users) {
Log.d("zzzz","updated value..")
}
});
}
Model
public class MyViewModel extends ViewModel {
private MutableLiveData<Integer> users;
public LiveData<Integer> getUsers() {
if (users == null) {
users = new MutableLiveData<Integer>();
users.setValue(10);
}
return users;
}
}
If you don't want to recreate view model declare your view model like this
private val model: MyViewModel by activityViewModels()
for more details refer ViewModel
I think the issue lies in your kotlin viewmodel class, if you are not getting the value(unless you have few more issues in other classes)
Fix your kotlin viewmodel class in which data is not set in MutableLiveData, you forgot to add a piece of code.
//Here it is like this
objectTypeModel.value= MyRepository.getModelFromService()
AFAIK onCreate() only gets called when activity is created. So its natural if your viewmodel is getting created again. You can also check it by init{} method in your viewmodel class.
Still if you are not satisfied move your api call from activity's onCreate() method to viewmodels init{} method and just observe the changes from Activity. Your getMutableLiveDataModel() will called once when viewmodel object gets created.
If your java viewmodel example is running as you expected. Then,try to convert the java class to kotlin and run it again(just paste the java code to a kotlin file, it will ask you to convert it), it should work.
I've tried the same concept and as expected, the functionality in Java and Kotlin is identical. In the LogCat, I expected that the log should be printed on every rotation and it does. Now, let me tell you why it happens.
So, as per the documentation ViewModel instance stays alive after the configuration change. Basically, ViewModel uses the same instance if your activity is re-creating numerous times but it's not getting destroyed (calling finish()). But it's not the magic of the ViewModel it's the magic of LiveData.
LiveData is an observable data view holder so it sends the latest preserved value to the active observers on every configuration change which you're observing in the onCreate().
Let me present you my code.
Java
// Activity
public class JavaActivity extends AppCompatActivity {
private static final String TAG = "JavaActivity";
private JavaViewModel javaViewModel;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_java);
// Ignore this listener
findViewById(R.id.go_to_kotlin_activity).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
finish();
}
});
// Main
javaViewModel = new ViewModelProvider(this).get(JavaViewModel.class);
javaViewModel.getJavaLiveData().observe(this, new Observer<Integer>() {
#Override
public void onChanged(Integer integer) {
Log.d(TAG, "onChanged: " + integer);
}
});
}
}
// ViewModel
public class JavaViewModel extends ViewModel {
private MutableLiveData<Integer> javaLiveData;
public LiveData<Integer> getJavaLiveData() {
if(javaLiveData == null) {
javaLiveData = new MutableLiveData<>();
javaLiveData.setValue(10);
}
return javaLiveData;
}
}
Kotlin
// Activity
class KotlinActivity : AppCompatActivity() {
companion object {
private const val TAG = "KotlinActivity"
}
private lateinit var kotlinViewModel: KotlinViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_kotlin)
// Ignore this listener
findViewById<Button>(R.id.go_to_java_activity_btn).setOnClickListener {
startActivity(Intent(this, JavaActivity::class.java))
}
// Main
kotlinViewModel = ViewModelProvider(this).get(KotlinViewModel::class.java)
kotlinViewModel.getKotlinLiveData().observe(this, Observer {
Log.d(TAG, "onCreate: $it")
})
}
}
// ViewModel
class KotlinViewModel : ViewModel() {
private lateinit var kotlinLiveData: MutableLiveData<Int>
fun getKotlinLiveData(): LiveData<Int> {
if (!::kotlinLiveData.isInitialized) {
kotlinLiveData = MutableLiveData()
kotlinLiveData.value = 10
}
return kotlinLiveData
}
}
If you have any follow-up questions, leave them in comments.
Thanks!
References
LiveData - Official Documentation
ViewModel - Official Documentation
This is a great article on how ViewModel works internally.
Do read this article as well
Try
MainActivity
lateinit var viewModel : MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
viewModel.objectTypeModel.observe(this, Observer {
Log.d("zzzz","lamda executes onChanged method -> "+ it.otherValues).
//element from model
})
}
ViewModel
class MyViewModel : ViewModel() {
val objectTypeModel= MutableLiveData<MyTestModel>()
init {
objectTypeModel.value = MyRepository.getModelFromService()
}
}

Android: clean architecture with Room database and LiveData in DAO

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:
Change Repository method findByUsername to return LiveData>
Or call Room's DAO directly from ViewModel skipping repository implementation completely
Both of these options have sufficient drawbacks:
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.
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?
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 ->
// ...
}
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.
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()

Categories

Resources