Issue
When the app is ran, no errors occur however, the databases folder in device file explorer is empty. I have tried to fix the issue myself but since I'm new to android studio and kotlin I haven't been able to I just want to ensure the database is working before I carry on with the rest of the code.
Any help would be greatly appreciated.
Code:
Todo
package com.example.todoit.data
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "todo_data")
data class Todo (
@PrimaryKey val id: Int,
val title: String,
var isChecked: Boolean = false
)
TodoDao
package com.example.todoit.data
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
@Dao
interface TodoDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun addTodo(todo: Todo)
@Query("SELECT * FROM todo_data ORDER BY id ASC")
fun readAllData(): LiveData<List<Todo>>
}
TodoDataBase
package com.example.todoit.data
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [Todo::class],version = 1, exportSchema = false)
abstract class TodoDataBase: RoomDatabase() {
abstract fun todoDao(): TodoDao
companion object{
@Volatile
private var INSTANCE: TodoDataBase? = null
fun getDataBase(context: Context):TodoDataBase{
val tempInstance = INSTANCE
if(tempInstance != null){
return tempInstance
}
synchronized(this){
val instance = Room.databaseBuilder(
context.applicationContext,
TodoDataBase::class.java,
"todo_database"
).build()
INSTANCE = instance
return instance
}
}
}
}
TodoRepository
package com.example.todoit.data
import androidx.lifecycle.LiveData
class TodoRepository(private val todoDao:TodoDao) {
val readAllData: LiveData<List<Todo>> = todoDao.readAllData()
suspend fun addTodo(todo:Todo) {
todoDao.addTodo(todo)
}
}
TodoViewModel
package com.example.todoit.data
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class TodoViewModel(application: Application) : AndroidViewModel(application) {
private val readAllData: LiveData<List<Todo>>
private val repository: TodoRepository
init {
val todoDao = TodoDataBase.getDataBase(application).todoDao()
repository = TodoRepository(todoDao)
readAllData = repository.readAllData
}
fun addTodoToDataBase(todo: Todo) {
viewModelScope.launch(Dispatchers.IO) {
repository.addTodo(todo)
}
}
}
MainActivity
package com.example.todoit
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.todoit.data.Todo
import com.example.todoit.data.TodoViewModel
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
private lateinit var todoAdapter: TodoAdapter
private lateinit var todoViewModel: TodoViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
todoViewModel = ViewModelProvider(this).get(TodoViewModel::class.java)
todoAdapter = TodoAdapter(mutableListOf())
rvTodoItems.layoutManager = LinearLayoutManager(this)
rvTodoItems.adapter = todoAdapter
btnAddTodo.setOnClickListener {
val todoTitle = etTodoTitle.text.toString()
if (todoTitle.isNotEmpty()) {
val todo = Todo(0,todoTitle,false)
etTodoTitle.text.clear()
insertDataToDataBase(todo)
todoAdapter.addTodo(todo)
}
btnDeleteTodo.setOnClickListener {
todoAdapter.deleteDoneTodos()
}
}}
private fun insertDataToDataBase(todo: Todo) {
val todoTitle = etTodoTitle.text.toString()
if(todoTitle.isNotEmpty()) {
//Add data to database
todoViewModel.addTodoToDataBase(todo)
}
}
}
TodoAdapter
package com.example.todoit
import android.graphics.Paint.STRIKE_THRU_TEXT_FLAG
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.example.todoit.data.Todo
import kotlinx.android.synthetic.main.item_todo.view.*
class TodoAdapter(
private val todos: MutableList<Todo>,
) : RecyclerView.Adapter<TodoAdapter.TodoViewHolder>() {
class TodoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoViewHolder {
return TodoViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_todo,
parent,
false
)
)
}
fun addTodo(todo: Todo) {
todos.add(todo)
notifyItemInserted(todos.size - 1)
}
fun deleteDoneTodos() {
todos.removeAll { todo ->
todo.isChecked
}
notifyDataSetChanged()
}
private fun toggleStrikeThrough(tvTodoTitle: TextView, isChecked: Boolean) {
if (isChecked) {
tvTodoTitle.paintFlags = tvTodoTitle.paintFlags or STRIKE_THRU_TEXT_FLAG
} else {
tvTodoTitle.paintFlags = tvTodoTitle.paintFlags and STRIKE_THRU_TEXT_FLAG.inv()
}
}
override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
val curTodo = todos[position]
holder.itemView.apply {
tvTodoTitle.text = curTodo.title
cbDone.isChecked = curTodo.isChecked
toggleStrikeThrough(tvTodoTitle, curTodo.isChecked)
cbDone.setOnCheckedChangeListener { _, isChecked ->
toggleStrikeThrough(tvTodoTitle, isChecked)
curTodo.isChecked = !curTodo.isChecked
}
}
}
override fun getItemCount(): Int {
return todos.size
}
}
Gradle(Module)
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id "kotlin-android-extensions"
}
apply plugin: 'kotlin-kapt'
android {
compileSdk 32
defaultConfig {
applicationId "com.example.todoit"
minSdk 21
targetSdk 32
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
//ROOM
def roomVersion = "2.4.2"
implementation "androidx.room:room-ktx:$roomVersion"
kapt "androidx.room:room-compiler:$roomVersion"
// Navigation Component
implementation 'androidx.navigation:navigation-fragment-ktx:2.2.2'
implementation 'androidx.navigation:navigation-ui-ktx:2.2.2'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.6.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
// Lifecycle components
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
// Kotlin components
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.72"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5"
}
Gradle(Project)
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '7.2.0' apply false
id 'com.android.library' version '7.2.0' apply false
id 'org.jetbrains.kotlin.android' version '1.5.30' apply false
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Solution
I just want to ensure the database is working before I carry on with the rest of the code.
Then I would suggest testing that in isolation (there are some issues).
I would suggest temporarily adding .allowMainThreadQueries
to the databaseBuilder in the TodoDataBase class e.g.
synchronized(this){
val instance = Room.databaseBuilder(
context.applicationContext,
TodoDataBase::class.java,
"todo_database"
)
.allowMainThreadQueries() // ADDED <<<<<<<<<<
.build()
INSTANCE = instance
return instance
}
You can now circumvent having to run on another thread.
You can then add another function to the TodoDao e.g.
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun altInsert(todo: Todo): Long
You can then, again temporarily, add/amend MainActivity to invoke the insert on the main thread by:-
a) Adding 2 lateinits for vars to hold a TodoDataBase and a TodoDao instance e.g. :-
lateinit var db: TodoDataBase
lateinit var dao: TodoDao
b) Instantiating the new lateinits (suggest immediately after the setContentView) :-
db = TodoDataBase.getDataBase(this)
dao = db.todoDao()
c) In the Add Button's on click listener replace the insertDataToDatabase with invocation of altInsert, perhaps with checking (Toast as an example) e.g. :-
/* ADDED */
//insertDataToDataBase(todo)
if (dao.altInsert(todo) > 0) {
Toast.makeText(this,"Added",Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this,"Oooops",Toast.LENGTH_SHORT).show()
}
/* END OF ADDED */
Now you can concentrate on the database aspect by running the App and then testing, say by adding 3 Todo's (e.g. Test001, Test002 and Test003) and:-
So when the App is running enter Test001 in the title and then click the ADD button:-
- Now the Toast says Oops. However, using App Inspection (much better then Device Explorer) :-
So now on to Test002
Obviously there is an issue as Test001 has disappeared.
This is a twofold issue as such, the Todo's id column is defined using @PrimaryKey val id: Int,
and your insert sets the id to 0. Thus a conflict occurs because an id of 0 already exists and thus Test001 is replaced by Test002, when I believe that you would want 2 Todos Test001 and Test002.
I would suggest changing to use @PrimaryKey val id: Long?=null,
. This allows no value/null to be supplied and thus SQLite will generate a unique id (typically 1 then 2 then 3 ....). The id can be a 64 bit signed integer, thus Long rather than Int is more apt (it takes no more storage as far as SQLite is concerned as it will store the value in as few bytes as it can).
An alternative would be to code
@PrimaryKey(autoGenerate = true)
when 0 is converted to no value/null by Room. However, this is at the expense of the inclusion of the AUTOINCREMENT keyword, which is very unlikely to be necessary and that is inefficient.- The SQLite documentation says The AUTOINCREMENT keyword imposes extra CPU, memory, disk space, and disk I/O overhead and should be avoided if not strictly needed. It is usually not needed.
So Todo could become :-
@Entity(tableName = "todo_data")
data class Todo (
@PrimaryKey val id: Long?=null,
val title: String,
var isChecked: Boolean = false
)
To not use 0, with the above change, then the insert could be changed to val todo = Todo(title = todoTitle, isChecked = false)
After uninstalling the App (so no need to increase the version) and running, then adding the 3 tests:-
However, the App itself does not reflect this, it shows:-
This is an issue within the TodoAdapter, which would be another issue/question.
And of course using Device Explorer, then :-
Answered By - MikeT
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.