Apparently, Room is not able to handle MutableLiveData and we have to stick to LiveData as it returns the following error:
error: Not sure how to convert a Cursor to this method's return type
I created a "custom" MutableLiveData in my DB helper this way:
class ProfileRepository #Inject internal constructor(private val profileDao: ProfileDao): ProfileRepo{
override fun insertProfile(profile: Profile){
profileDao.insertProfile(profile)
}
val mutableLiveData by lazy { MutableProfileLiveData() }
override fun loadMutableProfileLiveData(): MutableLiveData<Profile> = mutableLiveData
inner class MutableProfileLiveData: MutableLiveData<Profile>(){
override fun postValue(value: Profile?) {
value?.let { insertProfile(it) }
super.postValue(value)
}
override fun setValue(value: Profile?) {
value?.let { insertProfile(it) }
super.setValue(value)
}
override fun getValue(): Profile? {
return profileDao.loadProfileLiveData().getValue()
}
}
}
This way, I get the updates from DB and can save the Profile object but I cannot modify attributes.
For example:
mutableLiveData.value = Profile() would work.
mutableLiveData.value.userName = "name" would call getValue() instead postValue() and wouldn't work.
Did anyone find a solution for this?
Call me crazy but AFAIK there is zero reason to use a MutableLiveData for the object that you received from the DAO.
The idea is that you can expose an object via LiveData<List<T>>
#Dao
public interface ProfileDao {
#Query("SELECT * FROM PROFILE")
LiveData<List<Profile>> getProfiles();
}
Now you can observe them:
profilesLiveData.observe(this, (profiles) -> {
if(profiles == null) return;
// you now have access to profiles, can even save them to the side and stuff
this.profiles = profiles;
});
So if you want to make this live data "emit a new data and modify it", then you need to insert the profile into the database. The write will re-evaluate this query and it will be emitted once the new profile value is written to db.
dao.insert(profile); // this will make LiveData emit again
So there is no reason to use getValue/setValue, just write to your db.
If you really need to, then you can use the mediator trick.
In your ViewModel
val sourceProduct: LiveData<Product>() = repository.productFromDao()
val product = MutableLiveData<Product>()
val mediator = MediatorLiveData<Unit>()
init {
mediator.addSource(sourceProduct, { product.value = it })
}
In fragment/activity
observe(mediator, {})
observe(product, { /* handle product */ })
A Kotlin extension to trasform LiveData into MutableLiveData:
/**
* Transforms a [LiveData] into [MutableLiveData]
*
* #param T type
* #return [MutableLiveData] emitting the same values
*/
fun <T> LiveData<T>.toMutableLiveData(): MutableLiveData<T> {
val mediatorLiveData = MediatorLiveData<T>()
mediatorLiveData.addSource(this) {
mediatorLiveData.value = it
}
return mediatorLiveData
}
Since Room doesn't support MutableLiveData and has support for LiveData only, your approach of creating a wrapper is the best approach I can think of. It will be complicated for Google to support MutableLiveDatasince the setValue and postValue methods are public. Where as for LiveData they are protected which gives more control.
In your repository you can get LiveData and transform it to MutableLivedata:
var data= dao.getAsLiveData()
return MutableLiveData<T>(data.value)
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 was following one of the UDACITYs Android Tutorial on LiveData/Room/Persistence and Repository Architecture.
After gluing the codes all together, I came across (what I believe, a very common issue) Type Mismatch exception.
On the course example, a VideosRepository was created with a member videos which is a LiveData:
class VideosRepository(private val database: VideosDatabase) {
/**
* A playlist of videos that can be shown on the screen.
*/
val videos: LiveData<List<Video>> =
Transformations.map(database.videoDao.getVideos()) {
it.asDomainModel()
}
and in the Model, I have a introduce a MutableLiveData of _video
val playlist = videosRepository.videos //works fine
// added by me
private val _video = MutableLiveData<List<Video>>()
val video: LiveData<List<Video>> = _video
When I tried to access the LiveData, this is where I am getting the Type mismatch.
fun sample(){
_video.value = videosRepository.videos //does not work and throws a Type mismatch.
//Required: List<Video> Found: LiveData<List<Video>>
}
And if I try to just stuff all LiveData in the ViewModel (meaning, only the ViewModel will have the LiveData object declarations) and converting all LiveData to just plain List and a function such as
fun getVideos(): List<Video>{
return database.videoDao.getVideo()
}
I would then get Cannot access database on the main thread since it may potentially lock the UI for a long period of time. which I understand clearly. So if that is the case, then LiveData is the only way to do it.
But how can I get away from the Type mismatch.
PS. I understand concepts of OOP as well as Java, but never had the in-depth hands-on experience, so please bear with me.
Input of _video.value is a List<Video> but you assigned videosRepository.videos that is a LiveData<List<Video>>
You have to get List<Video> from LiveData :
_video.value = videosRepository.videos.value
videosRepository.videos's data type is LiveData<List<Video>> but _video.value's data type is List<Video>, so you can't assign like that.
Try:
val video: LiveData<List<Video>> = videosRepository.videos
Then in the view, observe the livedata to do what you want with the data, an example in Fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
viewModel.video.observe(viewLifecycleOwner, Observer {
val data: List<Video> = it
// Do something with the data such as showing it...
})
}
If you really want to have a MutableLiveData in case of modifying it later, use MediatorLiveData:
private val _video = MediatorLiveData<List<Video>>().apply {
addSource(videosRepository.videos) {
value = it
}
}
val video: LiveData<List<Video>> = _video
I am using Room for handling db entities and I am adapting the code from the WordRoom example from adroid developers.
I understand that in order to eprform operations that can take a long time I have to use coroutines, and this seems to work fine for inserting and deleting objects into the database. In the main activity I have a recyclerview that onCreate gets binded to its layoutmanager and to the ViewModelProvider.
In the adapter I set an onClick listener to get the current ID of the object in the recycled view>
holder.mealItemView.setOnClickListener {
(callerContext as MainActivity).getID(current)
}
the getID from the main activity starts a new activity that should retrieve the element from the database and display its properties:
fun getID(meal:Meal){
val intent = Intent(applicationContext, ActivtyViewMeal::class.java)
intent.putExtra("mealId", meal.id.toString())
startActivity(intent)
}
Then in the ActivityViewMeal in the oncreate I get the intent and add an observer to the variable that should store the Entity from the database:
mealViewModel = ViewModelProvider(this).get(MealViewModel::class.java)
mealViewModel.aMeal.observe(this, Observer {meal ->
meal?.let {Log.d(...)
dataIn.text = it.mealAddDate.toString()})
mealViewModel.getSingleContentById(mealID.toInt())
and after binding the variable with the observer I try to retrieve the data.
My issue is that the log never gets executed.
aMeal is declared inside the ViewModel, a separate kotlin class:
class MealViewModel(application: Application) : AndroidViewModel(application) {
private val repository: MealRepository
val allMeals: LiveData<List<Meal>>
var aMeal: LiveData<Meal>
init {
val mealsDao = MealRoomDatabase.getDatabase(application, viewModelScope).mealDao()
repository = MealRepository(mealsDao)
allMeals = repository.allMeals
aMeal = repository.aMeal
}
...
fun getSingleContentById(id: Int)=viewModelScope.launch(Dispatchers.IO) {
repository.getSingleContentById(id)
}
}
I understand that the coroutine cannot return a value and shall not block.
The meal repository class is defined as follow:
/**
* Abstracted Repository as promoted by the Architecture Guide.
* https://developer.android.com/topic/libraries/architecture/guide.html
*/
class MealRepository(private val mealDao: MealDao) {
val allMeals: LiveData<List<Meal>> = mealDao.getAllContentById()
var aMeal: LiveData<Meal> = mealDao.getSingleContentById(0) //init to avoid null pointer errors
...
#Suppress("RedundantSuspendModifier")
#WorkerThread
fun getSingleContentById(id:Int){
aMeal = mealDao.getSingleContentById(id)
// no return from here
}
and to conclude, getSingleContentById is defined inside the DAO class, that is in another kotlin file.
#Query("SELECT * FROM meals_table WHERE id=:id")
fun getSingleContentById(id:Int):LiveData<Meal>
I have no hint on why I don't get the aMeal updated, while the variable allmeals gets updated correctly.
Any hint would be gladly appreciated.
You can change you DB operation to be suspend and return Meal object:
#Query("SELECT * FROM meals_table WHERE id=:id")
suspend fun getSingleContentById(id:Int): Meal
In your repository make getSingleContentById function suspend as well:
class MealRepository(private val mealDao: MealDao) {
suspend fun getSingleContentById(id: Int): Meal {
aMeal = mealDao.getSingleContentById(id)
return aMeal
}
}
In your MealViewModel make aMeal of type MutableLiveData and update it in getSingleContentById function:
class MealViewModel(application: Application) : AndroidViewModel(application) {
private val repository: MealRepository
val aMeal: MutableLiveData<Meal> = MutableLiveData()
init {
val mealsDao = MealRoomDatabase.getDatabase(application, viewModelScope).mealDao()
repository = MealRepository(mealsDao)
}
fun getSingleContentById(id: Int) = viewModelScope.launch(Dispatchers.Main) {
val meal = repository.getSingleContentById(id)
aMeal.postValue(meal)
}
}
The docs show how you can perform Transformations on a LiveData object? How can I perform a transformation like map() and switchMap() on a MutableLiveData object instead?
MutableLiveData is just a subclass of LiveData. Any API that accepts a LiveData will also accept a MutableLiveData, and it will still behave the way you expect.
Exactly the same way:
fun viewModelFun() = Transformations.map(mutableLiveData) {
//do somethinf with it
}
Perhaps your problem is you dont know how does yor mutable live data fit on this.
In the recent update mutable live data can start with a default value
private val form = MutableLiveData(Form.emptyForm())
That should trigger the transformation as soon as an observer is attached, because it will have a value to dispatch.
Of maybe you need to trigger it once the observer is attached
fun viewModelFun(selection: String) = liveData {
mutableLiveData.value = selection.toUpperCase
val source = Transformations.map(mutableLiveData) {
//do somethinf with it
}
emitSource(source)
}
And if you want the switch map is usually like this:
private val name = MutableLiveData<String>()
fun observeNames() = Transformations.switchMap(name) {
dbLiveData.search(name) //a list with the names
}
fun queryName(likeName: String) {
name.value = likeName
}
And in the view you would set a listener to the edit text of the search
searchEt.doAfterTextChange {...
viewModel.queryName(text)
}
I am using the Android Paging Library like described here:
https://developer.android.com/topic/libraries/architecture/paging.html
But i also have an EditText for searching Users by Name.
How can i filter the results from the Paging library to display only matching Users?
You can solve this with a MediatorLiveData.
Specifically Transformations.switchMap.
// original code, improved later
public void reloadTasks() {
if(liveResults != null) {
liveResults.removeObserver(this);
}
liveResults = getFilteredResults();
liveResults.observeForever(this);
}
But if you think about it, you should be able to solve this without use of observeForever, especially if we consider that switchMap is also doing something similar.
So what we need is a LiveData<SelectedOption> that is switch-mapped to the LiveData<PagedList<T>> that we need.
private final MutableLiveData<String> filterText = savedStateHandle.getLiveData("filterText")
private final LiveData<List<T>> data;
public MyViewModel() {
data = Transformations.switchMap(
filterText,
(input) -> {
if(input == null || input.equals("")) {
return repository.getData();
} else {
return repository.getFilteredData(input); }
}
});
}
public LiveData<List<T>> getData() {
return data;
}
This way the actual changes from one to another are handled by a MediatorLiveData.
I have used an approach similar to as answered by EpicPandaForce. While it is working, this subscribing/unsubscribing seems tedious. I have started using another DB than Room, so I needed to create my own DataSource.Factory anyway. Apparently it is possible to invalidate a current DataSource and DataSource.Factory creates a new DataSource, that is where I use the search parameter.
My DataSource.Factory:
class SweetSearchDataSourceFactory(private val box: Box<SweetDb>) :
DataSource.Factory<Int, SweetUi>() {
var query = ""
override fun create(): DataSource<Int, SweetUi> {
val lazyList = box.query().contains(SweetDb_.name, query).build().findLazyCached()
return SweetSearchDataSource(lazyList).map { SweetUi(it) }
}
fun search(text: String) {
query = text
}
}
I am using ObjectBox here, but you can just return your room DAO query on create (I guess as it already is a DataSourceFactory, call its own create).
I did not test it, but this might work:
class SweetSearchDataSourceFactory(private val dao: SweetsDao) :
DataSource.Factory<Int, SweetUi>() {
var query = ""
override fun create(): DataSource<Int, SweetUi> {
return dao.searchSweets(query).map { SweetUi(it) }.create()
}
fun search(text: String) {
query = text
}
}
Of course one can just pass a Factory already with the query from dao.
ViewModel:
class SweetsSearchListViewModel
#Inject constructor(
private val dataSourceFactory: SweetSearchDataSourceFactory
) : BaseViewModel() {
companion object {
private const val INITIAL_LOAD_KEY = 0
private const val PAGE_SIZE = 10
private const val PREFETCH_DISTANCE = 20
}
lateinit var sweets: LiveData<PagedList<SweetUi>>
init {
val config = PagedList.Config.Builder()
.setPageSize(PAGE_SIZE)
.setPrefetchDistance(PREFETCH_DISTANCE)
.setEnablePlaceholders(true)
.build()
sweets = LivePagedListBuilder(dataSourceFactory, config).build()
}
fun searchSweets(text: String) {
dataSourceFactory.search(text)
sweets.value?.dataSource?.invalidate()
}
}
However the search query is received, just call searchSweets on ViewModel. It sets search query in the Factory, then invalidates the DataSource. In turn, create is called in the Factory and new instance of DataSource is created with new query and passed to existing LiveData under the hood..
You can go with other answers above, but here is another way to do that: You can make the Factory to produce a different DataSource based on your demand. This is how it's done:
In your DataSource.Factory class, provide setters for parameters needed to initialize the YourDataSource
private String searchText;
...
public void setSearchText(String newSearchText){
this.searchText = newSearchText;
}
#NonNull
#Override
public DataSource<Integer, SearchItem> create() {
YourDataSource dataSource = new YourDataSource(searchText); //create DataSource with parameter you provided
return dataSource;
}
When users input new search text, let your ViewModel class to set the new search text and then call invalidated on the DataSource. In your Activity/Fragment:
yourViewModel.setNewSearchText(searchText); //set new text when user searchs for a text
In your ViewModel, define that method to update the Factory class's searchText:
public void setNewSearchText(String newText){
//you have to call this statement to update the searchText in yourDataSourceFactory first
yourDataSourceFactory.setSearchText(newText);
searchPagedList.getValue().getDataSource().invalidate(); //notify yourDataSourceFactory to create new DataSource for searchPagedList
}
When DataSource is invalidated, DataSource.Factory will call its create() method to create newly DataSource with the newText value you have set. Results will be the same