I have recently completed this (links below) codelabs tutorial which walks through how to implement Room with LiveData and Databinding in Kotlin.
https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/
https://github.com/googlecodelabs/android-room-with-a-view/tree/kotlin
Following on from this, I want to write some tests around the ViewModel, however, the GitHub repository where the code is stored does not contain any (it has a few tests around the DAO, not what I am interested in for now).
The ViewModel I am trying to test looks like this:
class WordViewModel(application: Application) : AndroidViewModel(application) {
private val repository: WordRepository
// Using LiveData and caching what getAlphabetizedWords returns has several benefits:
// - We can put an observer on the data (instead of polling for changes) and only update the
// the UI when the data actually changes.
// - Repository is completely separated from the UI through the ViewModel.
val allWords: LiveData<List<Word>>
init {
val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()
repository = WordRepository(wordsDao)
allWords = repository.allWords
}
/**
* Launching a new coroutine to insert the data in a non-blocking way
*/
fun insert(word: Word) = viewModelScope.launch(Dispatchers.IO) {
repository.insert(word)
}
}
My test class for the ViewModel looks like this:
#RunWith(JUnit4::class)
class WordViewModelTest {
private val mockedApplication = mock<Application>()
#Test
fun checkAllWordsIsEmpty() {
val vm = WordViewModel(mockedApplication)
assertEquals(vm.allWords, listOf<String>())
}
}
I get an error saying java.lang.IllegalArgumentException: Cannot provide null context for the database. This error then points to this line in the WordViewModel: val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao(). To get this to not crash, I believe I need to mock a lot of what is in the ViewModel, which I am fine with.
I would like to be able to run the test above and in the future, I would also like to mock return a list of data when repository.allWords is called. However, I am not sure how to do this. So my question is, how can I mock the following lines from WordViewModel to allow me to do this?
val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()
repository = WordRepository(wordsDao)
allWords = repository.allWords
Related
I started building my app using Room, Flow, LiveData and Coroutines, and have come across something odd: what I'm expecting to be a value flow actually has one null item in it.
My setup is as follows:
#Dao
interface BookDao {
#Query("SELECT * FROM books WHERE id = :id")
fun getBook(id: Long): Flow<Book>
}
#Singleton
class BookRepository #Inject constructor(
private val bookDao: BookDao
) {
fun getBook(id: Long) = bookDao.getBook(id).filterNotNull()
}
#HiltViewModel
class BookDetailViewModel #Inject internal constructor(
savedStateHandle: SavedStateHandle,
private val bookRepository: BookRepository,
private val chapterRepository: ChapterRepository,
) : ViewModel() {
val bookID: Long = savedStateHandle.get<Long>(BOOK_ID_SAVED_STATE_KEY)!!
val book = bookRepository.getBook(bookID).asLiveData()
fun getChapters(): LiveData<PagingData<Chapter>> {
val lastChapterID = book.value.let { book ->
book?.lastChapterID ?: 0L
}
val chapters = chapterRepository.getChapters(bookID, lastChapterID)
return chapters.asLiveData()
}
companion object {
private const val BOOK_ID_SAVED_STATE_KEY = "bookID"
}
}
#AndroidEntryPoint
class BookDetailFragment : Fragment() {
private var queryJob: Job? = null
private val viewModel: BookDetailViewModel by viewModels()
override fun onResume() {
super.onResume()
load()
}
private fun load() {
queryJob?.cancel()
queryJob = lifecycleScope.launch() {
val bookName = viewModel.book.value.let { book ->
book?.name
}
binding.toolbar.title = bookName
Log.i(TAG, "value: $bookName")
}
viewModel.book.observe(viewLifecycleOwner) { book ->
binding.toolbar.title = book.name
Log.i(TAG, "observe: ${book.name}")
}
}
}
Then I get a null value in lifecycleScope.launch while observe(viewLifecycleOwner) gets a normal value.
I think it might be because of sync and async issues, but I don't know the exact reason, and how can I use LiveData<T>.value to get the value?
Because I want to use it in BookDetailViewModel.getChapters method.
APPEND: In the best practice example of Android Jetpack (Sunflower), LiveData.value (createShareIntent method of PlantDetailFragment) works fine.
APPEND 2: The getChapters method returns a paged data (Flow<PagingData<Chapter>>). If the book triggers an update, it will cause the page to be refreshed again, confusing the UI logic.
APPEND 3: I found that when I bind BookDetailViewModel with DataBinding, BookDetailViewModel.book works fine and can get book.value.
LiveData.value has extremely limited usefulness because you might be reading it when no value is available yet.
You’re checking the value of your LiveData before it’s source Flow can emit its first value, and the initial value of a LiveData before it emits anything is null.
If you want getChapters to be based on the book LiveData, you should do a transformation on the book LiveData. This creates a LiveData that under the hood observes the other LiveData and uses that to determine what it publishes. In this case, since the return value is another LiveData, switchMap is appropriate. Then if the source book Flow emits another version of the book, the LiveData previously retrieved from getChapters will continue to emit, but it will be emitting values that are up to date with the current book.
fun getChapters(): LiveData<PagingData<Chapter>> =
Transformations.switchMap(book) { book ->
val lastChapterID = book.lastChapterID
val chapters = chapterRepository.getChapters(bookID, lastChapterID)
chapters.asLiveData()
}
Based on your comment, you can call take(1) on the Flow so it will not change the LiveData book value when the repo changes.
val book = bookRepository.getBook(bookID).take(1).asLiveData()
But maybe you want the Book in that LiveData to be able to be changed when the repo changes, and what you want is that the Chapters LiveData retrieved previously does not change? So you need to manually get it again if you want it to be based on the latest Book? If that's the case, you don't want to be using take(1) there which would prevent the book from appearing updated in the book LiveData.
I would personally in that case use a SharedFlow instead of LiveData, so you could avoid retrieving the values twice, but since you're currently working with LiveData, here's a possible solution that doesn't require you to learn those yet. You could use a temporary Flow of your LiveData to easily get its current or first value, and then use that in a liveData builder function in the getChapters() function.
fun getChapters(): LiveData<PagingData<Chapter>> = liveData {
val singleBook = book.asFlow().first()
val lastChapterID = singleBook.lastChapterID
val chapters = chapterRepository.getChapters(bookID, lastChapterID)
emitSource(chapters)
}
I saw all of the following scenarios in different example projects from Google's Codelabs and other sources and do not fully understand where the values from the LiveData object are retrieved from.
Scenario 1 - Current Understanding:
According to https://developer.android.com/.../viewmodel one reason to use a ViewModel is to store/cache UI related data that I want to re-use after the corresponding UI has been rebuild after a configuration change.
Given the following simplified ViewModel and Repository: After updateName() is called the first time, the LiveData object of _currentName contains a String. If the UI is then rebuild after a screen rotation, the view that needs to display the current name requests it by observing currentName which in turn returns the value of the LiveData object that is contained in the field of the _currentName property. Am I correct?
ViewModel
class NamesViewModel(): ViewModel() {
private val respository = NamesRepository()
private val _currentName: MutableLivedata<String?> = MutableLiveData(null)
val currentName: LiveData<String?> get() = this._currentName
...
// Called as UI event listener.
fun updateName() {
this._currentName.value = this.repository.updateName()
}
}
Repository
class NamesRepository() {
fun updateName(): String {
val nextName: String
...
return nextName
}
}
Scenario 2:
What happens if the UI is rebuild after a screen rotation in the following case? _currentName in the ViewModel 'observes' currentName in the repository, but it still is a property and therefore stores its own LiveData object in its field. When the view then requests currentName from the ViewModel, the value is retrieved from the LiveData object that is contained in the field of the _currentName property in the ViewModel. Is this correct?
ViewModel
class NamesViewModel(): ViewModel() {
private val respository = NamesRepository()
private val _currentName: LiveData<String?> = this.repository.currentName
val currentName: LiveData<String?> get() = this._currentName
...
// Called as UI event listener.
fun updateName() {
this.repository.updateName()
}
}
Repository
class NamesRepository() {
private val _currentName: MutableLivedata<String?> = MutableLiveData(null)
val currentName: LiveData<String?> get() = this._currentName
fun updateName() {
val nextName: String
...
this._currentName.value = nextName
}
}
Scenario 3:
In the following scenario, if the UI is rebuild and a view requests currentNam from the ViewModel, where is the requested value stored? My current understanding is, that currentName falls back to the field of the property _currentName in the repository. Isn't that against the idea of the ViewModel to store relevant UI data to be re-used after a configuration change? In the case below, it might be no problem to retrieve the value from the repository instead of the viewModel, but what if the repository itself retrieves the value directly from a LiveData object that comes from a Room database? Wouldn't a database access take place every time a view requests _currentName from the viewModel?
I hope somebody can clarify the situation more, in order to understand how to cache UI related data in the viewModel the correct way (or at least to understand what are the incorrect ways).
ViewModel
class NamesViewModel(): ViewModel() {
private val respository = NamesRepository()
val currentName: LiveData<String?> get() = this.repository.currentName
...
// Called as UI event listener.
fun updateName() {
this.repository.updateName()
}
}
Repository
class NamesRepository() {
private val _currentName: MutableLivedata<String?> = MutableLiveData(null)
val currentName: LiveData<String?> get() = this._currentName
fun updateName() {
val nextName: String
...
this._currentName.value = nextName
}
}
To answer your question scenario#1 is correct usage of LiveData.
Firstly, LiveData is not responsible for caching, it is just LifeCycleAware Observable, given that caching is done at ViewModel, when your activity recreates due to any configuration changes, android will try to retrieve the existing instance of ViewModel, if found then it's state and data are retained as is else it will create a new instance of ViewModel.
Second, using LiveData in repository is a bad idea at many levels, repository instances are held by ViewModel and LiveData are part of Android Framework which makes repositories rely on Android Framework thus creating problems in Unit Testing. Always use LiveData only in ViewModels.
In my project I'm using a slightly modified repository pattern:
DataSource (e.g. API, database). Provides CRUD of entities
Repository of specific data (e.g. UserRepository, SettingsRepository) that handles reconciliation of DataSources (e.g. update database from API call). Provides basic functionality above CRUD
ViewModel that uses repositories and creates a flow out of the calls of repositories (e.g. use UserRepository to sync user data, then SettingsRepository to sync settings for the user)
View is databound
In my Repositories, I use exposed LiveData<*> fields to communicate state - e.g. said UserRepository would have a currentUser field of public type LiveData, privately MediatorLiveData, and it would be linked to a private field that holds the current user ID to be retrieved.
However, these subscriptions (using MediatorLiveData's addSource() {} method) for some reason do not fire.
An almost 1:1 example (replaced model name due to NDA) would be the following:
abstract class BaseRepository: ViewModel(), KoinComponent {
val isLoading: LiveData<Boolean> = MutableLiveData<Boolean>().apply { postValue(false) }
}
class UserRepository: BaseRepository() {
private val client: IClient by inject() // Koin injection of API client
private val sharedPref: SharedPrefManager by inject() // custom wrapper around SharedPreferences
private val currentUserId = MutableLiveData()
val currentUser: LiveData<User> = MediatorLiveData()
val users: LiveData<List<User>> = MutableLiveData()
init {
(currentUser as MediatorLiveData).addSource(currentUserId) { updateCurrentUser() }
(currentUser as MediatorLiveData).addSource(users) { updateCurrentUser() }
(currentUserId as MutableLiveData).postValue(sharedPref.getCurrentUserId())
// sharedPref.getCurrentUserId() will return UUID? - null if
}
fun updateCurrentUser() {
// Here I have the logic deciding which user to push into `currentUser` based on the list of users, and if there's a `currentUserId` present.
}
}
With this example implemented, updateCurrentUser() never gets called, even though the subscription to the other LiveData fields happens and is visible while debugging on the currentUser object.
The same subscription via addSource works just fine in other repositories, and they're constructed the very same way as above.
What could be going wrong here?
MediatorLiveData will not observe source LiveData if it does not have any observer subscribed to itself. updateCurrentUser() will be called as soon as you subscribe to currentUser.
I am trying to build an Android app following the recommended design structure.
Let's say, there is a UserRepository for handling the users. However, I would like to have certain settings in the app, for example "Show profile picture", "Sort by", etc. I would like to store these settings in a Room database, just like the Users.
According to my understanding, the cleanest way is to have a separate UserRepository and a SettingsRepository. And of course, the Settings should have a sort of Model, let's call it SettingsModel, in order to be able to retrieve the Settings as a Map for example. Note, that this is not a ViewModel, it has nothing to do with the UI.
Then, the UserRepository should implement its own business (handling the users), just like in the example linked above. Besides that, it also should have a dependency of that SettingsModel, so that it can easily retrieve the settings, which affect how the Users should be retrieved.
The SettingsModel needs to turn the "raw database data" into a map, so that I can reach the settings like this: settings.show_profile_pictures and settings.sort_by, etc.
To achieve this, I need to extract the data from the LiveData, which implies, that I need to observe that LiveData, so that I can update the Map, whenever the settings change.
And here comes the problem: the observe() method needs a LifecycleOwner, which I cannot provide in my tests.
1st attempt: mock with Mockito
It would be an instrumented test, becase that way I have access to an Activity, which is needed to retrieve the DAO.
I am trying to #Inject it with Dagger, but the mocking wasn't successful:
class SettingsRepositoryTest {
private lateinit var settingsDao: SettingsDao
#Mock
private lateinit var mockLifecycleOwner: LifecycleOwner
#Before
fun createDb(){
MockitoAnnotations.initMocks(LifecycleOwner::class.java)
val appContext = InstrumentationRegistry.getTargetContext()
val db = Room.inMemoryDatabaseBuilder(appContext, CurrencyConverterDb::class.java).allowMainThreadQueries().build()
settingsDao = db.settingsDao()
}
#Test
fun testSettingsMap() {
val repo = SettingsRepository(settingsDao, mockLifecycleOwner) // throws the exception here
}
}
The exception:
kotlin.UninitializedPropertyAccessException: lateinit property mockLifecycleOwner has not been initialized
at com.helmet91.currencyconverter.repositories.SettingsRepositoryTestInst.testSettingsMap(SettingsRepositoryTestInst.kt:46)
2nd attempt: use Roboelectric to create an AppCompatActivity, which is in fact a LifecycleOwner.
It is not an instrumented test, because Roboelectric doesn't work in the androidTest environment.
The Activity still has to be mocked, however, it throws a NullPointerException. The only way I can think of, is to go through this Exception stack, and mock everything in it, if it's even possible at all. But that sounds insane to me. There has to be a better solution.
class SettingsRepositoryTest {
private lateinit var settingsDao: SettingsDao
private lateinit var activity: AppCompatActivity
#Before
fun createDb(){
val built = Robolectric.buildActivity(MainActivity::class.java) // throws the exception here
val created = built.create()
val controller = created.start()
activity = controller.get() as AppCompatActivity
val db = Room.inMemoryDatabaseBuilder(activity, CurrencyConverterDb::class.java).allowMainThreadQueries().build()
settingsDao = db.settingsDao()
}
#Test
fun testSettingsMap() {
val repo = SettingsRepository(settingsDao, activity)
val settingsMap = repo.getSettings()
val settingsEntity = Settings(1, "show_flags", "1", "bool")
settingsDao.insert(settingsEntity)
assertTrue(settingsMap.show_flags)
}
}
The exception:
java.lang.NullPointerException
at org.robolectric.internal.bytecode.ShadowImpl.extract(ShadowImpl.java:17)
at org.robolectric.shadow.api.Shadow.extract(Shadow.java:25)
at org.robolectric.Shadows.shadowOf(Shadows.java:1215)
at org.robolectric.shadows.CoreShadowsAdapter.getMainLooper(CoreShadowsAdapter.java:23)
at org.robolectric.android.controller.ComponentController.<init>(ComponentController.java:29)
at org.robolectric.android.controller.ComponentController.<init>(ComponentController.java:21)
at org.robolectric.android.controller.ActivityController.<init>(ActivityController.java:33)
at org.robolectric.android.controller.ActivityController.of(ActivityController.java:25)
at org.robolectric.Robolectric.buildActivity(Robolectric.java:97)
at org.robolectric.Robolectric.buildActivity(Robolectric.java:93)
at com.helmet91.currencyconverter.repositories.SettingsRepositoryTest.createDb(SettingsRepositoryTest.kt:29)
Is it really impossible to test anything, that involves Lifecycle components?
I am currently applying Room + ViewModel + LiveData to my project.
In my app, there is "obviously" observe data that is needed, but not all.
The code below is example code for category data. In my situation, category data does not change and always maintains the same value state (13 categories and content does not change). Categories are data that is loaded from the Database through the CategoryItemDao class.
Does category data need to be wrapped with livedata?
Or is there a reason enough to use LiveData in addition to its observerable feature?
I've read the guide to LiveData several times, but I do not understand the exact concept.
CategoryItemDao
#Dao
interface CategoryItemDao {
#Query("SELECT * FROM CategoryItem")
fun getAllCategoryItems(): LiveData<MutableList<CategoryItem>>
}
CategoryRepository
class CategoryRepository(application: Application) {
private val categoryItemDao: CategoryItemDao
private val allCategories: LiveData<MutableList<CategoryItem>>
init {
val db = AppDatabase.getDatabase(application)
categoryItemDao = db.categoryItemDao()
allCategories = categoryItemDao.getAllCategoryItems()
}
fun getAllCategories() = allCategories
}
CategoryViewModel
class CategoryViewModel(application: Application) : AndroidViewModel(application) {
private val repository = CategoryRepository(application)
private val allCategories: LiveData<MutableList<CategoryItem>>
init {
allCategories = repository.getAllCategories()
}
fun getAllCategories() = allCategories
}
This is fine, but you can make a few changes:
Change LiveData<MutableList<CategoryItem>> to LiveData<List<CategoryItem>>. Don't use a MutableList unless you really have to. In your case, List would work fine.
In your CategoryRepository instead of fetching in init, do it during the getAllCategories() call. So change your code like this: fun getAllCategories() = categoryItemDao.getAllCategoryItems()
Similarly do the same in CategoryViewModel as well. Change you code to: fun getAllCategories() = repository.getAllCategories()
A common misconception is to use LiveData only when the data changes. But that's not true. Your 13 categories may not change, but that's in a database. So if you were to accomplish this without a LiveData you have to query the DB and populate the view in the main thread, or you need to wrap this around in a background thread. But if you do this via LiveData, you get the Asynchronous Reactive way of coding for free. Whenever possible, try to make your view observe a LiveData.