0
votes

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

1
Run ./gradlew clean build --stacktrace and post the relevant error here. - Nicolas
Updated to add the stacktrace errors. - Jamaal Rogers

1 Answers

1
votes

I think, problem is in your DAO here:

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(card: CreditCards)

CreditCards - is just a data class, not Room's entity. There is no SQLite's table, that is associated with it. So you cannot use it to insert in. Let's say this class - is "read-only" for Room (it is filled with data only on-the-fly during Query) and you can use it only as a result's type in queries.

You have another 2 entities - Card and Category, each of them is associated with SQLite table. You should implement @Insert methods for them. After changes in these tables (in one or both of them) you'll get new result with query, resulting CreditCards value.