I have an Activity where I have a LiveData and I wanna display it in a ListView.
My code snippet:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_records)
this.recordListView.emptyView = recordListEmptyView
viewModel = ViewModelProvider(this).get(RecordsViewModel::class.java)
recordDAO = AppDatabase.getDb(this).recordDao()
recordDAO.findAllSync().observe(this) {
//display livedata in listview after getting added
//adapter = ArrayAdapter(this,list_items,syncedRecords)
//listview.adapter = adapter
}
}
After I added a record I wanna display the records in the place where I have the comment.
I could make it work with a normal findAll() and set my adapter to the return of findAll() but I could not make it work with LiveData.
My DAO class:
#Dao
interface RecordDAO{
#Update(onConflict = REPLACE)
fun update(record:Record) : Int
#Insert(onConflict = IGNORE)
fun persist(record: Record): Long
#Delete
fun delete(record: Record): Int
#Delete
fun deleteAll(records: List<Record>)
#Query("SELECT * FROM record")
fun findAll(): List<Record>
#Query("SELECT * FROM record")
fun findAllSync(): LiveData<List<Record>>
#Query("SELECT * FROM record WHERE id = :id")
fun findById(id: Int): Record?
}
Here is how it should work. Since you did not inclue the viewmodel code, it is difficult to know the exact error. Here you observe the result of a function (recordDAO.findAllSync().observe), so I guess that the livedata is a variable only in the scope of findAllSync.
This is not how it works. Live data should be a member variable of the viewModel
class RecordsViewModel : ViewModel() {
val records = MutableLiveData<RecordDAO>()
// this function gives the records to the live data. No need for a return value
fun findAllSync() {
// Retrieve the records somehow, here this should be edited by you
records.value = someService.getRecords()
}
}
Then in the activity/fragment, you observe the live data and request a record update (You can also request the update directly from the init of the viewmodel depending on how/when you want to get the data)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_records)
this.recordListView.emptyView = recordListEmptyView
viewModel = ViewModelProvider(this).get(RecordsViewModel::class.java)
// THIS CHANGED
viewModel.findAllSync()
viewModel.records.observe(this) { records ->
val adapter = ArrayAdapter(this,list_items, records)
listview.adapter = adapter
}
}
Note: I used a MutableLiveData. The cleaner way is to have a private MutableLiveData and a public LiveData
private val _records = MutableLiveData<RecordDAO>()
val records: LiveData<RecordDAO> = _records
The vm uses _records.value to set the data but since the Activity can only read the non mutable one, it cannot edit the value, only the viewmodel can. You don't need to do it this way it but it's always nice to know
Related
I want to use livedata in an recyclerview. But I only want to observe Livedata with a certain ID. The data gets loaded, but it doesn't update.
So here is the function in m Dao:
#Query("SELECT * FROM zutaten_table NATURAL JOIN table_ref WHERE table_ref.rezeptid = :id")
fun getZutatenforRezept(id:Int): LiveData<List<ZutatenData>>
I use a Viewmodel and a repository like this:
class LiveDataZutatenRepository(private val rezeptDao: AllDao, rezeptID: Int){
val Dao = rezeptDao
val allZutaten = Dao.getZutatenforRezept(rezeptID)
}
class SpecialZutatViewmodel(application: Application): AndroidViewModel(application){
private val repository: JustGetSpecialTypesRepository
private lateinit var repositoryLiveData: LiveDataZutatenRepository
lateinit var ZutatenforRezept : LiveData<List<ZutatenData>>
val Dao : AllDao
init {
Dao = EssenRoomDatabase.getDatabase(application, viewModelScope).allDao()
repository = JustGetSpecialTypesRepository(Dao)
}
suspend fun getRezeptWithZutat(id: Int):RezeptWithZutat{
return repository.getRezeptWithZutatFromID(id)
}
suspend fun getMengen(rezid: Int): List<RefZutatRezept>{
return repository.getMengen(rezid)
}
fun setLiveData(rezeptid: Int){
repositoryLiveData = LiveDataZutatenRepository(Dao, rezeptid )
ZutatenforRezept = repositoryLiveData.allZutaten
}
}
an in my view I use an observer to get the livedata:
val zutatViewmodel = ViewModelProvider(this).get(SpecialZutatViewmodel::class.java)
lifecycleScope.launch {
zutatViewmodel.setLiveData(rezeptid)
}
zutatViewmodel.ZutatenforRezept.observe(this, Observer { zutaten ->
zutaten?.let { adapterzut.setZutaten(it) }
})
Viewholder function:
override fun onBindViewHolder(holder: ZutatenViewHolder, position: Int) {
val current = zutaten[position]
holder.rezepteItemView.text = current.zutname
if(current.bild>=0) {
holder.rezeptePicView.setImageResource(current.bild)
holder.rezeptePicView.drawable.isFilterBitmap = false
}
}
unfortenatly the list doesn't update when the database is changed, but is loaded correctly the first time. What am I doing wrong?
it seems you are using room. So, instead of passing the entire list to the adapter, you can pass a list of all the ids in the database. Then, in the onBindViewholder, you can call the rest of the elements by using the id of the element. the code sample below might give you a better idea -
override fun onBindViewHolder(holder: PassViewHolder, position: Int) {
viewModel.getById(getItem(position)).asLiveData().observe(lifecycleOwner) {
try {
holder.bind(it)
}catch (e:Exception){
Log.e(TAG,"PassData passed = null")
e.printStackTrace()
}
}
}
I had the same problem where the views weren't getting updated but the changes where still being recorded. this method fixed it all.
the below piece of code returns the elements linked to the id as a flow.
viewModel.getById(getItem(position))
And then you covert it to live data and add an observer.
if you want, you can have a look at the project where I implemented this
I'm trying to get data from server and cache into database and return new fetched list to user. I'm getting response form server and saving it to the local database but when im trying to observer it from composable function it showing list is empty.
When i try to debug and collect flow data in myViewModel class it showing but it not showing is composable function.
dao
#Dao
interface CategoryDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(categories: List<Category>)
#Query("SELECT * FROM categories ORDER BY name")
fun read(): Flow<List<Category>>
#Query("DELETE FROM categories")
suspend fun clearAll()
}
repository class:
suspend fun getCategories(): Flow<List<Category>> {
val categories = RetrofitModule.getCategories().categories
dao.insert(categories)
return dao.read()
}
myViewModel
fun categoriesList(): Flow<List<Category>> {
var list: Flow<List<Category>> = MutableStateFlow(emptyList())
viewModelScope.launch {
list = repository.getCategories().flowOn(Dispatchers.IO)
}
return list
}
Observing from:
#Composable
fun StoreScreen(navController: NavController, viewModel: CategoryViewModel) {
val list = viewModel.categoriesList().collectAsState(emptyList())
Log.d("appDebug", list.value.toString()) // Showing always emptyList []
}
current response :
2021-05-15 16:08:56.017 5125-5125/com.demo.app D/appDebug: []
You are never updating the value of MutableStateFlow which has been collected as state in the Composable function.
Also you are assigning a Flow type object to a MutableStateFlow variable.
We can just update the value of the collected flow in the compose using:-
mutableFlow.value = newValue
We need to change the type of list to MutableStateFlow<List<Category>> instead of Flow<List<Category>>
Try this:-
var list: MutableStateFlow<List<Category>> = MutableStateFlow(emptyList()) // changed the type of list to mutableStateFlow
viewModelScope.launch {
repository.getCategories().flowOn(Dispatchers.IO).collect { it ->
list.value = it
}
}
Room , Mvvm Created , Kotlin - Add , Delete are working
but when i run update nothing changes
i had debugged the app , dao , repo and viewModel class are showing the changes but no changes reflect on my recyclerView or when i destroy and again open my app it does not changes
My Model/Entity Classs :
data class IceBreakerModel(val question:String,
val date:String,
val option1:String,
val option2:String,
val option3:String){
#PrimaryKey(autoGenerate = true)
var id: Int = 0
}
Dao : -
interface IcebreakerDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertQuestion(question:IceBreakerModel)
#Query("DELETE FROM ice_breaker_questions WHERE id=:ID")
fun deleteQuestion(ID:Int)
#Query("SELECT * FROM ice_breaker_questions")
fun getAllIcebreakerQuestions():LiveData<List<IceBreakerModel>>
#Update(onConflict = OnConflictStrategy.REPLACE)
fun updateIcebreakerQuestion(question:IceBreakerModel)
// #Query("UPDATE ice_breaker_questions SET question =:newQuestion ,date=:newDate,option1=:newOption1,option2=:newOption2,option3=:newOption3 WHERE id=:ID")
// fun updateQuestion(newQuestion:String,newDate:String,newOption1:String,newOption2:String,newOption3:String,ID:Int)
}
Repo :-
private class UpdateQuestion(dao:IcebreakerDao):AsyncTask<IceBreakerModel,Unit,Unit>(){
val questionDao = dao
override fun doInBackground(vararg params: IceBreakerModel?) {
val model = params[0]!!
questionDao.updateIcebreakerQuestion(model)
// questionDao.updateQuestion(model.question,model.date,model.option1,model.option2,model.option3,model.id)
}
}
and Finally Main Activity where i am updating
iceBreakerViewModel.updateQuestion(IceBreakerModel(question,Date().toString(),option1,option2,option3))
adapter.notifyDataSetChanged()
The method you commented:
#Query("UPDATE ice_breaker_questions SET question =:newQuestion ,date=:newDate,option1=:newOption1,option2=:newOption2,option3=:newOption3 WHERE id=:ID")
fun updateQuestion(newQuestion:String,newDate:String,newOption1:String,newOption2:String,newOption3:String,ID:Int)
Is the way to go. At least it's how I'm used to update my columns.
Also, I think you need to adapter.notifyDataSetChanged() inside the observer of the ViewModel in your activity and in your ViewModel, assuming you are using LiveData.
Let me know if you need anything else, as I might missed the point somewhere
I'm fairly new to Kotlin/Android development, and am trying to figure out the best way to update data in a Room database. After following some tutorials, I currently have an architecture that looks like this:
Room Database with tables and DAOs -> Repository -> ViewModel -> Activity
So the activity has a ViewModel that calls the Repository, which in turn updates the database.
The ViewModel for the activity has a LiveData list of the object (there's also a factory to create the ViewModel, but that's just to allow the bookId to be passed in):
class ViewBookViewModel(application: Application, bookId: Int) : AndroidViewModel(application) {
private val repository: AppRepository
internal val flashCards: LiveData<List<FlashCard>>
init {
val flashCardDao = AppDatabase.getDatabase(application, viewModelScope).flashCardDao()
val bookDao = AppDatabase.getDatabase(application, viewModelScope).bookDao()
repository = AppRepository(flashCardDao, bookDao)
flashCards = flashCardDao.getByBookId(bookId)
}
fun insert(flashCard: FlashCard) = viewModelScope.launch(Dispatchers.IO){
repository.insert(flashCard)
}
fun setIsFavorited(cardUid: Long, favorited: Boolean) = viewModelScope.launch(Dispatchers.IO) {
repository.setIsFavorited(cardUid, favorited)
}
}
//The actual query that gets called eventually
#Query("UPDATE flashcard SET is_favorited = :favorited WHERE uid LIKE :cardUid")
fun setFavorited(cardUid: Long, favorited: Boolean)
And the Activity sets up the viewModel and also creates an observer on the
class ViewBookActivity : AppCompatActivity() {
private lateinit var flashCards: LiveData<List<FlashCard>>
private var layoutManager: RecyclerView.LayoutManager? = null
private lateinit var viewModel: ViewBookViewModel
private var bookId: Int = 0
private lateinit var bookTitle: String
override fun onCreate(savedInstanceState: Bundle?) {
...
bookId = intent.extras["bookId"] as Int
bookTitle = intent.extras["bookTitle"].toString()
layoutManager = LinearLayoutManager(this)
flashCardRecyclerView.layoutManager = layoutManager
viewModel = ViewModelProviders.of(this, ViewBookViewModelFactory(application, bookId as Int)).get(ViewBookViewModel::class.java)
flashCards = viewModel.flashCards
flashCards.observe(this, Observer { flashCards:List<FlashCard> ->
flashCardRecyclerView.adapter = FlashCardRecyclerAdapter(flashCards, viewModel)
})
}
}
Finally, I have a custom RecyclerAdapter, which is where I'm running into trouble. I have it set up so that when the user taps the "favorite" button on the Flash Card, it updates the database. However, this also causes the Activity to "refresh", scrolling to the top. I assume this is because it is observing LiveData, and that data is being changed.
custom RecylcerAdapter with ViewHolder code (stripped not-relevant code):
class FlashCardRecyclerAdapter(val flashCards: List<FlashCard>, val viewModel: ViewBookViewModel) : RecyclerView.Adapter<FlashCardRecyclerAdapter.FlashCardViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FlashCardViewHolder {
val v: View = LayoutInflater
.from(parent.context)
.inflate(R.layout.flash_card, parent, false)
return FlashCardViewHolder(v)
}
override fun onBindViewHolder(holder: FlashCardViewHolder, position: Int) {
val card = flashCards[position]
holder.isFavorited = card.isFavorited
holder.uid = card.uid
holder.modifyFavoriteButtonImage(holder.isFavorited)
}
override fun getItemCount(): Int {
return flashCards.size
}
inner class FlashCardViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
var mFavorited: Button
var frontShowing: Boolean
var isFavorited: Boolean = false
var uid: Long = 0
init {
mFavorited = itemView.findViewById(R.id.favoriteButton)
mFavorited.setOnClickListener { _ ->
isFavorited = !isFavorited
viewModel.setIsFavorited(uid, isFavorited) // Here is the database call
modifyFavoriteButtonImage(isFavorited)
}
}
fun modifyFavoriteButtonImage(isFavorited: Boolean){
// Code removed, just updates the image to be a filled/empty star based on favorited status
}
}
I feel like I am probably doing something wrong, as passing the ViewModel into the recylcer adapter in order to update the DB does not seem correct. Is there a pattern I should be using for this sort of situation, or should I change the code to not be using LiveData? Any help would be greatly appreciated.
flashCards.observe(this, Observer { flashCards:List<FlashCard> ->
flashCardRecyclerView.adapter = FlashCardRecyclerAdapter(flashCards, viewModel)
}
you should not be making a new adapter instance here, instead, assign the values you get from the live data to the existing adapter (adapter.flashCards = flashCards, LiveData value) and call adapter.notifyDataSetChanged, this will tell your adapter that new data came in and it needs to update.
you should not be passing your ViewModel to your adapter (or anything).
you can do something like this instead:
class FlashCardRecyclerAdapter(val flashCards: List<FlashCard>, val callback:(FlashCard) -> Unit)
then, where you declare your adapter, you do this :
val adapter = FlashCardRecyclerAdapter(...) {
viewModel.update(it)
}
and then :
override fun onBindViewHolder(holder: FlashCardViewHolder, position: Int) {
val card = flashCards[position]
holder.isFavorited = card.isFavorited
holder.uid = card.uid
holder.itemView.setOnClickListener {
callback.invoke(card)
}
holder.modifyFavoriteButtonImage(holder.isFavorited)
}
In your repository method, I am not sure what you are doing there but rather than passing in a livedata instance, you should pass in the underlying data of the livedata instance. That way, the observer in the main activity doesn't get triggered everytime you call setIsFavorited(). If you do want to trigger the observer, then you can just call postValue() on the livedata instance. As for the adapter question, I do not know the best practices but I usually create a listener interface so I don't have to pass around my viewmodels everywhere. All of my viewmodels are contained within my fragments and never goes anywhere else. Let me know if this answers your questions.
Also, if you are using viewmodels with recyclerview, consider using list adapters. They are made to work seamlessly with viewmodels. https://developer.android.com/reference/android/support/v7/recyclerview/extensions/ListAdapter
It makes it much simpler to use viewmodels with recyclerview.
I'm having trouble figuring out how I'm supposed to update data in a Room Database after a change in RecyclerView item order. How am I supposed to update LiveData items in Room based on user action?
Using ItemTouchHelper.Callback I've set up an onMove callback that can make changes to the order of items presented to the user (on drag and drop), but when I make a call to update the order of items in the Room Database, using a ViewModel object, the user can then only move items one at a time. So if you drag an item, it will only move one space.
This is the onMove function I have defined in the ListAdapter, which implements ItemTouchHelperAdapter for the callback.
override fun onMove(
recyclerView: RecyclerView,
fromViewHolder: RecyclerView.ViewHolder,
toViewHolder: RecyclerView.ViewHolder
): Boolean {
d(this.TAG, "swap viewHolders: " + fromViewHolder.adapterPosition + " to " + toViewHolder.adapterPosition)
val workoutRoutine1 = workoutRoutines[fromViewHolder.adapterPosition]
val workoutRoutine2 = workoutRoutines[toViewHolder.adapterPosition]
workoutRoutine1.orderNumber = toViewHolder.adapterPosition.toLong()
workoutRoutine2.orderNumber = fromViewHolder.adapterPosition.toLong()
//this.workoutRoutinesViewModel.update(workoutRoutine1)
//this.workoutRoutinesViewModel.update(workoutRoutine2)
notifyItemMoved(fromViewHolder.adapterPosition, toViewHolder.adapterPosition)
return true
}
This is my DAO object
#Dao
interface WorkoutRoutineDAO {
#Insert
suspend fun insert(workoutRoutine: WorkoutRoutine)
#Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun update(workoutRoutine: WorkoutRoutine)
#Delete
suspend fun delete(workoutRoutine: WorkoutRoutine)
#Query("DELETE FROM workout_routine_table")
fun deleteAll()
#Query("SELECT * FROM workout_routine_table ORDER BY order_number ASC")
fun getAllWorkoutRoutines(): LiveData<List<WorkoutRoutine>>
#Query("SELECT COALESCE(MAX(order_number), -1) FROM workout_routine_table")
fun getLargestOrderNumber(): Long
}
This is my RoomDatabase object
#Database(entities = [WorkoutRoutine::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun workoutRoutineDAO(): WorkoutRoutineDAO
companion object {
#Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(
context: Context,
scope: CoroutineScope
): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance =
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database")
.addCallback(WorkoutRoutineDatabaseCallback(scope))
.build()
INSTANCE = instance
instance
}
}
}
private class WorkoutRoutineDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
}
}
This is the ViewModel object I implemented.
class WorkoutRoutinesViewModel(application: Application) : AndroidViewModel(application) {
private val workoutRoutinesRepository: WorkoutRoutineRepository
val allWorkoutRoutines: LiveData<List<WorkoutRoutine>>
init {
// Get the DAO
val workoutRoutineDAO = AppDatabase.getDatabase(application, viewModelScope).workoutRoutineDAO()
// Build a new data repository for workout routines
workoutRoutinesRepository = WorkoutRoutineRepository(workoutRoutineDAO)
// Get a live view of the workout routines database
allWorkoutRoutines = workoutRoutinesRepository.allRoutines
}
fun insert(workoutRoutine: WorkoutRoutine) = viewModelScope.launch(Dispatchers.IO) {
workoutRoutinesRepository.insert(workoutRoutine)
}
fun update(workoutRoutine: WorkoutRoutine) = viewModelScope.launch(Dispatchers.IO) {
workoutRoutinesRepository.update(workoutRoutine)
}
fun delete(workoutRoutine: WorkoutRoutine) = viewModelScope.launch(Dispatchers.IO) {
workoutRoutinesRepository.delete(workoutRoutine)
}
}
I expect the user to be able to move the item n spaces, then drop, and have the update to Room database execute when the user drops the item, but if I put the Room update in the onMove method, the user can only move the item once.
I'm trying to understand the right way to update the Room data when the order of objects change in the recycler view. I'm trying to get the order of those objects to persist even when the user exits the app or changes activities or whatever. How am I supposed to echo those changes back to the Room database, using LiveData?
You can follow this guide on Udacity. It is free, made by Google and uses Kotlin.