MutableLiveData with initial value from LiveData - android

I have a situation where I have a view model for a page that is rendered based on some input data (id) with fields that are initialized from my repository but can be edited by the user.
My repository function is pretty straightforward:
class AccountsRepository {
public LiveData<Account> getAccount(long id) {
return roomDb.accountDao().getAccount(id);
}
}
I want my view model to be re-used for different accounts so I have a LiveData<Long> that will be set with the account ID.
class EditAccountViewModel {
private MutableLiveData<Long> accountId = new MutableLiveData<>();
public void setAccountId(long id) {
accountId.setValue(id);
}
}
In my view model, I want to expose a mutable field name that will be bound to an EditText view. This field should be initialized by the data in the repository. If I use a simple non-editable binding, I am able to get one-way databinding working:
class EditAccountViewModel {
private MutableLiveData<Long> accountId = new MutableLiveData<>();
public LiveData<String> name;
EditAccountViewModel() {
this.name = Transformations.map(
Transformations.switchMap(accountId, repo::getAccount),
account -> account.name);
}
}
However, I cannot bind this using #={viewModel.name} because it complains that it does not know how to set the value. I tried writing a helper class like so that uses an underlying MediatorLiveData to set the value, but it looks like my onChanged callback is never called:
class MutableLiveDataWithInitialValue<T> extends MutableLiveData<T> {
MutableLiveDataWithInitialValue(LiveData<T> initialValue) {
MediatorLiveData<T> mediator = new MediatorLiveData<>();
mediator.addSource(
initialValue,
data -> {
// This never gets called per the debugger.
mediator.removeSource(initialValue);
setValue(data);
});
}
}
I updated the view model as follows:
class EditAccountViewModel {
private MutableLiveData<Long> accountId = new MutableLiveData<>();
public MutableLiveData<String> name;
EditAccountViewModel() {
this.name = new MutableLiveDataWithInitialValue<>(
Transformations.map(
Transformations.switchMap(accountId, repo::getAccount),
account -> account.name));
}
}
However, when I do this my EditText field never sets the value from the database, and this makes sense because the onChanged callback is never called in my MediatorLiveData in MutableLiveDataWithInitialValue.
This seems like a pretty common use case, so I'm wondering what I am screwing up?

I was able to workaround this with a very inelegant manner:
class EditAccountViewModel {
private final Executor ioExecutor;
public final MutableLiveData<String> name = new MutableLiveData<>();
public final MutableLiveData<String> someOtherField = new MutableLiveData<>();
public void setAccountId(long id) {
ioExecutor.execute(() -> {
Account account = repo.getAccountSync(id);
name.postValue(account.name);
someOtherField.postValue(account.someOtherField);
});
}
}
This is fine since only the user can edit the fields once they are initialized. But there are obvious race conditions... e.g. if the database takes too long to rade and the user starts typing a value in before then.

Your helper class was a good start. Seems like you've forgotten that LiveData objects (including your MediatorLiveData) aren't triggered unless you observe them - that's why your mediator's onChange() method was never called. If you pass observe/unobserve actions to your mediator it works as intended.
Since you're tying your ViewModel to a view it might be a good idea to provide at least some initial value in the constructor like you would do on the regular MutableLiveData (even if that value is shortly replaced by the one coming from LiveData).
Java:
public class MutableLiveDataWithInitialValue<T> extends MutableLiveData<T> {
private MediatorLiveData<T> mediator = new MediatorLiveData<>();
public MutableLiveDataWithInitialValue(T initialValue, LiveData<T> delayedInitialValue) {
super(initialValue);
mediator.addSource(
delayedInitialValue,
data -> {
mediator.removeSource(delayedInitialValue);
setValue(data);
});
}
#Override
public void observe(#NonNull LifecycleOwner owner, #NonNull Observer<? super T> observer) {
mediator.observe(owner, observer);
super.observe(owner, observer);
}
#Override
public void observeForever(#NonNull Observer<? super T> observer) {
mediator.observeForever(observer);
super.observeForever(observer);
}
#Override
public void removeObserver(#NonNull Observer<? super T> observer) {
mediator.removeObserver(observer);
super.removeObserver(observer);
}
#Override
public void removeObservers(#NonNull LifecycleOwner owner) {
mediator.removeObservers(owner);
super.removeObservers(owner);
}
}
Kotlin:
class MutableLiveDataWithInitialValue<T>(initialValue: T, delayedInitialValue: LiveData<T>) : MutableLiveData<T>(initialValue) {
private val mediator = MediatorLiveData<T>()
init {
mediator.addSource(delayedInitialValue) {
mediator.removeSource(delayedInitialValue)
value = it
}
}
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
mediator.observe(owner, observer)
super.observe(owner, observer)
}
override fun observeForever(observer: Observer<in T>) {
mediator.observeForever(observer)
super.observeForever(observer)
}
override fun removeObserver(observer: Observer<in T>) {
mediator.removeObserver(observer)
super.removeObserver(observer)
}
override fun removeObservers(owner: LifecycleOwner) {
mediator.removeObservers(owner)
super.removeObservers(owner)
}
}

Related

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

What do I supply to "LifeCycleOwner" parameter on observe in Kotlin?

I'm fairly new on the Android Room, which uses LiveData that I'm also not familiar with. I noticed that on the tutorials, the data returned is using LiveData wrapper, like this:
#Dao
interface PersonDao {
#Query("SELECT * FROM person")
fun getAll(): LiveData<List<Person>>
}
Then to read the data, I use this code:
class PersonListActivity: AppCompatActivity() {
List<Person> personList = listOf()
init {
val db = RoomDatabase.getDatabaseInstance()
db.personDao.getAll().observe(this, Observe<List<Person>> { data ->
personList = data
})
}
}
The problem is the IDE raise error "type mismatch". Required: LifeCycleOwner. Found: PersonListActivity. I don't understand how the tutorials can casually supply "this" into the observe owner parameter. I've also tried to supply context and applicationContext into the owner parameter and it doesn't work.
After I examine the LifeCycleOwner class, I tried to add the LifeCycleOwner implementation. But then the class requires getLifeCycle() function to be implemented. So I'm back at zero.
class PersonListActivity: AppCompatActivity(), LifeCycleOwner {
List<Person> personList = listOf()
init {
val db = RoomDatabase.getDatabaseInstance()
db.personDao.getAll().observe(this, Observe<List<Person>> { data ->
personList = data
})
}
override fun getLifeCycle() {
// what should I return here??????
}
}
Why all the tutorials I read about LiveData don't mention anything at all about LifeCycleOwner? Am I using the wrong observe function here?
public abstract class LiveData<T> {
...
#MainThread
public void observe(#NonNull LifecycleOwner owner, #NonNull Observer<? super T> observer) { ... }
...
}
Since the version 1.1.0 (excluding the not-stable releases) of androidx.appcompat:appcompat, AppCompatActivity implements LifecycleOwner (see ComponentActivity). So you can use this when calling:
db.personDao.getAll().observe(this, Observe<List<Person>> { data ->
personList = data
})
Without implementing anything else.
Furthermore, I would move those lines in Activity.onCreate().

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

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.

Should I make AsyncTask member of LiveData or Repository class - As replacement of Loader

LiveData/ ViewModel is a good replacement for complicated Loader.
Based on https://medium.com/google-developers/lifecycle-aware-data-loading-with-android-architecture-components-f95484159de4 ,
AsyncTask is member of LiveData.
public class JsonLiveData extends LiveData<List<String>> {
public JsonLiveData(Context context) {
loadData();
}
private void loadData() {
new AsyncTask<Void,Void,List<String>>() {
}.execute();
}
}
However, based on the presentation from Lyla Fujiwara:
Should I make AsyncTask member of Repository class?
You should avoid running your AsyncTask in LiveData. LiveData should really only be concerned with the observation of data. Not the act of changing the data.
The best way of dealing with this situation is to use the ViewModel / Repository pattern.
Activity / Fragment observes LiveData from ViewModel, ViewModel observes LiveData from Repository. Changes are made in the repository, which are pushed to its LiveData. Those changes are delivered to the Activity / Fragment (through the ViewModel).
I would avoid using AsyncTask in this situation. The bonus of AsyncTask is that you can get results on the UI thread after doing work. In this case though, that isn't necessary. LiveData will do that for you.
Here is an (untested) example:
Activity
public class MyActivity extends AppCompatActivity {
private MyViewModel viewModel;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Set up your view model
viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
// Observe the view model
viewModel.getMyLiveData().observe(this, s -> {
// You work with the data provided through the view model here.
// You should only really be delivering UI updates at this point. Updating
// a RecyclerView for example.
Log.v("LIVEDATA", "The livedata changed: "+s);
});
// This will start the off-the-UI-thread work that we want to perform.
MyRepository.getInstance().doSomeStuff();
}
}
ViewModel
public class MyViewModel extends AndroidViewModel {
#NonNull
private MyRepository repo = MyRepository.getInstance();
#NonNull
private LiveData<String> myLiveData;
public MyViewModel(#NonNull Application application) {
super(application);
// The local live data needs to reference the repository live data
myLiveData = repo.getMyLiveData();
}
#NonNull
public LiveData<String> getMyLiveData() {
return myLiveData;
}
}
Repository
public class MyRepository {
private static MyRepository instance;
// Note the use of MutableLiveData, this allows changes to be made
#NonNull
private MutableLiveData<String> myLiveData = new MutableLiveData<>();
public static MyRepository getInstance() {
if(instance == null) {
synchronized (MyRepository.class) {
if(instance == null) {
instance = new MyRepository();
}
}
}
return instance;
}
// The getter upcasts to LiveData, this ensures that only the repository can cause a change
#NonNull
public LiveData<String> getMyLiveData() {
return myLiveData;
}
// This method runs some work for 3 seconds. It then posts a status update to the live data.
// This would effectively be the "doInBackground" method from AsyncTask.
public void doSomeStuff() {
new Thread(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException ignored) {
}
myLiveData.postValue("Updated time: "+System.currentTimeMillis());
}).start();
}
}

Unit testing Room and LiveData

I'm currently developing an app using the newly Android Architecture Components. Specifically, I'm implementing a Room Database that returns a LiveData object on one of its queries. Insertion and querying work as expected, however I have an issue testing the query method using a unit test.
Here is the DAO I'm trying to test:
NotificationDao.kt
#Dao
interface NotificationDao {
#Insert
fun insertNotifications(vararg notifications: Notification): List<Long>
#Query("SELECT * FROM notifications")
fun getNotifications(): LiveData<List<Notification>>
}
As you can tell, the query function returns a LiveData object, if I change this to be just a List, Cursor, or basically whatever then I get the expected result, which is the data inserted in the Database.
The issue is that the following test will always fail because the value of the LiveData object is always null:
NotificationDaoTest.kt
lateinit var db: SosafeDatabase
lateinit var notificationDao: NotificationDao
#Before
fun setUp() {
val context = InstrumentationRegistry.getTargetContext()
db = Room.inMemoryDatabaseBuilder(context, SosafeDatabase::class.java).build()
notificationDao = db.notificationDao()
}
#After
#Throws(IOException::class)
fun tearDown() {
db.close()
}
#Test
fun getNotifications_IfNotificationsInserted_ReturnsAListOfNotifications() {
val NUMBER_OF_NOTIFICATIONS = 5
val notifications = Array(NUMBER_OF_NOTIFICATIONS, { i -> createTestNotification(i) })
notificationDao.insertNotifications(*notifications)
val liveData = notificationDao.getNotifications()
val queriedNotifications = liveData.value
if (queriedNotifications != null) {
assertEquals(queriedNotifications.size, NUMBER_OF_NOTIFICATIONS)
} else {
fail()
}
}
private fun createTestNotification(id: Int): Notification {
//method omitted for brevity
}
So the question is: Does anyone knows of a better way to perform unit tests that involve LiveData objects?
Room calculates the LiveData's value lazily when there is an observer.
You can check the sample app.
It uses a getValue utility method which adds an observer to get the value:
public static <T> T getOrAwaitValue(final LiveData<T> liveData) throws InterruptedException {
final Object[] data = new Object[1];
final CountDownLatch latch = new CountDownLatch(1);
Observer<T> observer = new Observer<T>() {
#Override
public void onChanged(#Nullable T o) {
data[0] = o;
latch.countDown();
liveData.removeObserver(this);
}
};
liveData.observeForever(observer);
latch.await(2, TimeUnit.SECONDS);
//noinspection unchecked
return (T) data[0];
}
Better w/ kotlin, you can make it an extensions function :).
When you return a LiveData from a Dao in Room it makes the query asynchronously, and as #yigit said Room sets the LiveData#value lazily after you kick off the query by observing the LiveData. This pattern is reactive.
For unit tests you want the behavior to be synchronous, so you must block the test thread and wait for the value to be passed to the observer, then grab it from there and then you can assert on it.
Here's a Kotlin extension function for doing this:
private fun <T> LiveData<T>.blockingObserve(): T? {
var value: T? = null
val latch = CountDownLatch(1)
val observer = Observer<T> { t ->
value = t
latch.countDown()
}
observeForever(observer)
latch.await(2, TimeUnit.SECONDS)
return value
}
You can use it like this:
val someValue = someDao.getSomeLiveData().blockingObserve()
I found Mockito is very helpful in such case. Here is an example:
1.Dependencies
testImplementation "org.mockito:mockito-core:2.11.0"
androidTestImplementation "org.mockito:mockito-android:2.11.0"
2.Database
#Database(
version = 1,
exportSchema = false,
entities = {Todo.class}
)
public abstract class AppDatabase extends RoomDatabase {
public abstract TodoDao todoDao();
}
3.Dao
#Dao
public interface TodoDao {
#Insert(onConflict = REPLACE)
void insert(Todo todo);
#Query("SELECT * FROM todo")
LiveData<List<Todo>> selectAll();
}
4.Test
#RunWith(AndroidJUnit4.class)
public class TodoDaoTest {
#Rule
public TestRule rule = new InstantTaskExecutorRule();
private AppDatabase database;
private TodoDao dao;
#Mock
private Observer<List<Todo>> observer;
#Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
Context context = InstrumentationRegistry.getTargetContext();
database = Room.inMemoryDatabaseBuilder(context, AppDatabase.class)
.allowMainThreadQueries().build();
dao = database.todoDao();
}
#After
public void tearDown() throws Exception {
database.close();
}
#Test
public void insert() throws Exception {
// given
Todo todo = new Todo("12345", "Mockito", "Time to learn something new");
dao.selectAll().observeForever(observer);
// when
dao.insert(todo);
// then
verify(observer).onChanged(Collections.singletonList(todo));
}
}
Hope this help!
As #Hemant Kaushik said, in this case you SHOULD use InstantTaskExecutorRule.
From developer.android.com:
A JUnit Test Rule that swaps the background executor used by the Architecture Components with a different one which executes each task synchronously.
It really works!
Slightly different approach than other answers might be to use https://github.com/jraska/livedata-testing.
You avoid mocking and the test can use API similar to RxJava testing and also you can get advantage from Kotlin extension functions.
NotificationDaoTest.kt
val liveData = notificationDao.getNotifications()
liveData.test()
.awaitValue() // for the case where we need to wait for async data
.assertValue { it.size == NUMBER_OF_NOTIFICATIONS }
If you are using JUnit 5, since rules are not applicable to it, thanks to this article you can manually create the extension:
class InstantExecutorExtension : BeforeEachCallback, AfterEachCallback {
override fun beforeEach(context: ExtensionContext?) {
ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() {
override fun executeOnDiskIO(runnable: Runnable) = runnable.run()
override fun postToMainThread(runnable: Runnable) = runnable.run()
override fun isMainThread(): Boolean = true
})
}
override fun afterEach(context: ExtensionContext?) {
ArchTaskExecutor.getInstance().setDelegate(null)
}
}
and then in your test class use it like this:
#ExtendWith(InstantExecutorExtension::class /* , Other extensions */)
class ItemDaoTests {
...
}

Categories

Resources