user16422658
user16422658

Reputation:

Abstract room database implementation not found dagger hilt

I am new to Android dev and need to build my first project for university. The university was using the old XML version with Java, but I wanted to learn Compose so I learnt Kotlin.

Now after getting everything setup, I am trying to use hiltViewModel() method to inject the view model inside the composable function and I am getting an error. I watched this tutorial: https://www.youtube.com/watch?v=A7CGcFjQQtQ&t=10s and a few other stack overflow questions which suggest doing the same thing but I am not sure what is going on.

After getting this to work. Now it says a database class implementation is not found, but I expect Dagger Hilt to produce this? for Room database

Here is the basic code:

Dependencies:

build.gradle:

buildscript {
    ext {
        compose_version = '1.0.0'
    }
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:7.0.0"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.10"

        //->Adding the Dagger Hilt class path here as suggested:
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.38.1'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

app/build.gradle:

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    //->Adding Kotlin annotation processing plugin and the dagger hilt plugin:
    id 'kotlin-kapt'
    id 'dagger.hilt.android.plugin'
}

android {
    compileSdk 31

    defaultConfig {
        applicationId "ac.gre.mxpenseresit"
        minSdk 21
        targetSdk 31
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        vectorDrawables {
            useSupportLibrary true
        }
    }

    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'
        useIR = true
    }
    buildFeatures {
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion compose_version
        kotlinCompilerVersion '1.5.10'
    }
    packagingOptions {
        resources {
            excludes += '/META-INF/{AL2.0,LGPL2.1}'
        }
    }
}

dependencies {

    implementation 'androidx.core:core-ktx:1.8.0'
    implementation 'androidx.appcompat:appcompat:1.4.2'
    implementation 'com.google.android.material:material:1.6.1'
    implementation "androidx.compose.ui:ui:$compose_version"
    implementation "androidx.compose.material:material:$compose_version"
    implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
    implementation 'androidx.activity:activity-compose:1.4.0'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
    debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"

    //This version for navigation is hard coded, probably will need to update later, but should be fine for assignment
    implementation("androidx.navigation:navigation-compose:2.4.2")

    //Room Dependencies:
    def room_version = "2.4.2"
    implementation "androidx.room:room-runtime:$room_version"
    annotationProcessor "androidx.room:room-compiler:$room_version"

    //Coroutine dependency:
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'

    //->Dagger Hilt Dependency for DI copied from official docs:
    implementation "com.google.dagger:hilt-android:2.38.1"
    kapt "com.google.dagger:hilt-compiler:2.38.1"

}

import android.app.Application import dagger.hilt.android.HiltAndroidApp

/**
 * Adding the Hilt Android App dependency for the Application class
 */
@HiltAndroidApp
class MExpenseApp : Application() {

}

Main & only activity:

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MXPenseResitTheme {
                // A surface container using the 'background' color from the theme
                Surface(color = MaterialTheme.colors.background) {
                   App(modifier = Modifier)
                }
            }
        }
    }
}

Main Module:

**
 * Setup dependencies:
 */
@Module
@InstallIn(ActivityComponent::class)
object MainModule {

    @Provides
    @Singleton
    fun provideDb(application: Application): MExpenseDb {
        return Room
            .databaseBuilder(
                application,
                MExpenseDb::class.java,
                "MExpenseDb"
            ).build()
    }

    /**
     * We are type hinting to the Interface so we can change implementations
     */
    @Provides
    @Singleton
    fun provideTripRepository(mExpenseDb: MExpenseDb): TripRepositoryContract {
        return TripRepository(mExpenseDb.tripDao)
    }

}

View Model:

@HiltViewModel
class TripListVm @Inject constructor(
    private val tripRepository: TripRepository
) : ViewModel() {

    /**
     * @var trips
     * Get all trips
     */
    val trips = tripRepository.getTrips()
    val testStr: String = "Test String!"
    
}

Composable:

@Composable
fun TripList(
    navController: NavHostController,
    modifier: Modifier = Modifier,
    tripListVm: TripListVm = hiltViewModel()
) {
    
    Text(text = "Trip List")
    TripListItem(
        name = "Trip Name",
        date = "16/04/1885",
        amount = 46.66,
        modifier = modifier
    )
}

Result:

hiltViewModel() unresolved

I am also getting an error: [Dagger/MissingBinding] exception. ac.gre.mxpenseresit.data.repository.TripRepository cannot be provided without an @Inject constructor or an @Provides-annotated method

Here is the code for the data layer:

Database class:

@Database(
    entities = [
        Trip::class,
        Expense::class,
    ],
    version = 1
)
abstract class MExpenseDb : RoomDatabase() {

    abstract val tripDao: TripDao

}

Dao:

@Dao
interface TripDao {

    /**
     * Gets all trips
     */
    @Query("SELECT * FROM Trip")
    fun getTrips(): Flow<List<Trip>>

    /**
     * Gets trips based on a named search,
     * This functionality can be extended later
     */
    @Query("SELECT * FROM Trip t WHERE t.name LIKE :name")
    suspend fun getTripsBySearchName(name: String)

    /**
     * Gets trip by Id
     */
    @Query("SELECT * FROM Trip WHERE Trip.id = :id")
    suspend fun getTripById(id: Long)

    /**
     *
     */
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun saveTrip(trip: Trip): Long


    @Delete
    suspend fun deleteTrip(trip: Trip)

}

Trip Repository Contract (Interface):

interface TripRepositoryContract {

    fun getTrips(): Flow<List<Trip>>
    suspend fun getTripById(id: Long): Trip?
    suspend fun getTripsBySearchName(keyword: String): List<Trip>
    suspend fun saveTripLocal(trip: Trip)
    suspend fun saveTripRemote(trip: Trip)
    suspend fun deleteTrip(trip: Trip)

}

Trip Repository implementation:

class TripRepository (val tripDao: TripDao) : TripRepositoryContract {

    override fun getTrips(): Flow<List<Trip>> {
        return tripDao.getTrips();
    }

    override suspend fun getTripById(id: Long): Trip? {
        TODO("Not yet implemented")
    }

    override suspend fun getTripsBySearchName(keyword: String): List<Trip> {
        TODO("Not yet implemented")
    }

    override suspend fun saveTripLocal(trip: Trip) {
        TODO("Not yet implemented")
    }

    override suspend fun saveTripRemote(trip: Trip) {
        TODO("Not yet implemented")
    }

    override suspend fun deleteTrip(trip: Trip) {
        TODO("Not yet implemented")
    }
}

Now the hiltViewModel() method works correctly, but I am getting a MExpenseDb_impl class not found

I looked at this stack overflow question: Android room persistent: AppDatabase_Impl does not exist

And it says to use the kept dependency, I already have the same thing with annotationProcessing so I'm not sure if this is an issue

All the tutorials online are either too long & irrelevant or too vague and I am trying to gain a deeper understanding about how this works. Any advice would be appreciated

Upvotes: 1

Views: 1745

Answers (1)

Thales Isidoro
Thales Isidoro

Reputation: 3169

To provide the TripRepository you need to create a class with Hilt's Bind functions. Example:

@Module
@InstallIn(SingletonComponent::class)
interface RepositoryModule {    
    @Binds
    @Singleton
    fun bindTripRepository(
        repository: TripRepository // here the class
    ): TripRepositoryContract // here the interface that the class implements
}

It is also necessary to modify the TripRepository. You must add the @Inject constructor() (even if your class doesn't use any dependencies) so that Hilt can create the class.
In your case it will look like this:

class TripRepository @Inject constructor(
    val tripDao: TripDao
) : TripRepositoryContract {
    // ...
}

The last change is in your MainModule:

@Module
@InstallIn(SingletonComponent::class)
object MainModule {
    // providing the db implementation normally
    @Provides
    @Singleton
    fun provideDb(application: Application): MExpenseDb {
        return Room.databaseBuilder(
            application,
            MExpenseDb::class.java,
            "MExpenseDb"
        ).build()
    }

    // providing your dao interface to be injected into TripRepository
    @Provides
    @Singleton
    fun provideTripDao(
        mExpenseDb: MExpenseDb
    ): TripDao = mExpenseDb.tripDao
}

Note that I changed from ActivityComponent to SingletonComponent in the hilt's modules, in both cases we want them to be singleton throughout the project, not just a component created for activity (which can also be singleton).
See components life cycles here.
I also recommend upgrading the Hilt version in your project, because you are using a very old one. The newest version is 2.42.

I think this video can help you better understand some things about Hilt. And there is also this repository of a project that uses Hilt together with Room that can be useful for you to consult.

Important edit:
In the TripListVm you are using the private val tripRepository: TripRepository (your class that implements the TripRepositoryContract), it is not recommended to directly inject the implementation class, instead you should inject the interface (TripRepositoryContract) and let Hilt take care of providing the implementation of it. Because that's what we taught Hilt to do in the RepositoryModule.
So to make it ideal, the TripListVm would look like this:

@HiltViewModel
class TripListVm @Inject constructor(
    private val tripRepositoryContract: TripRepositoryContract
) : ViewModel() {
    // ...
}

Upvotes: 1

Related Questions