i want to use SearchView for search some element in room db and i have a problem with this because i cant use getFilter in RecyclerViewAdapter because i have ViewModel maybe whoes know how to combine all of this element in one project.
I search one way use Transormations.switchMap. But I couldn’t connect them.
ProductViewModel
class ProductViewModel(application: Application) : AndroidViewModel(application) {
private val repository: ProductRepository
val allProducts: LiveData<List<ProductEntity>>
private val searchStringLiveData = MutableLiveData<String>()
init {
val productDao = ProductsDB.getDatabase(application, viewModelScope).productDao()
repository = ProductRepository(productDao)
allProducts = repository.allProducts
searchStringLiveData.value = ""
}
fun insert(productEntity: ProductEntity) = viewModelScope.launch {
repository.insert(productEntity)
}
val products = Transformations.switchMap(searchStringLiveData) { string ->
repository.getAllListByName(string)
}
fun searchNameChanged(name: String) {
searchStringLiveData.value = name
}
}
ProductDao
interface ProductDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertProduct(productEntity: ProductEntity)
#Query("SELECT * from products")
fun getListAllProducts(): LiveData<List<ProductEntity>>
#Query("DELETE FROM products")
suspend fun deleteAll()
#Query("SELECT * FROM products where product_name_ent LIKE :name or LOWER(product_name_ent) like LOWER(:name)")
fun getListAllByName(name: String):LiveData<List<String>>
}
ProductDao
#Query("SELECT * FROM products where product_name_ent LIKE :name or LOWER(product_name_ent) like LOWER(:name)")
fun getListAllByName(name: String):LiveData<List<ProductEntity>>
This Method in your dao should return LiveData<List<ProductEntity>> and not LiveData<List<String>>, because this query selects everything (*) from the entity and not a specific column.
it is similar to (#Query("SELECT * from products") fun getListAllProducts():LiveData<List<ProductEntity>>)
ProductViewModel
class ProductViewModel(application: Application) : AndroidViewModel(application) {
private val repository: ProductRepository
init {
val productDao = ProductsDB.getDatabase(
application,
viewModelScope
).productDao()
repository = ProductRepository(productDao)
}
private val searchStringLiveData = MutableLiveData<String>("") //we can add initial value directly in the constructor
val allProducts: LiveData<List<ProductEntity>>=Transformations.switchMap(searchStringLiveData)
{
string->
if (TextUtils.isEmpty(string)) {
repository.allProducts()
} else {
repository.allProductsByName(string)
}
}
fun insert(productEntity: ProductEntity) = viewModelScope.launch {
repository.insert(productEntity)
}
fun searchNameChanged(name: String) {
searchStringLiveData.value = name
}
}
Repository
...with the other method that you have, add the following:
fun allProducts():LiveData<List<ProductEntity>>=productDao.getListAllProducts()
fun allProductsByNames(name:String):LiveData<List<ProductEntity>>=productDao.getListAllByName(name)
In Your Activity Or Fragment Where you have the recyclerview adapter
inside onCreate() (if it is an activity)
viewModel.allProducts.observe(this,Observer{products->
//populate your recyclerview here
})
or
onActivityCreated(if it is a fragment)
viewModel.allProducts.observe(viewLifecycleOwner,Observer{products->
//populate your recyclerview here
})
Now set a listener to the searchView, everytime the user submit the query , call viewModel.searchNameChanged(// pass the new value)
Hope this helps
Regards,
Related
In my application I want use Koin , Room and LiveData.
I write below codes, but after add new item into room not auto update recyclerview list!
Should close app and open again for show updated list !
Dao codes :
#Query("SELECT * FROM $NOTE_TABLE")
fun getAllNote(): MutableList<NoteModel>
Repository codes :
class RoomRepository(private val dao: NoteDao) {
suspend fun saveNote(note: NoteModel) = dao.saveNote(note)
fun getAllNotes() = dao.getAllNote()
}
ViewModel codes:
class RoomViewModel(private val repository: RoomRepository) : ViewModel() {
val notesList = MutableLiveData<MutableList<NoteModel>>()
fun saveNote(note:NoteModel) = viewModelScope.launch {
repository.saveNote(note)
}
fun loadAllNotes() = viewModelScope.launch {
val list = repository.getAllNotes()
if (list.isNotEmpty()) {
notesList.postValue(list)
}
}
}
Activity codes :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityKoinRoomBinding.inflate(layoutInflater)
setContentView(binding.root)
//InitViews
binding.apply {
//Save
btnSave.setOnClickListener {
val title = titleEdt.text.toString()
note.id = 0
note.title = title
viewModel.saveNote(note)
}
//Load notes
viewModel.loadAllNotes()
viewModel.notesList.observe(this#KoinRoomActivity) {
noteAdapter.differ.submitList(it)
notesList.apply {
layoutManager = LinearLayoutManager(this#KoinRoomActivity)
adapter = noteAdapter
}
}
}
How can I fix this issue?
In order to update data immediately, you need to return LiveData<T> from the database itself. In your case, do something like that:
#Query("SELECT * FROM $NOTE_TABLE")
fun getAllNote(): LiveData<List<NoteModel>>
I also changed MutableList to List, because there is no need for mutability here.
You ViewModel can then be like this:
class RoomViewModel(private val repository: RoomRepository) : ViewModel() {
val notesList = repository.getAllNotes()
...
}
and you can also remove function loadAllNotes() from it.
I'm trying to retrieve a single object from my room database given its title but when I get to the fragment where I want this to happen, I have no way to access the object's fields. The method I'm trying to use is retrieveMovie but the observer shows that it = List! and for some reason I cannot get the movie given its title and then get its fields to bind them to my view. How can I do this?
I tried accessing the movie with it's index(0) but the second movie I click on causes an app crash that returns: 'java.lang.IndexOutOfBoundsException: Index: 0, Size: 0'
This is the fragment's part:
if(arguments != null){
val titleString = arguments?.getString("Title")
//observe viewmodel
if (titleString != null) {
mMoviesViewModel.retrieveMovie(titleString).observe(viewLifecycleOwner, Observer { movie ->
itemTextTitle.text = movie.title //this doesn't work
})
}
} else {
//display error message if arguments are null
Toast.makeText(context, "Error loading content", Toast.LENGTH_SHORT).show()
}
This is the db's viewmodel:
class MoviesViewModel(application: Application): AndroidViewModel(application) {
val readAllData: LiveData<List<Movies>>
private val repository: MoviesRepository
init {
val moviesDao = MoviesDatabase.getDatabase(application).moviesDao()
repository = MoviesRepository(moviesDao)
readAllData = repository.readAllData
}
fun addMovie(movie: Movies) {
viewModelScope.launch(Dispatchers.IO) {
repository.addMovie(movie)
}
}
fun movieExists(id: Int): Boolean{
viewModelScope.launch(Dispatchers.IO){
repository.movieExists(id)
}
return true
}
fun retrieveMovie(title: String): LiveData<List<Movies>> {
return repository.retrieveMovie(title)
}
}
This is the db's repository:
class MoviesRepository (private val moviesDao: MoviesDao) {
val readAllData: LiveData<List<Movies>> = moviesDao.readALlData()
fun addMovie(movie: Movies){
moviesDao.addMovie(movie)
}
fun movieExists(id:Int){
moviesDao.movieExists(id)
}
fun retrieveMovie(title:String): LiveData<List<Movies>> {
return moviesDao.retrieveMovie(title)
}
}
Dao:
package com.app.challengemovieapp.db
import androidx.lifecycle.LiveData
import androidx.room.*
import com.app.challengemovieapp.model.Movie
import kotlinx.coroutines.flow.Flow
#Dao
interface MoviesDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun addMovie(movie:Movies)
#Query("SELECT * FROM movie_table ORDER BY id ASC")
fun readALlData(): LiveData<List<Movies>>
#Query("SELECT EXISTS(SELECT * FROM movie_table WHERE id = :id)")
fun movieExists(id : Int) : Boolean
#Query("SELECT * FROM movie_table WHERE title = :title")
fun retrieveMovie(title:String): LiveData<List<Movies>>
}
To get a single entity you can use limit. Here is an example
#Query("SELECT * FROM movie_table WHERE title = :title LIMIT 1")
fun retrieveMovie(title:String): LiveData<Movies>
so the reason why you can't access the Movie object is cause you are mentioning this.
#Query("SELECT * FROM movie_table WHERE title = :title")
fun retrieveMovie(title:String): LiveData<List<Movies>>.
So LiveData is returning a List of Movie objects.
what you can do is
it.get(0).getTitle()
It should work.
Update: how to use ViewModelFactory and is it necessary to use this for passing our parameter? What's the benefit? is that going to break live data concept?
I want to send a parameter to my word Dao of my room database for query but in my case, I don't know how to pass that parameter. so let's begin with codes...
WordDao.kt
#Dao
interface WordDao {
#Insert
fun insert(word: Word)
#Update
fun update(word: Word)
#Delete
fun delete(word: Word)
#Query("delete from En_Fa")
fun deleteAllNotes()
#Query("SELECT * FROM En_Fa ORDER BY id ASC")
fun getAllNotes(): LiveData<List<Word>>
#Query("Select * From En_Fa WHERE date == :today ")
fun getTodayWords(today: String): LiveData<List<Word>>
}
WordRepository.kt
class WordRepository(private val wordDao: WordDao, today: String) {
val readAllData: LiveData<List<Word>> = wordDao.getAllNotes()
val readToday: LiveData<List<Word>> = wordDao.getTodayWords(today)
fun addWord(word: Word) {
wordDao.insert(word)
}
}
WordViewModel.kt
class WordViewModel(application: Application): AndroidViewModel(application) {
val readAllData2: LiveData<List<Word>>
private val repository: WordRepository
init {
val wordDao = WordDatabase.getInstance(application).wordDao()
repository = WordRepository(wordDao, today)
readAllData2 = repository.readToday
}
fun addWord(word: Word){
viewModelScope.launch(Dispatchers.IO){
repository.addWord(word)
}
}
}
and this is the line of my code that make an object of this wordview model class in my fragment
private val mWordViewModel: WordViewModel by viewModels()
so how to pass my (today) variable from my fragment to my WordViewModel class
I think you are looking for this:
#Dao
interface WordDao {
// ...
#Query("Select * From En_Fa WHERE date == :today ")
fun getTodayWords2(today: String): <List<Word>
}
Then in the repository:
class WordRepository{
// ... ...
var mutableWords: MutableLiveData<List<Word>> = MutableLiveData()
fun getWords(today: String): List<Word> { // WARNING! run this in background thread else it will crash
return wordDao.getTodayWords2(today)
}
fun getWordsAsync(today: String) {
Executors.newSingleThreadExecutor().execute {
val words = getWords(today)
liveWords.postValue(words) // <-- just doing this will trigger the observer and do next thing, such as, updating ui
}
}
}
Then in your viewModel:
class WordViewModel(application: Application): AndroidViewModel(application) {
// ... ...
val liveWords: LiveData<List<Word>> = repository.mutableWords
fun getWordsAsync(today: String) {
repository.getWordsAsync(today)
}
}
Then finally inside your activity / fragment:
fun viewModelDemo() {
mWordViewModel.liveWords.observe(this, Observer{
// todo: update the ui, eg
someTextView.text = it.toString() // <-- here you get the output
})
someButton.setOnClickListener{
// here you give the input
mWordViewModel.getWordsAsync(today) // get `today` from date picker or something
}
}
Edit
So you have a recyclerView which has an adapter. When the dataset changes, you call notifyDataSetChanged. Suppose, the adapter looks like this:
class MyAdapter: RecyclerView.Adapter<ViewHolder> {
private var words: List<Word> = ArrayList() // initially points to an empty list
override fun getCount() { return words.size }
// ... ... other methods
// public method:
fun submitList(words1: List<Word>) {
this.words = words1 // so now it points to the submitted list
this.notifyDataSetChanged() // this tells recyclerView to update itself
}
}
Then in your activity or fragment:
private lateinit var myAdapter: MyAdapter
override fun onCreate() { // or onViewCreated if using fragment
// ... ... some codes
this.myAdapter = MyAdapter()
binding.recyclerView.adapter = myAdapter
viewModelDemo()
}
fun viewModelDemo() {
mWordViewModel.liveWords.observe(this, Observer{
// todo: update the ui, eg
myAdapter.submitList(it) // <----- Here you call the submitList method
// <-- here you get the output
})
// --- ---
}
I hope this works.
If i get your question correctly then,
First, you need to make a function that will return the liveData object to observe, then you need to use ViewModelProviders that will provide you the object of your ViewModel in your fragment.
mWordViewModel: WordViewModel = ViewModelProvider(this/getActivity()).get(WordViewModel::class.java)
mWordViewModel.getLiveDataFunction().observe(this/lifeCycleOwner, {
process result/response here
}
Then simply use.
mWordViewModel.addWord(today)
Ín my app's room database I have a table called movie_table. When the user clicks the item, it is saved to another table called favorite_table. I tried almost two days but I can't solve it. I am new to MVVM.
For better understanding please see the code:
FavoriteMovieDao.kt
#Dao
interface FavoriteMovieDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertFavMovie(favorite: Favorite)
#Query("SELECT * FROM favorite_table WHERE id LIKE :id")
suspend fun getFavoriteMovieById(id:Int): Favorite
#Query("SELECT * FROM favorite_table")
suspend fun getAllFavoriteMovie(): List<Favorite>
#Delete
suspend fun deleteFavorite(favorite: Favorite)
}
Repository.kt
class Repository(context: Context) {
private val favoriteMovieDao: FavoriteMovieDao = MovieDatabase.invoke(context).getFavoriteMovieDao()
suspend fun insertFavMovie(favorite: Favorite){
favoriteMovieDao.insertFavMovie(favorite)
}
MovieDetailsViewModel.kt
class MovieDetailsViewModel(private val repository: Repository):ViewModel() {
private val favMovieResponse:MutableLiveData<List<Favorite>> = MutableLiveData()
val actorResponse:MutableLiveData<List<Movie>> = MutableLiveData()
private val insertFavMovie:MutableLiveData<Favorite> = MutableLiveData()
fun actorDetail(){
viewModelScope.launch {
val actor = repository.getAllMovieDB()
actorResponse.value = actor
}
}
fun insertFavMovie(favorite: Favorite){
viewModelScope.launch {
//i can't insert this
val insertFav = repository.insertFavMovie(favorite)
insertFavMovie.value = insertFav
}
}
}
}
MovieDetailsActivity.kt
class MovieDetailsActivity : AppCompatActivity(){
private lateinit var actorViewModel: MovieDeatilsViewModel
private val actorAdapter by lazy { ActorAdapter() }
private var isFav: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_movie_details)
val repository = Repository(this)
val viewModelFactory = MovieDetailsViewModelFactory(repository)
actorViewModel = ViewModelProvider(this,viewModelFactory).get(MovieDeatilsViewModel::class.java)
setUpPostRecyclerView()
actorViewModel.actorDetail()
actorViewModel.actorResponse.observe(this, Observer {actorList ->
actorAdapter.setData(actorList)
})
initBundle()
favBtn.setOnClickListener {
if (isFav){
isFav = false
favBtn.supportImageTintList = ColorStateList.valueOf(Color.parseColor("#E4C1C1"))
Log.d("msg","Not Favorite!")
}else{
isFav = true
favBtn.supportImageTintList = ColorStateList.valueOf(Color.parseColor("#FFC107"))
Log.d("msg","Favorite!")
}
}
}
private fun initBundle() {
val bundle:Bundle? = intent.extras
movieTitle.text = bundle!!.getString("title")
directorbc.text = bundle.getString("director")
genre.text = bundle.getString("genre")
releaseYear.text = bundle.getInt("year").toString()
language.text = bundle.getString("language")
country.text = bundle.getString("country")
rating.text = bundle.getString("rating")
plotText.text = bundle.getString("plot")
//moviePosterD.setImageDrawable(bundle!!.getString("image"))
Glide.with(this).load(bundle.getString("image")).into(moviePosterD);
val player = SimpleExoPlayer.Builder(this).build()
player.preparePlayer(movieView)
player.setSource(applicationContext, "http://html5videoformatconverter.com/data/images/happyfit2.mp4")
}
private fun setUpPostRecyclerView(){
actorRecyclerview.adapter = actorAdapter
actorRecyclerview.layoutManager = LinearLayoutManager(this,LinearLayoutManager.HORIZONTAL,false)
}
}
How can I insert it? Please help me. Thank you
In Movies table, to handle the favorite movies, you can make use of a variable in each movie item which you can for example set to true or false. This allows you to query using filters to show them on the screen. I don't think you need another table to store Favourites. I hope that helps! I can help you with the design if you share more details there.
#Entity
data class Movie( val title: String, val isFavourite: Boolean = false)
Update 1:
When user clicks on favButton, in MovieViewModel update the Movie entity like this:
fun setMovie(movie: Movie){
_movie.value = movie
}
fun updateMovie(movie: Movie) {
movie.isFavourite = !movie.isFavourite
viewModelScope.launch {
repository.updateMovie(movie)
setMovie(movie)
}
}
in MovieDao
#Update
suspend fun updateMovie(movie: Movie)
Update 2:
In MoviesDao
#Update
suspend fun updateMovie(movie: Movie)
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertAll(vararg movie: Movie)
#Query("select * from movies_table where isFavourite = 1")
fun getFavMovies(): LiveData<List<Movie>>
#Query("Select * from movies_table")
fun getAllMovies(): LiveData<List<Movie>>
In Repository
val favMovies: LiveData<List<Movie>> = moviesDao.getFavMovies()
Update 3:
favBtn.setOnClickListener {
_viewModel.updateMovie(movie)
}
_viewModel.movie.observe(viewLifecycleOwner, Observer {
//Update UI based on isFavourite value on Movie
})
I have Room Entity Class "Symptom" with name of Symptom and id of it.
#Entity(tableName = "symptoms")
data class Symptom(
#PrimaryKey #NonNull val id: Int,
val name: String) {
override fun toString(): String {
return "Symptom $id: $name"
}
}
I'm getting it in the following classses:
SymptomDao
#Dao
interface SymptomDao {
#Query("SELECT * FROM symptoms WHERE id=:id LIMIT 1")
fun getSymptom(id: Int): Symptom
#Query("SELECT * FROM symptoms")
fun getAllSymptoms(): LiveData<List<Symptom>>
}
SymptomRepository
class SymptomRepository(private val symptomDao: SymptomDao) {
fun getSymptom(id: Int) = symptomDao.getSymptom(id)
fun getAllSymptoms() = symptomDao.getAllSymptoms()
}
SymptomsViewModel
class SymptomsViewModel(symptomRepository: SymptomRepository): ViewModel() {
private val symptomsList = symptomRepository.getAllSymptoms()
private val symptomsItemsList: MutableLiveData<List<SymptomItem>> = MutableLiveData()
fun getAllSymptoms(): LiveData<List<Symptom>> {
return symptomsList
}
fun getAllSymptomsItems(): LiveData<List<SymptomItem>> {
return symptomsItemsList
}
}
I have RecyclerView with list of SymptomItem with Checkboxes to remember which Symptoms of a list users chooses:
data class SymptomItem(
val symptom: Symptom,
var checked: Boolean = false)
Question
My question is how can I get LiveData<List<SymptomItem>> by LiveData<List<Symptom>>? I have just started learning MVVM and I can't find a simply answer how to do that. I have already tried to fill this list in various ways, but It loses checked variable every time I rotate my phone. I'll be grateful for any hints.
You'll need to store which items are checked by storing their Ids in a List within the ViewModel. Then you'll have combine the list of your Symptom objects and the list of which items are checked, and generate the list of SymptomItem objects.
I'm going to use Kotlin Flow to achieve this.
#Dao
interface SymptomDao {
#Query("SELECT * FROM symptoms")
fun flowAllSymptoms(): Flow<List<Symptom>>
}
class SymptomRepository(private val symptomDao: SymptomDao) {
fun flowAllSymptoms() = symptomDao.flowAllSymptoms()
}
class SymptomsViewModel(
private val symptomRepository: SymptomRepository
) : ViewModel() {
private val symptomsListFlow = symptomRepository.flowAllSymptoms()
private val symptomsItemsList: MutableLiveData<List<SymptomItem>> = MutableLiveData()
private var checkedIdsFlow = MutableStateFlow(emptyList<Int>())
init {
viewModelScope.launch {
collectSymptomsItems()
}
}
private suspend fun collectSymptomsItems() =
flowSymptomsItems().collect { symptomsItems ->
symptomsItemsList.postValue(symptomsItems)
}
private fun flowSymptomsItems() =
symptomsListFlow
.combine(checkedIdsFlow) { list, checkedIds ->
list.map { SymptomItem(it, checkedIds.contains(it.id)) }
}
fun checkItem(id: Int) {
(checkedIdsFlow.value as MutableList<Int>).add(id)
checkedIdsFlow.value = checkedIdsFlow.value
}
fun uncheckItem(id: Int) {
(checkedIdsFlow.value as MutableList<Int>).remove(id)
checkedIdsFlow.value = checkedIdsFlow.value
}
fun getSymptomsItems(): LiveData<List<SymptomItem>> {
return symptomsItemsList
}
}
In your Fragment, observe getSymptomsItems() and update your adapter data.
The code is not tested, you may have to make small adjustments to make it compile.