Search code examples
androidkotlinandroid-roomandroid-livedataandroid-viewmodel

One-to-many in Room with Kotlin


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?


Solution

  • 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.