I have two tables in my Room DB - Events and Notes. For each event I have displayed in the RecycleView - I have a link to launch a note for that event. On first click - Note is created. On the second time the note is clicked, I would like to retrieve the previous note and then edit. Also, I am using the same activity to already edit/create new notes by passing on appropriate values, which works but uses parcelized note.
For editing an existing event Note - I am sending across the event ID (which is also stored in the Note table - not as a Foreign key) using the putExtra method. DB structure below (assocId refers to eventId)
ViewModel
fun setNotesByAssocEventId(assocEventId: String): Note {
return dao.getByAssocEventId(assocEventId)
}
DAO
#Query("SELECT * FROM notes WHERE assocEventId = :assocEventId")
fun getByAssocEventId(assocEventId: String): Note
NoteEntity
#Entity(tableName = "notes")
#Parcelize
data class Note(
//PrimaryKey annotation to declare primary key with auto increment value
//ColumnInfo annotation to specify the column's name
#PrimaryKey(autoGenerate = true) #ColumnInfo(name = "id") var id: Int = 0,
#ColumnInfo(name = "assocEventId") var assocEventId: String = "",
#ColumnInfo(name = "title") var title: String = "",
#ColumnInfo(name = "label") var label: String = "",
#ColumnInfo(name = "date") var date: String = "",
#ColumnInfo(name = "time") var time: String = "",
#ColumnInfo(name = "updatedDate") var updatedDate: String = "",
#ColumnInfo(name = "updatedTime") var updatedTime: String = "",
#ColumnInfo(name = "body") var body: String = ""
) : Parcelable
I am using the below code to edit/create new notes. While I am able to create/Edit notes. I am unable to retrieve a node for a particular event using the eventId. One of the errors I am getting is Note object has not been initialized when I am assigning the note object returned from the ViewModel. What could be the issue?
assocID is the event ID obtained using putExtra and the corresponding event note is to be retrieved...
private lateinit var binding: ActivityEditNoteBinding
private lateinit var notesViewModel: NotesViewModel
private lateinit var note: Note
private var assocId: String? = ""
private var isUpdate = false
private val dateChange = DateChange()
var refUsers: DatabaseReference? = null
var firebaseUser: FirebaseUser? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityEditNoteBinding.inflate(layoutInflater)
setContentView(binding.root)
assocId = intent.getStringExtra("eventId").toString()
initView()
initListener()
}
private fun initView() {
firebaseUser = FirebaseAuth.getInstance().currentUser
initViewModel()
if (assocId != null) {
findViewById<TextView>(R.id.editNote).text = "Edit Event Note"
Toast.makeText(this, "EvetnId received", Toast.LENGTH_SHORT).show()
isUpdate = true
binding.editNoteDelete.visibility = View.VISIBLE
notesViewModel.getNotes()
note = notesViewModel.setNotesByAssocEventId("%${assocId}%")
binding.editTextTitle.setText(note.title)
binding.editTextBody.setText(note.body)
binding.editTextTitle.setSelection(note.title.length)
//set spinner position
val compareValue = note.label
val adapter = ArrayAdapter.createFromResource(
this, R.array.NoteSpinnerVals,
android.R.layout.simple_spinner_item
)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.spLabel.adapter = adapter
val spinnerPosition = adapter.getPosition(compareValue)
binding.spLabel.setSelection(spinnerPosition)
}
}
private fun initViewModel() {
notesViewModel = ViewModelProvider(this).get(NotesViewModel::class.java)
}
private fun initListener() {
// binding.editNoteBack.setOnClickListener(this)
binding.editNoteSave.setOnClickListener(this)
binding.editNoteDelete.setOnClickListener(this)
}
private fun deleteNote(note: Note) {
notesViewModel.deleteNote(note)
Toast.makeText(this#EditNote, "Note removed", Toast.LENGTH_SHORT).show()
}
private fun showDialog() {
AwesomeDialog.build(this)
.position(AwesomeDialog.POSITIONS.CENTER)
.title("Delete the note?")
.icon(R.drawable.ic_delete_black)
.background(R.drawable.background_dialog)
.onPositive(
"Yes, delete",
buttonBackgroundColor = R.drawable.button_bg,
textColor = ContextCompat.getColor(this, R.color.white)
) {
deleteNote(note)
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
}
.onNegative(
"Cancel",
buttonBackgroundColor = R.drawable.button_bg,
textColor = ContextCompat.getColor(this, R.color.white)
) {
}
}
Code of the ViewModel
class NotesViewModel(application: Application) : AndroidViewModel(application) {
private val context = getApplication<Application>().applicationContext
private val listNotes = MutableLiveData<ArrayList<Note>>()
private var dao: NoteDao
init {
val database = AppDatabase.getDatabase(context)
dao = database.getNoteDao()
}
fun setNotes() {
val listItems = arrayListOf<Note>()
listItems.addAll(dao.getAll())
listNotes.postValue(listItems)
}
fun setNotesByType(label: String) {
val listItems = arrayListOf<Note>()
listItems.addAll(dao.getByLabel(label))
listNotes.postValue(listItems)
}
fun setNotesByTitle(title: String) {
val listItems = arrayListOf<Note>()
listItems.addAll(dao.getByTitle(title))
listNotes.postValue(listItems)
}
fun setNotesByAssocEventId(assocEventId: String): Note {
return dao.getByAssocEventId(assocEventId)
}
fun insertNote(note: Note) {
dao.insert(note)
}
fun updateNote(note: Note) {
dao.update(note)
}
fun deleteNote(note: Note) {
dao.delete(note)
}
fun getNotes(): LiveData<ArrayList<Note>> {
return listNotes
}
}
The method in the DAO need to be changed a little
#Query("SELECT * FROM notes WHERE assocEventId = :assocEventId")
fun getByAssocEventId(assocEventId: String): Note
should be
#Query("SELECT * FROM notes WHERE assocEventId LIKE :assocEventId")
fun getByAssocEventId(assocEventId: String): LiveData<List<Note>>
In order to support wild character search, "%${assocId}%", LIKE keyword.
To get one Note only
#Query("SELECT * FROM notes WHERE assocEventId LIKE :assocEventId LIMIT 1")
fun getByAssocEventId(assocEventId: String): LiveData<Note>
in view model
fun setNotesByAssocEventId(assocEventId: String): LiveData<Note>{
return dao.getByAssocEventId(assocEventId)
}
in the activity
notesViewModel.setNotesByAssocEventId("%${assocId}%").observe(this, {
if(it!=null){
//if you using for single note only
}
//if(it.isNotEmpty()){
//if you using for list
//}
})
Related
i'm trying to delete a document in my firestore database
i'm using a listview to list some things of my data base, the items of this listview have a delete button, what i want is: when the user presses the delete button, this thing gets deleted from the list and from the firestore, there what i'm trying:
this is my entire activity to do this:
class RigBuilderActivity : AppCompatActivity() {
lateinit var botaoAddPc: FloatingActionButton
lateinit var pcName:EditText
lateinit var infoButton:FloatingActionButton
lateinit var pcListView: ListView
companion object{
const val TAG = "RigBuilderActivity"
}
var pc = arrayListOf<PC>()
val pcAdapter = PcAdapter()
val db = Firebase.firestore
val userId = FirebaseAuth.getInstance().currentUser!!.uid
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_rig_builder)
botaoAddPc = findViewById(R.id.rig_builder_button_add_pc)
pcName = findViewById(R.id.rig_builder_text_input)
infoButton = findViewById(R.id.rig_builder_button_info)
pcListView = findViewById(R.id.rig_builder_pc_list)
botaoAddPc.setOnClickListener{
var pc_text :String = pcName.text.toString()
val computer = PC(UUID.randomUUID().toString(),pc_text)
db.collection("users").document(userId).collection("PC").add(computer.toHashmapPC()).addOnSuccessListener { task->
Log.d(TAG, "DocumentSnapshot added with ID: ${task.id}")
Toast.makeText(this,"Pc added",Toast.LENGTH_SHORT).show()
}.addOnFailureListener{ e->
Log.w(TAG,"Error Adding PC",e)
Toast.makeText(this,"Failed",Toast.LENGTH_SHORT).show()
}
}
pcListView.adapter = pcAdapter
db.collection("users").document(userId).collection("PC").addSnapshotListener{ value, e->
if (e!=null){
Log.w(TAG,"Listen failed.",e)
return#addSnapshotListener
}
pc.clear()
for (doc in value!!){
val pc = PC.fromQueryDoc(doc)
this.pc.add(pc)
}
pcAdapter.notifyDataSetChanged()
}
}
fun getPcName():String{
return pcName.toString()
}
inner class PcAdapter : BaseAdapter() {
override fun getCount(): Int {
return pc.size
}
override fun getItem(p0: Int): Any {
return pc[p0]
}
override fun getItemId(p0: Int): Long {
return 0
}
override fun getView(p0: Int, p1: View?, p2: ViewGroup?): View {
val rootView = layoutInflater.inflate(R.layout.pc_item_layout, p2,false )
val textViewPcName = rootView.findViewById<TextView>(R.id.pc_name)
val editButton = rootView.findViewById<ImageButton>(R.id.pc_edit_button)
val deleteButton = rootView.findViewById<FloatingActionButton>(R.id.pc_delete_button)
textViewPcName.text = pc[p0].counter.toString()
deleteButton.setOnClickListener{
val ref : DocumentReference = db.collection("users").document(userId).collection("PC").document(pc[p0].getPcId())
ref.delete().addOnSuccessListener {
Log.d(TAG,"Pc deleted with success")
Toast.makeText(this#RigBuilderActivity,"Deleted with success",Toast.LENGTH_SHORT).show()
}
pcAdapter.notifyDataSetChanged()
}
editButton.setOnClickListener{
}
textViewPcName.text = pc[p0].name
return rootView
}
}
}
in case of needing, this is my PC class, i'm just using one of the constructors to test the list and make the delete works:
class PC {
var id:String
var name : String? = null
var counter : Long? = null
lateinit var cpu :CPU
lateinit var gpu: GPU
lateinit var motherBoard: MotherBoard
/**
* Construtor de teste
*/
constructor(id:String, name:String?){
this.id = id
this.name = name
}
/**
* Construtor ainda a implementar
*/
constructor(id:String, name:String?, cpu: CPU,gpu: GPU,motherBoard: MotherBoard){
this.id = id
this.name = name
this.cpu = cpu
this.gpu = gpu
this.motherBoard = motherBoard
}
fun toHashmapPC() : HashMap <String, Any?>{
return hashMapOf(
"id" to id,
"name" to name
)
}
fun getPcId():String{
return id
}
companion object{
fun fromQueryDoc(documentSnapshot: DocumentSnapshot):PC{
return PC(
documentSnapshot["id"] as String,
documentSnapshot["name"] as String
)
}
}
}
I am trying to make a function which will check whether this id is already present or not in the database, But it is not working as required.It take the insert value in database.
private fun CheckDuplicateId(id:String,donorDao:DonorDao):Boolean{
var result=true
lifecycleScope.launch {
donorDao.fetchAllDonor().collect(){
var Donorlist=ArrayList(it)
for(item in Donorlist){
if (item.id==id){
result=false
}
}
}
}
return result
}
I am using this function in while inserting the entry
private fun addRecord(donorDao:DonorDao){
val Id:String=binding?.etDonorId?.text.toString()
val bloodGr=binding?.etDonorBloodgroup?.text.toString()
if(Id.isNotEmpty() && bloodGr.isNotEmpty() && CheckDuplicateId(Id,donorDao)){
lifecycleScope.launch {
donorDao.insert(DonorEntity(id = binding?.etDonorId?.text.toString(), bloodGroup = bloodGr))
Toast.makeText(applicationContext, "Record saved", Toast.LENGTH_SHORT).show()
}
}else{
Toast.makeText(this,"duplicate value",Toast.LENGTH_SHORT).show()
}
}
here donor entity
#Entity(tableName = "donor-table")
data class DonorEntity(
#PrimaryKey(autoGenerate = true)
var sr_no:Int=0,
var id:String="",
var bloodGroup:String="")
here donor dao
#Dao
interface DonorDao {
#Insert
suspend fun insert(donorEntity:DonorEntity)
#Update
suspend fun update(donorEntity:DonorEntity)
#Delete
suspend fun delete(donorEntity:DonorEntity)
#Query("SELECT*FROM `donor-table`")
fun fetchAllDonor(): Flow<List<DonorEntity>>
#Query("SELECT*FROM `donor-table` Where id=:id")
fun fetchDonorById(id:Int): Flow<DonorEntity>
}
here the database
#Database(entities = [DonorEntity::class], version = 2)
abstract class DonorDatabase:RoomDatabase() {
abstract fun donorDao():DonorDao
companion object{
#Volatile
private var INSTANCE:DonorDatabase?=null
fun getInstance(context: Context):DonorDatabase{
synchronized(this){
var instance=INSTANCE
if (instance==null){
instance=Room.databaseBuilder(context.applicationContext,
DonorDatabase::class.java,"donor_database")
.fallbackToDestructiveMigration().build()
}
INSTANCE=instance
return instance
}
}
}
}
Your method returning true immediately:
private fun CheckDuplicateId(id:String,donorDao:DonorDao):Boolean{
var result=true
lifecycleScope.launch {
donorDao.fetchAllDonor().collect(){
var Donorlist=ArrayList(it)
for(item in Donorlist){
if (item.id==id){
result=false
}
}
}
}
return result // return true immediately
}
So, do it inside
private fun addRecord(donorDao:DonorDao){
val Id:String=binding?.etDonorId?.text.toString()
val bloodGr=binding?.etDonorBloodgroup?.text.toString()
if(Id.isNotEmpty() && bloodGr.isNotEmpty()){
lifecycleScope.launch {
// check here CheckDuplicateId(Id,donorDao) synchronously
donorDao.insert(DonorEntity(id = binding?.etDonorId?.text.toString(), bloodGroup = bloodGr))
Toast.makeText(applicationContext, "Record saved", Toast.LENGTH_SHORT).show()
}
}else{
Toast.makeText(this,"duplicate value",Toast.LENGTH_SHORT).show()
}
}
One way to do that is at DB level is using index. This will only insert if table as no with same id value. You can avoid select * query which would affect performance if you have many data in you case.
#Entity(tableName = "donor-table",indices = [Index(value = ["id"], unique = true)])
data class DonorEntity(
#PrimaryKey(autoGenerate = true)
var sr_no:Int=0,
#ColumnInfo(name = "id")
var id:String="",
var bloodGroup:String="")
To make certain fields or groups of fields in a database unique, you can enforce this uniqueness property by setting the unique property of an #Index annotation to true.
#Entity(indices = [Index(value = ["first_name", "last_name"],
unique = true)])
data class User(
#PrimaryKey val id: Int,
#ColumnInfo(name = "first_name") val firstName: String?,
#ColumnInfo(name = "last_name") val lastName: String?,
#Ignore var picture: Bitmap?
)
Here is the Official Docs
I have two related tables item and purchase. The purchase class contains an ignored column itemName which I want to fill with itemName from item
Although I have a method but I am concerned if there is a better way
to achieve it because my way has to do with reading all items from the database then comparing purchase.itemOwnerID to item.itemID
Here is my populating code along with others
LaunchedEffect(key1 = true) {
sharedViewModel.requestAllPurchases()
sharedViewModel.requestAllItems() //requesting all items
}
val allPurchases by sharedViewModel.allPurchases.collectAsState()
val allItems by sharedViewModel.allItems.collectAsState() //getting all items requested
if (allPurchases is RequestState.Success && allItems is RequestState.Success) {
(allPurchases as RequestState.Success<List<Purchase>>).data.forEach { purchase ->
(allItems as RequestState.Success<List<Item>>).data.first { it.itemID == purchase.itemOwnerID }//comparism
.apply {
purchase.itemName = itemName
purchase.costPrice = salePrice
}
}
}
Model
#Entity(tableName = "items", indices = [Index(value = ["itemName"], unique = true)])
data class Item(
#PrimaryKey(autoGenerate = true)
var itemID: Int,
var categoryOwnerID: Int = 0,
var itemName: String,
var costPrice: Int,
var salePrice: Int,
) {
#Ignore
var desiredQuantity: Int = 0
}
#Entity(tableName = "purchases")
data class Purchase(
#PrimaryKey(autoGenerate = true)
var purchaseID: Int,
var itemOwnerID: Int,
var quantity: Int,
var soldPrice: Int,
) {
#Ignore
var itemName: String = ""
}
sealed class RequestState<out T> {
object Idle : RequestState<Nothing>()
object Loading : RequestState<Nothing>()
data class Success<T>(val data: T) : RequestState<T>()
data class Error(val error: Throwable) : RequestState<Nothing>()
}
#Dao
#Query("select * from items order by itemName asc")
fun getAllItems(): Flow<List<Item>>
#Query("select * from purchases order by purchaseID asc")
fun getAllPurchases(): Flow<List<Purchase>>
Repository
val getAllItems: Flow<List<Item>> = itemDao.getAllItems()
val getAllPurchases: Flow<List<Purchase>> = itemDao.getAllPurchases()
SharedViewModel
private var _allItems = MutableStateFlow<RequestState<List<Item>>>(RequestState.Idle)
val allItems: StateFlow<RequestState<List<Item>>> = _allItems
fun requestAllItems() {
_allItems.value = RequestState.Loading
try {
viewModelScope.launch {
repository.getAllItems.collect {
_allItems.value = RequestState.Success(it)
}
}
} catch (e: Exception) {
_allItems.value = RequestState.Error(e)
}
}
private var _allPurchases =
MutableStateFlow<RequestState<List<Purchase>>>(RequestState.Idle)
val allPurchases: StateFlow<RequestState<List<Purchase>>> = _allPurchases
fun requestAllPurchases() {
_allPurchases.value = RequestState.Loading
try {
viewModelScope.launch {
repository.getAllPurchases.collect {
_allPurchases.value = RequestState.Success(it)
}
}
} catch (e: Exception) {
_allPurchases.value = RequestState.Error(e)
}
}
First, create another data class based on the columns needed, then use a #Query to get the needed columns from the database or a join query for multiple tables like the one below
Query("SELECT P.purchaseID, P.itemOwnerID, P.quantity, P.soldPrice, I.itemName FROM purchases as P INNER JOIN items AS I ON I.itemID = P.itemOwnerID")
fun getAllPurchases(): Flow<List<Purchase>>
More info could be found here
I am trying to observe Item data changes in a List using LiveData. But for some reason it is not working as expected.
ViewModel
#HiltViewModel
class TestScreenViewModel #Inject constructor(private val repository: TestRepository) :
ViewModel() {
val _orderItems: LiveData<List<OrderItem>> = repository.getAllTestOrder().asLiveData()
val orderItems: LiveData<List<OrderItem>> = _orderItems
fun addOrderItem() {
val item = OrderItem(name = "Order 1", price = 50, qty = 2)
viewModelScope.launch {
repository.addOrder(item)
}
}
fun deleteAll() = viewModelScope.launch { repository.deleteAll() }
fun changeValueOfItem() {
_orderItems.value!![0].addQty()
}
}
OrderItem
#Entity(tableName = "orders")
data class OrderItem constructor(
#PrimaryKey(autoGenerate = true)
#NonNull
val id: Int=0,
var name: String = "",
var price: Int = 0,
var imageUrl: String = "",
var qty: Int = 0
) {
fun addQty() {
qty++
}
fun removeQty() {
qty--
}
fun updateQty(q: Int) {
qty = q
}
}
During fun changeValueOfItem() call I just updated the qty by 1.
I already have a observable for orderItems in my Fragment but the changes are not detected.
What I am doing wrong here? Or Is there any other way to implement this scenario?
Faced similar behavior some time ago.
For it to work properly you need to actually return LiveData from your database like this:
//your DAO
#Query(select * from smth)
fun getAllTestOrder(): LiveData<List<OrderItems>>
And also remove .asLiveData() in your ViewModel and instead do something like this
val _orderItems = repository.getAllTestOrder()
val orderItems: LiveData<List<OrderItem>> = _orderItems
I get aMVoice from DetailViewModel and present it in UI for edit.
I invoke isChanged() in FragmentDetail and check whether the content of aMVoice is changed.
I can't get the correct result, I find that val b=(mDBVoiceRepository.getVoiceById(voiceId)).value always return null, why? How can I fix it ?
Code
class FragmentDetail : Fragment() {
private val mDetailViewModel by lazy {
getViewModel {
DetailViewModel( provideRepository(mContext), args.voiceId)
}
}
fun isChanged():Boolean{
mDetailViewModel.checkChanged()
return mDetailViewModel.isChanged.value!!
}
}
class DetailViewModel(private val mDBVoiceRepository: DBVoiceRepository, private val voiceId:Int) : ViewModel() {
val aMVoice=mDBVoiceRepository.getVoiceById(voiceId)
private val _isChanged= MutableLiveData<Boolean>(false)
val isChanged: LiveData<Boolean> = _isChanged
fun checkChanged(){
val a=aMVoice.value
val b=(mDBVoiceRepository.getVoiceById(voiceId)).value //It's null
_isChanged.value=!(a==b)
}
}
class DBVoiceRepository private constructor(private val mDBVoiceDao: DBVoiceDao){
fun getVoiceById(id:Int)=mDBVoiceDao.getVoiceById(id)
}
interface DBVoiceDao{
#Query("SELECT * FROM voice_table where id=:id")
fun getVoiceById(id:Int):LiveData<MVoice>
}
#Entity(tableName = "voice_table", indices = [Index("createdDate")])
data class MVoice(
#PrimaryKey (autoGenerate = true) #ColumnInfo(name = "id") var id: Int = 0,
var name: String = "",
var path: String = ""
)
It appears to me that you are using live data without observing it, live data does not emit ant data except it has an active observer. So you should create an observer in your fragment instead of manually checking for the value.
Also, it's not necessary to keep variables to detect if the value has changed, live data can do that for you with distinctUntilChanged() operator.
So your code should look like something below
class FragmentDetail : Fragment() {
private val mDetailViewModel by lazy {
getViewModel {
DetailViewModel(provideRepository(mContext), args.voiceId)
}
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
observeVoiceChange()
}
fun observeVoiceChange() {
mDetailViewModel.checkChanged().distinctUntilChanged().observe(viewLifecycleOwner, Observer<MVoice> {
//Do whatever you want when voice changes
})
}
}
class DetailViewModel(private val mDBVoiceRepository: DBVoiceRepository, private val voiceId: Int) : ViewModel() {
fun checkChanged(): LiveData<MVoice> {
return (mDBVoiceRepository.getVoiceById(voiceId))
}
}
class DBVoiceRepository private constructor(private val mDBVoiceDao: DBVoiceDao) {
fun getVoiceById(id: Int) = mDBVoiceDao.getVoiceById(id)
}
interface DBVoiceDao {
#Query("SELECT * FROM voice_table where id=:id")
fun getVoiceById(id: Int): LiveData<MVoice>
}
#Entity(tableName = "voice_table", indices = [Index("createdDate")])
data class MVoice(
#PrimaryKey(autoGenerate = true) #ColumnInfo(name = "id") var id: Int = 0,
var name: String = "",
var path: String = ""
)
once the fragment starts, it should observe for changes in the voice data and return and work with those changes.