When I originally built my application, it took the cardName and hosted it in a RecyclerView that held a list of each card by name. Since I'm still new to development, I did this to see if I could get the RecyclerView working with just the names. Now I want to be able to add categories to each card in a one to many relationship and I followed the instructions under https://developer.android.com/training/data-storage/room/relationships but I may be missing something because I am getting the error below. Ideally, the application should work the same as before and just show the names, but now also have additional values that I can slowly add to fragments over time. I greatly appreciate any assistance as there may be something I am overlooking. Thanks in advance.
Card
package com.example.android.pointmax.database
import androidx.room.*
@Entity
data class Card(
@PrimaryKey(autoGenerate = true)
var cardId: Long = 0L,
var cardName: String
)
@Entity
data class Category(
@PrimaryKey(autoGenerate = true)
val categoryId: Long = 0L,
val cardCategoryId: Long = 0L,
var type: String = "General",
var earnRate: Double = 1.0,
var protection: Int = 0,
var redeemValue: String = "Cash"
)
data class CreditCards(
@Embedded val card: Card,
@Relation(
parentColumn = "cardId",
entityColumn = "cardCategoryId"
)
val categories: List<Category>
)
CardDao
package com.example.android.pointmax.database
import androidx.lifecycle.LiveData
import androidx.room.*
@Dao
interface CardDao {
@Transaction
@Query("SELECT * from Card")
fun getCards(): LiveData<List<CreditCards>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(card: Card)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertCategory(category: Category)
@Query("DELETE FROM Card")
suspend fun deleteAll()
@Query("DELETE FROM Card WHERE cardName = :name")
suspend fun deleteByName(name: String)
@Query("UPDATE Card SET cardName = :newName WHERE cardName = :oldName")
suspend fun editName(newName: String, oldName: String)
}
CardRepository
package com.example.android.pointmax.database
import androidx.lifecycle.LiveData
// Declares the DAO as a private property in the constructor. Pass in the DAO
// instead of the whole database, because you only need access to the DAO
class CardRepository(private val cardDao: CardDao) {
// Room executes all queries on a separate thread.
// Observed LiveData will notify the observer when the data has changed.
val allCards: LiveData<List<CreditCards>> = cardDao.getCards()
suspend fun insert(card: Card) {
cardDao.insert(card)
}
suspend fun insertCategory(category: Category) {
cardDao.insertCategory(category)
}
suspend fun deleteByName(name: String){
cardDao.deleteByName(name)
}
suspend fun editName(newName: String, oldName: String){
cardDao.editName(newName, oldName)
}
}
CardRoomDatabase
package com.example.android.pointmax.database
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
// Annotates class to be a Room Database with a table (entity) of the Card class
@Database(entities = arrayOf(Card::class), version = 1, exportSchema = false)
public abstract class CardRoomDatabase : RoomDatabase() {
abstract fun cardDao(): CardDao
companion object {
// Singleton prevents multiple instances of database opening at the
// same time.
@Volatile
private var INSTANCE: CardRoomDatabase? = null
fun getDatabase(
context: Context,
scope: CoroutineScope
): CardRoomDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance
}
synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
CardRoomDatabase::class.java,
"card_database"
).addCallback(CardDatabaseCallback(scope)).build()
INSTANCE = instance
return instance
}
}
private class CardDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
INSTANCE?.let { database ->
scope.launch {
populateDatabase(database.cardDao())
}
}
}
suspend fun populateDatabase(cardDao: CardDao) {
// Delete all content here.
cardDao.deleteAll()
// Add sample cards.
var card = Card(cardName = "Petal Credit Card")
cardDao.insert(card)
var category = Category(cardCategoryId = card.cardId, type = "General", earnRate = 1.5, protection = 0, redeemValue = "cash")
cardDao.insertCategory(category)
// card = Card(cardName = "Discover IT")
// cardDao.insert(card)
}
}
}
}
Build
Executing tasks: [:app:assembleDebug] in project C:\Users\jjrog\AndroidStudioProjects\PointMax
Task :app:preBuild UP-TO-DATE Task :app:preDebugBuild UP-TO-DATE Task :app:compileDebugAidl NO-SOURCE Task :app:generateDebugBuildConfig UP-TO-DATE Task :app:writeDebugApplicationId UP-TO-DATE Task :app:compileDebugRenderscript NO-SOURCE Task :app:generateSafeArgsDebug UP-TO-DATE Task :app:prepareLintJar UP-TO-DATE Task :app:prepareLintJarForPublish UP-TO-DATE Task :app:generateDebugSources UP-TO-DATE Task :app:dataBindingExportBuildInfoDebug UP-TO-DATE Task :app:dataBindingMergeDependencyArtifactsDebug UP-TO-DATE Task :app:dataBindingMergeGenClassesDebug UP-TO-DATE Task :app:generateDebugResValues UP-TO-DATE Task :app:generateDebugResources UP-TO-DATE Task :app:mergeDebugResources UP-TO-DATE Task :app:dataBindingGenBaseClassesDebug UP-TO-DATE Task :app:dataBindingExportFeaturePackageIdsDebug UP-TO-DATE Task :app:mainApkListPersistenceDebug UP-TO-DATE Task :app:createDebugCompatibleScreenManifests UP-TO-DATE Task :app:extractDeepLinksDebug UP-TO-DATE Task :app:processDebugManifest UP-TO-DATE Task :app:processDebugResources UP-TO-DATE Task :app:kaptGenerateStubsDebugKotlin Task :app:mergeDebugShaders UP-TO-DATE Task :app:compileDebugShaders UP-TO-DATE Task :app:generateDebugAssets UP-TO-DATE Task :app:mergeDebugAssets UP-TO-DATE Task :app:processDebugJavaRes NO-SOURCE Task :app:checkDebugDuplicateClasses UP-TO-DATE Task :app:desugarDebugFileDependencies UP-TO-DATE Task :app:mergeExtDexDebug UP-TO-DATE Task :app:mergeDebugJniLibFolders UP-TO-DATE Task :app:mergeDebugNativeLibs UP-TO-DATE Task :app:stripDebugDebugSymbols UP-TO-DATE Task :app:validateSigningDebug UP-TO-DATE
Task :app:kaptDebugKotlin ANTLR Tool version 4.5.3 used for code generation does not match the current runtime version 4.7.1ANTLR Runtime version 4.5.3 used for parser compilation does not match the current runtime version 4.7.1ANTLR Tool version 4.5.3 used for code generation does not match the current runtime version 4.7.1ANTLR Runtime version 4.5.3 used for parser compilation does not match the current runtime version 4.7.1C:\Users\jjrog\AndroidStudioProjects\PointMax\app\build\tmp\kapt3\stubs\debug\com\example\android\pointmax\database\CreditCards.java:12: error: There is a problem with the query: [SQLITE_ERROR] SQL error or missing database (no such table: Category) private final java.util.List categories = null; ^C:\Users\jjrog\AndroidStudioProjects\PointMax\app\build\tmp\kapt3\stubs\debug\com\example\android\pointmax\database\CardDao.java:17: error: Type of the parameter must be a class annotated with @Entity or a collection/array of it. com.example.android.pointmax.database.CreditCards card, @org.jetbrains.annotations.NotNull()
Task :app:kaptDebugKotlin FAILED ^ FAILURE: Build failed with an exception.
*What went wrong: Execution failed for task ':app:kaptDebugKotlin'.
A failure occurred while executing org.jetbrains.kotlin.gradle.internal.KaptExecution java.lang.reflect.InvocationTargetException (no error message)
- Try: Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
- Get more help at https://help.gradle.org
BUILD FAILED in 11s 29 actionable tasks: 2 executed, 27 up-to-date
Edit: BindingAdapter
package com.example.android.pointmax
import androidx.databinding.BindingAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.android.pointmax.database.Card
import com.example.android.pointmax.database.CreditCards
/**
* When there are no Cards (data is null), hide the [RecyclerView], otherwise show it.
*/
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView, data: List<CreditCards>?) {
val adapter = recyclerView.adapter as CardAdapter
adapter.submitList(data)
}
CardAdapter
package com.example.android.pointmax
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.android.pointmax.database.Card
import com.example.android.pointmax.database.CreditCards
import com.example.android.pointmax.databinding.RecyclerviewItemBinding
import com.example.android.pointmax.ui.wallet.WalletViewModel
import kotlinx.android.synthetic.main.recyclerview_item.view.*
class CardAdapter(val onClickListener: OnClickListener) :
ListAdapter<CreditCards, CardAdapter.CardViewHolder>(DiffCallback) {
/**
* The CardViewHolder constructor takes the binding variable from the associated
* LayoutViewItem, which nicely gives it access to the full [Card] information.
*/
class CardViewHolder (private var binding: RecyclerviewItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind (cards: CreditCards){
binding.creditCard = cards
// This is important, because it forces the data binding to execute immediately,
// which allows the RecyclerView to make the correct view size measurements
binding.executePendingBindings()
}
}
/**
* Allows the RecyclerView to determine which items have changed when the [List] of [Card]
* has been updated.
*/
companion object DiffCallback : DiffUtil.ItemCallback<CreditCards>() {
override fun areItemsTheSame(oldItem: CreditCards, newItem: CreditCards): Boolean {
return oldItem.card === newItem.card
}
override fun areContentsTheSame(oldItem: CreditCards, newItem: CreditCards): Boolean {
return oldItem.card.cardName == newItem.card.cardName
}
}
/**
* Create new [RecyclerView] item views (invoked by the layout manager)
*/
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CardViewHolder {
return CardViewHolder(RecyclerviewItemBinding.inflate(LayoutInflater.from(parent.context)))
}
/**
* Replaces the contents of a view (invoked by the layout manager)
*/
override fun onBindViewHolder(holder: CardViewHolder, position: Int) {
val current = getItem(position)
holder.itemView.setOnClickListener {
onClickListener.onClick(current)
}
holder.bind(current)
}
/**
* Custom listener that handles clicks on [RecyclerView] items. Passes the [CreditCards]
* associated with the current item to the [onClick] function.
* @param clickListener lambda that will be called with the current [Card]
*/
class OnClickListener(val clickListener: (cards : CreditCards) -> Unit) {
fun onClick(cards:CreditCards) = clickListener(cards)
}
}
Recyclerview_item
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="creditCard"
type="com.example.android.pointmax.database.CreditCards" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/card_name"
style="@style/card_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{creditCard.card.cardName.toString()}"
android:background="@android:color/holo_orange_light"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginBottom="@dimen/activity_vertical_margin"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</layout>
fragment_wallet.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.example.android.pointmax.ui.wallet.WalletViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.android.pointmax.MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/wallet_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/recyclerview_item"
app:layout_constraintBottom_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:listData="@{viewModel.allCards}"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
WalletViewModel
package com.example.android.pointmax.ui.wallet
import android.app.Application
import androidx.lifecycle.*
import com.example.android.pointmax.database.Card
import com.example.android.pointmax.database.CardRepository
import com.example.android.pointmax.database.CardRoomDatabase
import com.example.android.pointmax.database.CreditCards
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
class WalletViewModel(application: Application) : AndroidViewModel(application) {
private val repository: CardRepository
// Using LiveData and caching what getAlphabetizedWords returns has several benefits:
// - We can put an observer on the data (instead of polling for changes) and only update the
// the UI when the data actually changes.
// - Repository is completely separated from the UI through the ViewModel.
// The external LiveData interface to the property is immutable, so only this class can modify
val allCards: LiveData<List<CreditCards>>
// Internally, we use a MutableLiveData to handle navigation to the selected cxa
private val _navigateToSelectedCard = MutableLiveData<Card>()
// The external immutable LiveData for the navigation property
val navigateToSelectedCard: LiveData<Card>
get() = _navigateToSelectedCard
// Create a Coroutine scope using a job to be able to cancel when needed
private var viewModelJob = Job()
// the Coroutine runs using the Main (UI) dispatcher
private val coroutineScope = CoroutineScope(viewModelJob + Dispatchers.Main)
/**
* When the card is clicked, set the [_navigateToSelectedCard] [MutableLiveData]
* @param card The [Card] that was clicked on.
*/
fun displayCardDetails(card: Card) {
_navigateToSelectedCard.value = card
}
/**
* After the navigation has taken place, make sure navigateToSelectedProperty is set to null
*/
fun displayCardDetailsComplete() {
_navigateToSelectedCard.value = null
}
init {
val cardsDao = CardRoomDatabase.getDatabase(application, viewModelScope).cardDao()
repository = CardRepository(cardsDao)
allCards = repository.allCards
}
}
Errors I am still facing regarding the one to many relationships in app:
Task :app:kaptDebugKotlin ANTLR Tool version 4.5.3 used for code generation does not match the current runtime version 4.7.1ANTLR Runtime version 4.5.3 used for parser compilation does not match the current runtime version 4.7.1ANTLR Tool version 4.5.3 used for code generation does not match the current runtime version 4.7.1ANTLR Runtime version 4.5.3 used for parser compilation does not match the current runtime version 4.7.1C:\Users\jjrog\AndroidStudioProjects\PointMax\app\build\tmp\kapt3\stubs\debug\com\example\android\pointmax\database\CreditCards.java:12: error: There is a problem with the query: [SQLITE_ERROR] SQL error or missing database (no such table: Category) private final java.util.List categories = null; ^C:\Users\jjrog\AndroidStudioProjects\PointMax\app\build\tmp\kapt3\stubs\debug\com\example\android\pointmax\database\CardDao.java:22: error: com.example.android.pointmax.database.CardDao is part of com.example.android.pointmax.database.CardRoomDatabase but this entity is not in the database. Maybe you forgot to add com.example.android.pointmax.database.Category to the entities section of the @Database? public abstract java.lang.Object insertCategory(@org.jetbrains.annotations.NotNull() [WARN] Incremental annotation processing requested, but support is disabled because the following processors are not incremental: androidx.room.RoomProcessor (DYNAMIC).
Task :app:kaptDebugKotlin FAILED ^ FAILURE: Build failed with an exception.
- What went wrong:
Execution failed for task ':app:kaptDebugKotlin'.
A failure occurred while executing org.jetbrains.kotlin.gradle.internal.KaptExecution java.lang.reflect.InvocationTargetException (no error message)
Stacktrace:
Caused by: org.gradle.workers.internal.DefaultWorkerExecutor$WorkExecutionException: A failure occurred while executing org.jetbrains.kotlin.gradle.internal.KaptExecution
Caused by: java.lang.reflect.InvocationTargetException
Caused by: org.jetbrains.kotlin.kapt3.base.util.KaptBaseError: Error while annotation processing
./gradlew clean build --stacktraceand post the relevant error here. - Nicolas