Dagger2 Inject class with parameter (using Room) - android

I have a problem with injecting classes with Dagger2. I am using RoomDatabase for database access.
My room setup:
Dao's
interface noteDao()
interface noteTypeDao()
interface userDao()
NoteRepository
#Singleton
class NoteRepository #Inject constructor(
private val noteDao: NoteDao,
private val noteTypeDao: NoteTypeDao,
private val userDao: UserDao
) {
}
AppDatabase
#Database(entities = [Note::class, User::class, NoteType::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun noteDao(): NoteDao
abstract fun userDao(): UserDao
abstract fun noteTypeDao(): NoteTypeDao
companion object {
#Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance
}
synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"NoteDatabase"
).build()
INSTANCE = instance
return instance
}
}
}
}
Dagger 2 setup:
AppModule
#Module
class AppModule {
#Provides
fun provideNoteRepository(app: Application): NoteRepository {
return NoteRepository(
AppDatabase.getDatabase(app).noteDao(),
AppDatabase.getDatabase(app).noteTypeDao(),
AppDatabase.getDatabase(app).userDao()
)
}
#Provides
fun provideApplication(): Application {
return Application()
}
}
AppComponent
#Component(modules = [AppModule::class])
interface AppComponent {
fun inject(app: MainActivity)
}
I am getting a NullPointerExeption int the AppDatabase in the line context.applicationContext. Any suggetion how to solve the problem?
It seems that the AppDatabase doesnt get the application instance from Dagger2.

Application is a framework class, you can not just instantiate it yourself by calling its constructor. Instead, you need to pass in your application that the framework instantiates for you into your module, and provide that:
#Module
class AppModule(val application: Application) {
...
#Provides
fun provideApplication(): Application {
return application
}
}
Now, if you were creating your AppComponent like this before, in your application's onCreate (presumably, as that's the usual way to do it):
override fun onCreate() {
injector = DaggerAppComponent.create()
}
You'd have to replace it with something like this, passing in your application instance to the module so that it can then provide it:
override fun onCreate() {
injector = DaggerAppComponent.builder()
.appModule(appModule(this))
.build()
}

Related

Hilt inject context in module without Application

My Android project is composed of two modules : the app and a library android.
In the library module i want implement Room and inject it with Hilt.
Like this :
#InstallIn(SingletonComponent::class)
#Module
object DatabaseModule {
#Provides
#Singleton
fun provideMyDatabase(#ApplicationContext appContext: Context): MyDatabase {
return Room.databaseBuilder(
appContext,
MyDatabase::class.java,
"MyDatabase"
).build()
}
#Provides
fun provideMyDbDao(myDatabase: MyDatabase): MyDbDao {
return myDatabase.myDbDao()
}
}
In my module haven't Application(#HiltAndroidApp) or Activity(#AndroidEntryPoint), so #ApplicationContext doesn't work.
In this module i have a controller :
class MyController private constructor(private val applicationContext: Context) {
companion object {
private var instance: MyController? = null
fun getInstance(applicationContext: Context): MyController {
if (instance == null) // NOT thread safe!
instance = MyController(applicationContext)
return instance!!
}
}
}
How can i provide ApplicationContext to Hilt from this controller ?

How to inject Room?

My application has DI. It includes a RoomModule
and RoomComponent.
RoomModule.kt
#Module
class RoomModule(private val app: Application) {
val localDatabase: LocalDatabase = Room
.databaseBuilder(app, LocalDatabase::class.java, "local-db")
.build()
#Provides
#Singleton
internal fun providesApplication(): Application {
return app
}
#Singleton
#Provides
internal fun providesRoomDatabase(): LocalDatabase {
return localDatabase
}
#Singleton
#Provides
internal fun providesUserDao(localDatabase: LocalDatabase): UserDao {
return localDatabase.getUserDao()
}
#Singleton
#Provides
internal fun providesUserRepo(userDao: UserDao): UserRepo {
return UserDataSource(userDao)
}
}
RoomComponent.kt
#Singleton
#Component(dependencies = [], modules = [RoomModule::class])
interface RoomComponent {
fun userDao(): UserDao
fun localDatabase(): LocalDatabase
fun userRepo(): UserRepo
}
To create a RoomDatabase I need an App
Therefore, I do the following in my first activity.
MyActivity.java
public class MyActivity extends MvpAppCompatActivity {
#Inject
Interactor interactor;
#InjectPresenter
Presenter presenter;
#ProvidePresenter
Presenter providePresenter(){
DaggerAppComponent.builder()
.build()
.inject(this);
return new Presenter(interactor);
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.myactivity);
DaggerRoomComponent.builder()
.roomModule(new RoomModule(getApplication()))
.build();
...
presenter.someMethod();
}
...
Next, this activity calls the method in Presentor. Presentor calls a method in Interactor, where I want to inject RoomDatabase.
Presentor.kt
#InjectViewState
class Presenter(val interactor:
Interactor): MvpPresenter<MyView>(){
fun someMethod(){
if (interactor.getUser() != null) {
// TODO smth...
}
}
...
Interactor.kt
#Inject
lateinit var userRepo: UserRepo //null
override fun getUser(): User? {
// Using userRepo
}
But the variable is null.
The problem is that in the class where I need the database there is no Context, which is necessary to create it.
Look at the visualization of my problem
I implemented the code described in this answer Dagger2 Inject class with parameter (using Room)
This code is control about your instance.
#Singleton
#Provides
internal fun providesRoomDatabase(): LocalDatabase {
return localDatabase
}
to create an instance and make it injectable you should provide it through providesRoomDatabase, like this:
#Singleton
#Provides
internal fun providesRoomDatabase(): LocalDatabase {
val localDatabase: LocalDatabase = Room
.databaseBuilder(app, LocalDatabase::class.java, "local-db")
.build()
return localDatabase
}

Dagger 2 get own Room instance

I want to add a callback to the room database to populate initial data.
#Provides
#Singleton
fun provideRoom(context: Context): MyRoomDatabase {
return Room.databaseBuilder(context, MyRoomDatabase::class.java, "my_database")
.fallbackToDestructiveMigration()
.addCallback(object : RoomDatabase.Callback() {
#Override
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
}
})
.build()
}
For that i need the database instance in the callback to access DAO for inserting data.
How does this work?
EDIT:
What I want to achieve:
Create initial data for the room database at the app installation
My Callback Class:
class RoomCallback(
var myRoomDatabase : MyRoomDatabase
) : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
myRoomDatabase.basicItemDao().insertList(
listOf(
BasicItem(),
BasicItem()
)
)
}
}
How i provide the RoomDatabase and the RoomCallback:
#Provides
#Singleton
fun provideRoom(context: Context, roomCallback: RoomCallback): MyRoomDatabase {
return Room.databaseBuilder(context, MyRoomDatabase::class.java, "my_database")
.fallbackToDestructiveMigration()
.addCallback(roomCallback)
.build()
}
#Provides
#Singleton
fun provideRoomCallback(myRoomDatabase: MyRoomDatabase): RoomCallback {
return RoomCallback(myRoomDatabase)
}
PROBLEM:
- The RoomCallback and RoomDatabase instance need both the other instance.
UPDATE: Using Kotlin Coroutine and Dagger2
Late in the party but for future readers, it's very easy to prepopulate your database at creation time or openning time. Make sure you have already added dependency in the gradle file for Coroutine. First create your database like:
/**
* Main database.
*/
#Database(
entities = [
Login::class],
version = 1,
exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
abstract fun loginDao(): LoginDao
companion object {
#Volatile private var INSTANCE: AppDatabase? = null
fun getInstance(app: Application): AppDatabase = INSTANCE ?: synchronized(this) {
INSTANCE ?: buildDatabase(app).also { INSTANCE = it }
}
private fun buildDatabase(app: Application) =
Room.databaseBuilder(app,
AppDatabase::class.java,
"your database name")
.addCallback(object : Callback() {
// Pre-populate the database after onCreate has been called. If you want to prepopulate at opening time then override onOpen function instead of onCreate.
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
// Do database operations through coroutine or any background thread
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught during database creation --> $exception")
}
CoroutineScope(Dispatchers.IO).launch(handler) {
prePopulateAppDatabase(getInstance(app).loginDao())
}
}
})
.build()
suspend fun prePopulateAppDatabase(loginDao: LoginDao) {
val admin = Login(0, "Administrator", "1234", 1, isActive = true, isAdmin = true, isLogged = false)
loginDao.insertLoginData(admin)
}
}
}
Then you can provide singleton instance by placing below code to your dagger AppModule or in a separate database module as you wish.
#Singleton
#Provides
fun provideDb(app: Application): AppDatabase {
return AppDatabase.getInstance(app)
}
#Singleton
#Provides
fun provideLoginDao(db: AppDatabase): LoginDao {
return db.loginDao()
}
that's it, you are done. Inject anywhere your singleton database object like:
#Inject lateinit var loginDao: LoginDao
then use it.
Setup a database first
#Database(
entities = [User::class],
version = VERSION_CODE
)
abstract class DatabaseManager : RoomDatabase() {
abstract fun userDao(): UserDao
}
Now create a DatabaseModule
#Module
class DatabaseModule {
#Singleton
#Provides
fun provideRoomDatabase(#ApplicationContext context: Context): RoomDatabase {
return Room.databaseBuilder(context, RoomDatabase::class.java, "dbName")
.setJournalMode(JournalMode.TRUNCATE)
.build()
}
}
You can create a separate module or add a method in DatabaseModule it self providing dao object. Say for example I have a UserDao then
#Module
class UserModule {
#Singleton
#Provides
fun provideDao(database: DatabaseManager): UserDao {
return database.userDao()
}
}

Dagger 2 Error with Kotlin and Room

I been refactoring an app to Kotlin and currently I have been facing a weird error from Dagger. Im trying to implement a MVVM design but im hard stuck with the dagger error.
AppModule
#Module
class AppModule(val app: App) {
companion object {
private var INSTANCE: RecorderisDB? = null
private fun getInstance(context: Context): RecorderisDB?{
if (INSTANCE == null) {
synchronized(RecorderisDB::class){
INSTANCE = Room.databaseBuilder(context.applicationContext,
RecorderisDB::class.java,
"recorderis.db")
.build()
}
}
return INSTANCE
}
fun destroyInstance(){
INSTANCE = null
}
}
#Provides #Singleton
fun provideApp() = app
#Provides #Singleton #Nullable
fun getDB(context: Context): RecorderisDB? = getInstance(context)
#Provides #Singleton
fun provideDateVM(db: RecorderisDB): DateViewModel {
return DateViewModel(db)
}
AppComponent
#Singleton
#Component(modules = [(AppModule::class)])
interface AppComponent {
fun inject(app: App)
fun inject(form: Form)
}
DateViewModel
class DateViewModel #Inject constructor(val dB: RecorderisDB){
fun createDate(name: String, symbol: String, date: String): Completable {
return Completable.fromAction{ dB.getDateDao().newDate(Date(name, symbol, date))}
}
Form.kt
class Form : AppCompatActivity() {
#Inject
lateinit var dateVM: DateViewModel
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_form)
App.graph.inject(this)
initDialog()
setUpRecyclerView()
}
Stacktrace Log
English is not my first language but this error i think is being contradictory? Is telling me that my DB is not nullable BUT is being provided? Basically what i Have i my companion object inside the AppModule.
15:27:21.882 [ERROR] [org.gradle.api.Task] e: C:\Users\diego\Apps\Recorderis\app\build\tmp\kapt3\stubs\debug\tech\destinum\recorderis\DI\AppComponent.java:13:
error: [Dagger/Nullable] tech.destinum.recorderis.Data.RecorderisDB is not nullable,
but is being provided by #org.jetbrains.annotations.Nullable #Singleton
#Provides tech.destinum.recorderis.Data.RecorderisDB
tech.destinum.recorderis.DI.AppModule.getDB(android.content.Context)
public abstract void inject(#org.jetbrains.annotations.NotNull()
^
tech.destinum.recorderis.Data.RecorderisDB is injected at
tech.destinum.recorderis.DI.AppModule.provideDateVM(db)
tech.destinum.recorderis.Data.ViewModels.DateViewModel is injected at
tech.destinum.recorderis.activities.Form.dateVM
tech.destinum.recorderis.activities.Form is injected at
tech.destinum.recorderis.DI.AppComponent.inject(tech.destinum.recorderis.activities.Form)
Well it specifically says that the problem is that you are injecting RecorderisDB, even though you are providing RecorderisDB?.
The solution? Dagger already handles the double-checked locking for you just by using #Singleton #Provides. There is no need for that code at all.
#Module
class AppModule(val app: App) {
#Provides
fun provideApp() = app
#Provides #Singleton
fun getDB(context: App): RecorderisDB = Room.databaseBuilder(context.applicationContext,
RecorderisDB::class.java,
"recorderis.db")
.build()
#Provides
// #Singleton // are you CERTAIN this is singleton?
fun provideDateVM(db: RecorderisDB): DateViewModel {
return DateViewModel(db)
}
}

Using room as singleton in kotlin

I'm trying to use Room as singleton so I didn't have to invoke Room.databaseBuilder() -which is expensive- more than once.
#Database(entities = arrayOf(
Price::class,
StationOrder::class,
TicketPrice::class,
Train::class,
TrainCategory::class
), version = 2)
#TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun dao(): TrainDao
companion object {
fun createDatabase(context: Context): AppDatabase
= Room.databaseBuilder(context, AppDatabase::class.java, "trains.db").build()
}
}
Note:
Can't use Object because Room requires using abstract class.
singleton must be thread safe because multiple threads might access it at the same time.
must be able to take Context as an argument.
I have looked at all similar StackOverflow questions and none of them satisfy my requirements
Singleton with argument in Kotlin isn't thread-safe
Kotlin - Best way to convert Singleton DatabaseController in Android isn't thread-safe
Kotlin thread save native lazy singleton with parameter uses object
After some research, I found that I have two options.
Double-checked locking
Initialization-on-demand holder idiom
I considered implementing one of them, but this didn't felt right for Kotlin - too much boilerplate code.
After more research, I stumbled upon this great article which provides an excellent solution, which uses Double-checked locking but in an elegant way.
companion object : SingletonHolder<AppDatabase, Context>({
Room.databaseBuilder(it.applicationContext, AppDatabase::class.java, "train.db").build()
})
From the article:
A reusable Kotlin implementation:
We can encapsulate the logic to
lazily create and initialize a singleton with argument inside a
SingletonHolder class. In order to make that logic thread-safe, we
need to implement a synchronized algorithm and the most efficient
one — which is also the hardest to get right — is the double-checked
locking algorithm.
open class SingletonHolder<T, A>(creator: (A) -> T) {
private var creator: ((A) -> T)? = creator
#Volatile private var instance: T? = null
fun getInstance(arg: A): T {
val i = instance
if (i != null) {
return i
}
return synchronized(this) {
val i2 = instance
if (i2 != null) {
i2
} else {
val created = creator!!(arg)
instance = created
creator = null
created
}
}
}
}
Extra:
if you want Singleton with two arguments
open class SingletonHolder2<out T, in A, in B>(creator: (A, B) -> T) {
private var creator: ((A, B) -> T)? = creator
#Volatile private var instance: T? = null
fun getInstance(arg0: A, arg1: B): T {
val i = instance
if (i != null) return i
return synchronized(this) {
val i2 = instance
if (i2 != null) {
i2
} else {
val created = creator!!(arg0, arg1)
instance = created
creator = null
created
}
}
}
}
In this particular case I would resort to using Dagger 2, or some other dependency injection library like Koin or Toothpick. All three libraries allow to provide dependancies as singletons.
Here's the code for Dagger 2 module:
#Module
class AppModule constructor(private val context: Context) {
#Provides
#Singleton
fun providesDatabase(): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"train.db")
.build()
}
}
AppComponent:
#Singleton
#Component(modules = arrayOf(
AppModule::class
))
interface AppComponent {
fun inject(viewModel: YourViewModel)
fun inject(repository: YourRepository)
}
Application class to provide injection:
class App : Application() {
companion object {
private lateinit var appComponent: AppComponent
val component: AppComponent get() = appComponent
}
override fun onCreate() {
super.onCreate()
initializeDagger()
}
private fun initializeDagger() {
component = DaggerAppComponent.builder()
.appModule(AppModule(this))
.build()
}
}
And then inject your database as singleton to wherever you need it (for example in your app's repository):
#Inject lateinit var appDatabase: AppDatabase
init {
App.component.inject(this)
}
Used #Volatile for thread safety.
public abstract class AppDatabase : RoomDatabase() {
abstract fun trainDao(): trainDao
companion object {
#Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): Db = INSTANCE ?: synchronized(this){
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase ::class.java,
"train-db"
).build()
INSTANCE = instance
instance
}
}
}
taken from : https://developer.android.com/codelabs/android-room-with-a-view-kotlin#7
You could make use of the Kotlin standard library's
fun <T> lazy(LazyThreadSafetyMode.SYNCHRONIZED, initializer: () -> T): Lazy<T>
companion object {
private lateinit var context: Context
private val database: AppDatabase by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
Room.databaseBuilder(context, AppDatabase::class.java, "trains.db").build()
}
fun getDatabase(context: Context): AppDatabase {
this.context = context.applicationContext
return database
}
}
Personally though, I would normally add ApplicationContext-dependent singletons inside the Application, e.g.
<!-- AndroidManifest.xml -->
<manifest>
<application android:name="MyApplication">
...
class MyApplication : Application() {
val database: AppDatabase by lazy {
Room.databaseBuilder(this, AppDatabase::class.java, "train.db").build()
}
}
You can even define an extension method for easy access as context.database.
val Context.database
get() =
generateSequence(applicationContext) {
(it as? ContextWrapper)?.baseContext
}.filterIsInstance<MyApplication>().first().database
Here's how i figured out...
#Database(entities = [MyEntity::class], version = dbVersion, exportSchema = true)
abstract class AppDB : RoomDatabase() {
// First create a companion object with getInstance method
companion object {
fun getInstance(context: Context): AppDB =
Room.databaseBuilder(context.applicationContext, AppDB::class.java, dbName).build()
}
abstract fun getMyEntityDao(): MyEntityDao
}
// This is the Singleton class that holds the AppDB instance
// which make the AppDB singleton indirectly
// Get the AppDB instance via AppDBProvider through out the app
object AppDBProvider {
private var AppDB: AppDB? = null
fun getInstance(context: Context): AppDB {
if (appDB == null) {
appDB = AppDB.getInstance(context)
}
return appDB!!
}
}
singleton in kotlin is real easy just do this
companion object {
#JvmStatic
val DATABASE_NAME = "DataBase"
#JvmField
val database = Room.databaseBuilder(App.context(), DataBase::class.java, DataBase.DATABASE_NAME).build()
}

Categories

Resources