Today I started learning how to use Room for my simple test project. My current issue is retrieving the saved data as a String, and not the entire entity. I included an image down below:
I just want the title for each entity. Any ideas?
MainActivity:
class MainActivity : AppCompatActivity() {
lateinit var mAdapter: MyAdapter
lateinit var db: NotesDatabase
var itemsList = mutableListOf<String>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
loadApp()
}
private fun loadApp() {
GlobalScope.launch {
dataBaseSetup()
withContext(Dispatchers.Main) {
setUpRecycler()
}
}
}
//Database
private fun dataBaseSetup() {
db = Room.databaseBuilder(
applicationContext, NotesDatabase::class.java, "notes-list.db"
).build()
//Database
GlobalScope.launch {
var dataBaseList = db.notesDao().getAllNotes() as MutableList
Log.d("Main", "$dataBaseList")
for (i in 0 until dataBaseList.size) {
itemsList.add("${dataBaseList[i]}")
}
}
}
private fun setUpRecycler() {
mAdapter =
MyAdapter(itemsList)
val mList: DragDropSwipeRecyclerView = findViewById(R.id.list)
mList.layoutManager = LinearLayoutManager(this)
mList.adapter = mAdapter
mList.orientation =
DragDropSwipeRecyclerView.ListOrientation.VERTICAL_LIST_WITH_VERTICAL_DRAGGING
mList.disableSwipeDirection(DragDropSwipeRecyclerView.ListOrientation.DirectionFlag.RIGHT)
val onItemSwipeListener = object : OnItemSwipeListener<String> {
override fun onItemSwiped(
position: Int,
direction: OnItemSwipeListener.SwipeDirection,
item: String
): Boolean {
Log.d("Main", "Position = $position, Direction = $direction, Item = $item")
when (direction) {
OnItemSwipeListener.SwipeDirection.RIGHT_TO_LEFT -> {
Toast.makeText(
applicationContext,
"$item deleted",
Toast.LENGTH_SHORT
).show()
//todo: add deleted code here
//Database
GlobalScope.launch(Dispatchers.Default) {
db.notesDao().delete(NotesEntity("$item"))
}
}
OnItemSwipeListener.SwipeDirection.LEFT_TO_RIGHT -> {
Toast.makeText(
applicationContext,
"$item archived",
Toast.LENGTH_SHORT
).show()
//todo: add archived code here
}
else -> return false
}
return false
}
}
mList.swipeListener = onItemSwipeListener
// button
fabAddItem()
}
private fun fabAddItem() {
fab_add.setOnClickListener {
Log.d("Main", "Button pressed")
val builder = AlertDialog.Builder(this)
val inflater = layoutInflater
val dialogLayout = inflater.inflate(R.layout.edit_text_layout, null)
val editText = dialogLayout.findViewById<EditText>(R.id.et_editText)
with(builder) {
setTitle("Enter some text!")
setPositiveButton("OK") { dialog, which ->
mAdapter.updateItem(editText.text.toString())
//Database
GlobalScope.launch(Dispatchers.Default) {
db.notesDao().insertAll(NotesEntity(editText.text.toString()))
}
Toast.makeText(
applicationContext,
"Text added successfully",
Toast.LENGTH_SHORT
).show()
}
setNegativeButton("Cancel") { dialog, which ->
Log.d("Main", "Negative button clicked")
}
setView(dialogLayout)
show()
}
}
}
}
NotesEntity Class:
#Entity(tableName = "notes_items")
data class NotesEntity(
#PrimaryKey var title: String
)
NotesDao:
#Dao
interface NotesDao {
#Query("SELECT * FROM notes_items")
fun getAllNotes(): List<NotesEntity>
#Query("SELECT * FROM notes_items WHERE title LIKE :title")
fun getTitle(title: String): NotesEntity
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(vararg todo: NotesEntity)
#Delete
fun delete(title: NotesEntity)
#Update
fun updateNotes(vararg title: NotesEntity)
}
NotesDatabase:
#Database(entities = [NotesEntity::class], version = 1)
abstract class NotesDatabase : RoomDatabase() {
abstract fun notesDao(): NotesDao
companion object {
#Volatile private var instance: NotesDatabase? = null
private val LOCK = Any()
operator fun invoke(context: Context)= instance ?: synchronized(LOCK){
instance ?: buildDatabase(context).also { instance = it}
}
private fun buildDatabase(context: Context) = Room.databaseBuilder(context,
NotesDatabase::class.java, "todo-list.db")
.build()
}
}
"${dataBaseList[i]}" should be "${dataBaseList[i].title}", the only field of your entity
In the SQL query, when you type Select *, the * here stands for ALL. So your query will return the object, in order to just return the string, you have to replace the * with the column name (attribute) you want to fetch as:
Select title from notes_items
Related
I am using flow to get data after any changes in my room database ,my app is note and it use SELECT by searching and filter by priority [High,Normal,Low]. and it has delete and update and insert too. I get flows from model and in viewModel by search and filter I Collect my data from flows and post it to my single liveData and in the last I get them in my Observer in the view
my problem is after in SELECT when I use UPDATE or INSERTT or DELETE , all of my old SELECT(search and filter) repeat in the moment. it means if I search 1000 once, and use filter to my notes 99 once; I will collect 1099 flows in the moment again and my observer in the my view will be bombing!!!
My Dao:
#Dao
interface NoteDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun saveNote(entity : NoteEntity)
#Delete
suspend fun deleteNote(note : NoteEntity)
#Update
suspend fun updateNote(note : NoteEntity)
#Query("SELECT * FROM $NOTE_TABLE")
fun getAllNotes() : Flow<MutableList<NoteEntity>>
#Query("SELECT * FROM $NOTE_TABLE WHERE id == :id")
fun getNote(id : Int) : Flow<NoteEntity>
#Query("SELECT * FROM $NOTE_TABLE WHERE priority == :priority")
fun fileNote(priority : String) : Flow<MutableList<NoteEntity>>
#Query("SELECT * FROM $NOTE_TABLE WHERE title LIKE '%' || :title || '%'")
fun searchNote(title : String) : Flow<MutableList<NoteEntity>>
}
My Repository:
class MainRepository #Inject constructor(private val dao : NoteDao) {
fun getAllNotes() = dao.getAllNotes()
fun getNotesByPriority(priority : String) = dao.fileNote(priority)
fun getNotesBySearch(title : String) = dao.searchNote(title)
suspend fun deleteNote(note: NoteEntity) = dao.deleteNote(note)
}
My ViewModel:
#HiltViewModel
class MainViewModel #Inject constructor(private val repository: MainRepository) : ViewModel() {
val notesData = object : MutableLiveData<DataStatus<MutableList<NoteEntity>>>(){
override fun postValue(value: DataStatus<MutableList<NoteEntity>>?) {
super.postValue(value)
Log.e("TAGJH", "postValue: ${value?.data?.size}")
}
}
fun getAllNotes() = viewModelScope.launch {
repository.getAllNotes().collect {
Log.e("TAGJH", "getAll: ${it.size}")
notesData.postValue(DataStatus.success(it, it.isEmpty()))
}
}
fun getNoteByPriority(priority: String) = viewModelScope.launch {
repository.getNotesByPriority(priority).collect {
Log.e("TAGJH", "priority ${it.size }->$priority")
notesData.postValue(DataStatus.success(it, it.isEmpty()))
}
}
fun getNoteBySearch(characters: String) = viewModelScope.launch {
repository.getNotesBySearch(characters).collect {
Log.e("TAGJH", "collect: ${it.size}")
notesData.postValue(DataStatus.success(it, it.isEmpty()))
}
}
fun deleteNote(note: NoteEntity) = viewModelScope.launch {
repository.deleteNote(note)
}
}
My View:
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private var selectedItem: Int = 0
private var _binding: ActivityMainBinding? = null
private val binding get() = _binding
#Inject
lateinit var noteAdapter: NoteAdapter
#Inject
lateinit var noteEntity: NoteEntity
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding?.root)
binding?.apply {
setSupportActionBar(notesToolbar)
addNoteBtn.setOnClickListener {
NoteFragment().show(supportFragmentManager, NoteFragment().tag)
}
viewModel.notesData.observe(this#MainActivity) {
showEmpty(it.isEmpty)
noteAdapter.setData(it.data!!)
noteList.apply {
layoutManager =
StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
adapter = noteAdapter
}
}
viewModel.getAllNotes()
notesToolbar.setOnMenuItemClickListener {
when (it.itemId) {
R.id.actionFilter -> {
filterByProperty()
return#setOnMenuItemClickListener true
}
else -> return#setOnMenuItemClickListener false
}
}
deleteAndUpdateListener()
}
}
private fun deleteAndUpdateListener() =
noteAdapter.onItemClickListener { note, title ->
when (title) {
DELETE -> {
noteEntity.id = note.id
noteEntity.title = note.title
noteEntity.des = note.des
noteEntity.priority = note.priority
noteEntity.category = note.category
viewModel.deleteNote(noteEntity)
}
EDIT -> {
val noteFragment = NoteFragment()
val bundle = Bundle()
bundle.putInt(BUNDLE_ID, note.id)
noteFragment.arguments = bundle
noteFragment.show(supportFragmentManager, NoteFragment().tag)
}
}
}
private fun showEmpty(isShown: Boolean) {
binding?.apply {
if (isShown) {
emptyShowing.visibility = View.VISIBLE
noteList.visibility = View.GONE
} else {
emptyShowing.visibility = View.GONE
noteList.visibility = View.VISIBLE
}
}
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_toolbar, menu)
val search = menu.findItem(R.id.actionSearch)
val searchView = search.actionView as SearchView
searchView.queryHint = getString(R.string.search)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return false
}
override fun onQueryTextChange(newText: String): Boolean {
viewModel.getNoteBySearch(newText)
Log.e("TAGJH", "searching")
return true
}
})
return super.onCreateOptionsMenu(menu)
}
private fun filterByProperty() {
val builder = AlertDialog.Builder(this)
val priories = arrayOf(ALL, HIGH, NORMAL, LOW)
builder.setSingleChoiceItems(priories, selectedItem) { dialog, item ->
when (item) {
0 -> {
viewModel.getAllNotes()
}
in 1..3 -> {
viewModel.getNoteByPriority(priories[item])
}
}
selectedItem = item
dialog.dismiss()
}
val dialog: AlertDialog = builder.create()
dialog.show()
}
}
I want to see my note after any update or delete or insert is updating in my last SELECT from room database
So I am encountering a weird issue in my code where a coroutine is being completely skipped when I debug through my code and I am not sure why it is happening.
Basically I call this method(insertLabelBind) in my Fragment in the onViewCreated.
//FRAGMENT
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val addLabelButton: ImageView = view.findViewById(R.id.profile_edit_add_label_row_button)
val addQuestionButton: ImageView = view.findViewById(R.id.profile_edit_add_question_response_button)
val editBookImage: ImageView = view.findViewById(R.id.profile_image_edit)
navController = Navigation.findNavController(view)
bindBasicInfo(view)
bindLabels(view)
bindQandA(view)
addLabelButton.setOnClickListener{
insertLabelBind(view)
//add to label live data
}}
private fun insertLabelBind(view: View) = launch{
val word: EditText? = view.findViewById(R.id.profile_label_edit_add_row)
val labelTag = viewModel.createLabel(word?.text.toString())
labelTag?.observeOnce(viewLifecycleOwner, Observer { tag ->
if (tag == null) return#Observer
CoroutineScope(Main).launch {
setLabelTextInUI(tag, word)
}
})
}
Then in the Fragment's ViewModel this gets called and it keeps failing to retrieve for the label result
//VIEWMODEL
suspend fun createLabel(label: String): LiveData<LabelTag>? {
var labelResult: LiveData<LabelTag>? = null
var userLabelResult: LiveData<UserLabel>? = null
val labelTag = LabelTag(
0,
profileId,
label,
LocalDateTime.now().toString(),
LocalDateTime.now().toString(),
""
)
labelResult = coverRepository.createLabelTag(labelTag)
userLabelResult = coverRepository.getSingleUserLabel(profileId, labelResult?.value!!.id)
if (userLabelResult == null) {
val userLabel = UserLabel(
0,
profileId,
labelResult!!.value!!.id,
1,
label,
LocalDateTime.now().toString(),
LocalDateTime.now().toString(),
""
)
userLabelResult = coverRepository.createUserLabel(userLabel)
}
return if (userLabelResult != null) {
labelResult
} else
null
}
In the repository, this createLabelTag Method gets called
//REPOSITORY
override suspend fun createLabelTag(labelTag: LabelTag): LiveData<LabelTag>? {
return withContext(IO) {
val result = createLabelTagSource(labelTag)
when (result.status) {
Status.SUCCESS -> labelTagDao.getTagById(labelTag.id)
Status.LOADING -> null
Status.ERROR -> null
}
}
}
private suspend fun createLabelTagSource(labelTag: LabelTag): Resource<LabelTag> {
return labelTagDataSource.createLabelTag(
labelTag
)
}
This then calls the labelTagDataSource
//DATASOURCE
override suspend fun createLabelTag(labelTag: LabelTag): Resource<LabelTag> {
return try {
val fetchedLabelTag = responseHandler.handleSuccess(coverApiService
.createLabelTag(labelTag))
val list = listOf(fetchedLabelTag.data!!)
list.first().profileId = labelTag.profileId
_downloadedLabelTag.postValue(list)
fetchedLabelTag
} catch (e: NoConnectivityException) {
Log.e("Connectivity", "No internet connection.", e)
responseHandler.handleException(e)
} catch (e: Exception) {
responseHandler.handleException(e)
}
}
The datasource triggers an observer in the repository for the downloadLabelTag and the data gets persisted into the labelTag room DB
//REPOSITORY
labelTagDataSource.apply {
downloadedLabelTag.observeForever { newLabelTag ->
persistFetchedLabelTag(newLabelTag)
}
}
private fun persistFetchedLabelTag(labelTags: List<LabelTag>) {
job = Job()
job?.let { repoJob ->
CoroutineScope(IO + repoJob).launch {
if(labelTags.size > 1)
labelTagDao.deleteAll()
for(result in labelTags){
labelTagDao.upsertTag(result)
}
repoJob.complete()
}
}
}
//DAO
interface LabelTagDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsertTag(label: LabelTag)
#Query("SELECT * FROM label_tag WHERE label = :tag")
fun getLabelByTag(tag: String): LiveData<LabelTag>
#Query("SELECT * FROM label_tag WHERE id = :id")
fun getTagById(id: Long): LiveData<LabelTag>
#Query("SELECT * FROM label_tag WHERE profileId = :profileId")
fun getTagMultipleTags(profileId: Int): LiveData<List<LabelTag>>
#Query("DELETE FROM label_tag")
suspend fun deleteAll()
}
The issue is occurring the Repository.persistFetchedLabelTag and that CoroutineScope is being completely skipped. This results the ViewModel's labelResult to return null. Would like to know why this coroutine is being skipped. Any help will be grateful.
I am using the following DAO
#Dao
interface GroceryListDao {
#Insert
fun insert(list: GroceryList)
#Update
fun update(list: GroceryList)
#Query("Delete from grocery_list_table")
fun clear()
#Query ("Select * from grocery_list_table")
fun getAllItems(): LiveData<List<GroceryList>>
#Query ("Select * from grocery_list_table where itemId = :item")
fun getItem(item: Long): GroceryList?
#Query ("Select * from grocery_list_table where item_status = :status")
fun getItemsBasedOnStatus(status: Int): List<GroceryList>
}
And my database has 3 columns groceryId(Long - autogenerated), groceryName(String) and groceryStatus(Int - 1/0).
When I am using getItemsBasedOnStatus(status: Int) without using LiveData I am able to retrieve the data. But when it is wrapped with LiveData I am getting null.
The other issue is when I get a list of items from a database without wrapping with LiveData and assigning to MutableLiveData in ViewModel, then the assigned MutableLiveData is displaying null values. I see lot of questions on this but no answer.
Adding code for my viewModel and Fragment
ViewModel
class GroceryListViewModel(
val database: GroceryListDao,
application: Application
) : AndroidViewModel(application) {
private var viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
var grocerylistItems = database.getAllItems()
var groceryItem = MutableLiveData<GroceryList>()
var groceryitems = MutableLiveData<List<GroceryList>>()
init {
getAllItemsFromDatabase()
}
fun insertIntoDatabase(item: GroceryList) {
uiScope.launch {
insert(item)
}
}
private suspend fun insert(item: GroceryList) {
withContext(Dispatchers.IO) {
database.insert(item)
}
}
fun updateGrocerylist(itemId: Long, status: Int) {
uiScope.launch {
groceryItem.value = getItem(itemId)
groceryItem.value?.itemStatus = status
groceryItem.value?.let { update(it) }
}
}
private suspend fun update(item: GroceryList) {
withContext(Dispatchers.IO) {
database.update(item)
}
}
private suspend fun getItem(itemId: Long): GroceryList? {
return withContext(Dispatchers.IO) {
var item = database.getItem(itemId)
item
}
}
fun getAllItemsFromDatabase() {
uiScope.launch {
getAllItems()
}
}
private suspend fun getAllItems() {
withContext(Dispatchers.IO) {
grocerylistItems = database.getAllItems()
}
}
fun getPendingItemsFromDatabase(status: Int) {
uiScope.launch {
getPendingItems(status)
}
}
private suspend fun getPendingItems(status: Int) {
withContext(Dispatchers.IO) {
val items = database.getItemsBasedOnStatus(status)
groceryitems.postValue(items)
Log.i("Grocery List", "Pending Items:" + items.size)
}
}
fun getDoneItemsFromDatabase(status: Int) {
uiScope.launch {
getDoneItems(status)
}
}
private suspend fun getDoneItems(status: Int) {
withContext(Dispatchers.IO) {
val items = database.getItemsBasedOnStatus(status)
groceryitems.postValue(items)
Log.i("Grocery List", "Done Items:" + items.size)
}
}
fun clearAllItemsFromDatabase() {
uiScope.launch {
clearItems()
}
}
private suspend fun clearItems() {
withContext(Dispatchers.IO) {
database.clear()
getAllItemsFromDatabase()
}
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
Fragment
class GroceryLIstFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val binding = FragmentGroceryLIstBinding.inflate(inflater,container,false)
val application = requireNotNull(this.activity).application
val dataSource = GroceryDatabase.getInstance(application)?.groceryListDatabaseDao
val viewModelFactory = dataSource?.let { GroceryListViewModelFactory(it, application) }
val viewModel = viewModelFactory?.let {
ViewModelProvider(
this,
it
).get(GroceryListViewModel::class.java)
}
viewModel?.grocerylistItems?.observe(this , Observer {
binding.grocerylistView.removeAllViews() // is it correct ?
for (item in it){
Log.i("Grocery List","Grocery Id=" + item.itemId+" ,Grocery Name=" + item.itemName +", Grocery status="+item.itemStatus)
addItemToScrollbar(item, binding, viewModel)
}
})
binding.addGrocery.setOnClickListener {
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(view?.windowToken, 0)
val item = binding.groceryitemField.text.toString()
if (!item.isNullOrBlank()) {
val newItem = GroceryList(itemName = item)
viewModel?.insertIntoDatabase(newItem)
if (viewModel != null) {
addItemToScrollbar(newItem, binding,viewModel)
}
binding.groceryitemField.text.clear()
}
}
binding.doneCheckbox.setOnClickListener {
if (binding.doneCheckbox.isChecked)
viewModel?.getDoneItemsFromDatabase(1)
else
viewModel?.getAllItemsFromDatabase()
}
binding.pendingCheckbox.setOnClickListener {
if (binding.pendingCheckbox.isChecked) {
viewModel?.getPendingItemsFromDatabase(0)
}
else
viewModel?.getAllItemsFromDatabase()
}
binding.clearGrocery.setOnClickListener {
viewModel?.clearAllItemsFromDatabase()
binding.grocerylistView.removeAllViews()
}
return binding.root
}
private fun addItemToScrollbar(
item: GroceryList,
binding: FragmentGroceryLIstBinding,
viewModel: GroceryListViewModel
) {
val itemBox = AppCompatCheckBox(context)
itemBox.text = item.itemName
itemBox.isChecked = item.itemStatus == 1
binding.grocerylistView.addView(itemBox)
itemBox.setOnClickListener {
val itemstatus: Int = if (itemBox.isChecked)
1
else {
0
}
viewModel?.updateGrocerylist(item.itemId,itemstatus)
}
}
}
Any help would be appreciated.
This most likely the same issue as here (read the linked answer). Due to way asynchronous way LiveData is working, it will return null when you call it. LiveData is meant to be used in conjunction with Observers, that will be triggered once changes to observed subject occur.
An Observer can look like this
database.getItemsBasedOnStatus(status).observe(viewLifecycleOwner, Observer { groceryList->
// Do cool grocery stuff here
})
If you want to retrieve your data inside your ViewModel you do not have a viewLifecycleOwner, you can then use "observeForever()", but then you have to remove the Observer explicitly, see this answer.
Same issue and answer also in this post
The task is to open an activity with notes attached to this diary when you select a single diary.
(one-to-many)
This is how entities in the database look like:
#Entity(tableName = "word_table")
data class Word(#ColumnInfo(name = "word") val word: String,
#ColumnInfo(name = "description") val description : String
)
{
#ColumnInfo(name = "id")
#PrimaryKey(autoGenerate = true)
var id : Long = 0
}
#Entity(tableName = "note_table")
data class Note(#ColumnInfo(name = "note_name") val note : String,
#ColumnInfo(name = "text") val text : String,
#ColumnInfo(name = "diaryId") val diaryId : Long
){
#PrimaryKey(autoGenerate = true)
var idNote : Long = 0
}
Using a data class in NoteRepository.kt
data class NotesAndWords (#Embedded val word : Word,
#Relation(parentColumn = "id", entityColumn = "diaryId")
val notes : List<Note>)
And a Query in WordDao.kt
#Transaction
#Query("SELECT * from word_table ")
fun getSomeNotes() : LiveData<List<NotesAndWords>>
I get the data and save it in the NoteRepository class:
class NoteRepository (private val wordDao : WordDao) {
var allNotes : LiveData<List<NotesAndWords>> = wordDao.getSomeNotes()
suspend fun insertNote(note : Note)
{
wordDao.insertNote(note)
}
}
Then via NoteViewModel.kt passing data to NoteActivity.kt:
class NoteViewModel(application: Application) : AndroidViewModel(application) {
private val repository: NoteRepository
val allNotes: LiveData<List<NotesAndWords>>
init {
val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()
repository = NoteRepository(wordsDao)
allNotes = repository.allNotes
}
fun insertNote(note: Note) = viewModelScope.launch {
repository.insertNote(note)
}
}
(NoteActivity.kt)
class NoteActivity : AppCompatActivity() {
private val newWordActivityRequestCode = 1
private lateinit var noteViewModel: NoteViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_note)
val adapter = NoteListAdapter(this, intent.getLongExtra("tag", -1) ){
val intent = Intent(this, ClickedActivity::class.java)
intent.putExtra("tag", it.note)
startActivity(intent)
}
recyclerview1.adapter = adapter
recyclerview1.layoutManager = LinearLayoutManager(this)
noteViewModel = ViewModelProvider(this).get(NoteViewModel::class.java)
noteViewModel.allNotes.observe(this, Observer {
adapter.setNotes(it)
})
val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
val intent = Intent(this, NewWordActivity::class.java)
startActivityForResult(intent, newWordActivityRequestCode)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK)
{
data?.getStringArrayListExtra(NewWordActivity.EXTRA_REPLY)?.let {
val note = Note(it[0], it[1], intent.getLongExtra("tag", -1))
noteViewModel.insertNote(note)
}
}
else
{
Toast.makeText(applicationContext, R.string.empty_not_saved,
Toast.LENGTH_LONG).show()
}
}
Then, in the adapter, I use MutableMap to transform the list so that the key is the name id and the value is the notes selected on request (attached to a specific diary)
NoteListAdapter.kt:
class NoteListAdapter internal constructor(
context: Context,
val wordId: Long,
private val listener : (Note) -> Unit
) : RecyclerView.Adapter<NoteListAdapter.NoteViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
//private val mContext = context
private var notes = emptyList<NotesAndWords>() // Cached copy of words
private var notesMapped = mutableMapOf<Long, List<Note>>()
inner class NoteViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val noteItemView: TextView = itemView.findViewById(R.id.textView1)
private val noteDescriptionView: TextView = itemView.findViewById(R.id.textView)
fun bindView(note: Note, listener : (Note) -> Unit) {
noteItemView.text = note.diaryId.toString()
noteDescriptionView.text = note.text
itemView.setOnClickListener {
listener(note)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteViewHolder {
val itemView = inflater.inflate(R.layout.recyclerview_layout, parent,
false)
return NoteViewHolder(itemView)
}
override fun onBindViewHolder(holder: NoteViewHolder, position: Int) {
holder.bindView(notesMapped[wordId]!![position], listener)
}
internal fun setNotes(notes: List<NotesAndWords>) {
this.notes = notes
for (i in this.notes) {
notesMapped[i.word.id] = i.notes
}
notifyDataSetChanged()
}
override fun getItemCount() = notesMapped[wordId]!!.size
}
Database:
#Database(entities = [Word::class, Note::class], version = 2, exportSchema = false)
abstract class WordRoomDatabase : RoomDatabase() {
abstract fun wordDao(): WordDao
private class WordDatabaseCallback(private val scope: CoroutineScope) : RoomDatabase.Callback()
{
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
INSTANCE?.let { database ->
scope.launch {
populateDatabase(database.wordDao())
}
}
}
suspend fun populateDatabase(wordDao: WordDao) {
//wordDao.deleteAll()
//wordDao.deleteAllNotes()
}
}
companion object {
#Volatile
private var INSTANCE: WordRoomDatabase? = null
fun getDatabase(context: Context, scope:CoroutineScope): WordRoomDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance
}
val instance = Room.databaseBuilder(context.applicationContext,
WordRoomDatabase::class.java, "word_database")
.addCallback(WordDatabaseCallback(scope))
//.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
return instance
}
}
}
I've created several diaries and one note in each of them, using the buttons to create new diaries and notes. Now, if you select several diaries in turn, then on some attempt to select a diary, a NullPointerException is issued in the adapter, in this line:
override fun getItemCount() = notesMapped[wordId]!!.size
Why is this exception thrown if notesMapped always has the wordId key?
NoteActivity is called from another activity and the diary id is passed to it
This repository on GitHub: https://github.com/Lomank123/RoomDatabase
Edit:
noteViewModel.allNotes.observe(this, Observer {
var getList = emptyList<Note>()
for(i in it)
{
if(i.word.id == wordId)
{
getList = i.notes
break
}
}
adapter.setNotes(getList)
})
I've changed the Observer in NoteActivity and changed setNotes() method in adapter, but now it returns nothing. With for() I get the right notes and give them to adapter.setNotes(). If it doesn't work, how can I get the correct list of notes?
Hi initially the map is empty and the map is returning a null value and you are checking size on a null object.
Also as a good practice do not use a map instead use a list of notes only and pass the list directly.
My project basically allows the user to create a list of products, where different products can be added. So in my case, the relation existing between my entities is many to many: I have a table for all the products that are added when the app is installed, I have a table with the lists the user creates and finally I have a table that records when the user add a product to a list.
The problem I´m getting, is that after a user add a product to a list, the LiveData that is being observed in the activity does not update the list and I cannot figure out why.
The activity (the product code is introduced by the user in another activity started for result):
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_list_of_products)
// Get the listname from the bundle
listName = intent.extras.getString(resources.getString(R.string.INTENT_EXTRA_LISTNAME))
// Set up the ViewModel
viewModel = ViewModelProviders.of(this, ListOfProductsViewModelFactory(application, listName)).get(ListOfProductsViewModel::class.java)
// RecyclerView setup
val recyclerView = findViewById<RecyclerView>(R.id.productRecyclerView)
val mAdapter = ProductAdapter(this)
recyclerView.adapter = mAdapter
recyclerView.layoutManager = LinearLayoutManager(this)
viewModel!!.getProductsInProductList().observe(this, Observer {
products -> mAdapter.setProducts(products!!)
})
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if(requestCode == NEW_PRODUCT_ACTIVITY_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
val code = data!!.extras.getString(resources.getString(R.string.ADD_MANUALLY_ACTIVITY_REPLY))
val resultOfInsertion = viewModel!!.insertProductInProductList(code)
if(resultOfInsertion) {
Toast.makeText(applicationContext, "${code} successful added",
Toast.LENGTH_LONG).show()
} else {
Toast.makeText(applicationContext, "${code} was not added",
Toast.LENGTH_LONG).show()
}
}
else {
Toast.makeText(applicationContext, "Insertion cancelled",
Toast.LENGTH_LONG).show()
}
}
The ViewModel:
private var mRepo = ProductsInProductListRepository(application, listName)
private val productsInProductList = mRepo.getProductsInProductList()
fun getProductsInProductList() : LiveData<List<Product>> {
return productsInProductList
}
fun insertProductInProductList(code: String) : Boolean {
return mRepo.insertProductInProductList(code)
}
The repository:
private var productsInProductListDao : ProductsInProductListDao
private var productsInProductList : LiveData<List<Product>>
private val listName : String
constructor(application : Application, listName: String) {
val db = ProductDatabase.getProductDatabase(application)
this.productsInProductListDao = db!!.productsInProductListDao()
this.listName = listName
this.productsInProductList = productsInProductListDao.getProducstForProductList(listName)
}
fun getProductsInProductList() : LiveData<List<Product>> {
return productsInProductList
}
fun insertProductInProductList(productCode : String) : Boolean {
if(isProductAlreadyAdded(productCode)) {
return false
}
InsertProductInProductListAsync(productsInProductListDao, listName).execute(productCode)
return true
}
private fun isProductAlreadyAdded(productCode : String): Boolean {
return productsInProductListDao.getProductAddedToCertainList(listName, productCode).isNotEmpty()
}
The DAO:
#Dao
interface ProductsInProductListDao {
#Insert(onConflict = OnConflictStrategy.FAIL)
fun insertProductInProductList(productInProductList: ProductsInProductList)
#Query("SELECT code, model, pvpr, qtr, segmentation FROM product_table INNER JOIN products_in_productlist_table ON code=productCode WHERE listName=:listName")
fun getProducstForProductList(listName : String) : LiveData<List<Product>>
#Query("SELECT code, model, pvpr, qtr, segmentation FROM product_table INNER JOIN products_in_productlist_table ON code=productCode WHERE listName=:listName and code=:productCode")
fun getProductAddedToCertainList(listName : String, productCode: String) : List<Product>
}
The Entity:
#Entity(
indices = [Index("productCode")],
tableName = "products_in_productlist_table",
primaryKeys = ["listName", "productCode"],
foreignKeys = [
ForeignKey( onDelete = ForeignKey.CASCADE,
entity = ProductList::class,
parentColumns = ["name"],
childColumns = ["listName"]),
ForeignKey( entity = Product::class,
parentColumns = ["code"],
childColumns = ["productCode"])
]
)
class ProductsInProductList {
#NonNull
#ColumnInfo(name = "listName")
val listName : String
#NonNull
#ColumnInfo(name = "productCode")
val productCode : String
constructor(listName: String, productCode: String) {
this.listName = listName
this.productCode = productCode
}
}
Product adapter:
private val mInflater = LayoutInflater.from(context)
private val context = context
private var mProducts : List<Product>? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder {
val itemView = mInflater.inflate(R.layout.product_item, parent, false)
return ProductViewHolder(itemView)
}
fun setProducts(products : List<Product>) {
this.mProducts = products
}
override fun getItemCount(): Int {
if(mProducts != null)
return mProducts!!.size
return 0
}
override fun onBindViewHolder(holder: ProductViewHolder, position: Int) {
if(mProducts != null) {
val current = mProducts!!.get(position)
holder.setText(current.code)
} else {
holder.setText(context.resources.getString(R.string.lbl_no_list_created))
}
}
fun getProductAtPosition(position: Int) : Product {
return mProducts!!.get(position)
}
ProductViewHolder:
private var productItemView : TextView = itemView.findViewById(R.id.productItemRecyclerView)
fun setText(current: String) {
productItemView.text = current
}
Any idea why when creating a new row in the products_in_productlist_table table, the LiveData is not being updated?
Add the notifyDataSetChange() in your method in your AdapterClass :
fun setProducts(products : List<Product>) {
this.mProducts = products
notifyDataSetChange()
}