4

I' trying to utilize Dagger 2 in a ViewModel + Respository + Room + Retrofit + Coroutines project written in Kotlin.

Currently each ViewModel initializes required repositories and their dependences by itself like so

class HomeViewModel(
    application: Application
) : AndroidViewModel(application) {
    private val repository: UserRepository =
        UserRepository(
            Webservice.create(),
            AppDatabase.getDatabase(application, viewModelScope).userDao()
        )

I would like to get this simplified to this

class HomeViewModel @Inject constructor(
    private val repository: UserRepository
): ViewModel()

What I have achieved so far

Created the dagger component and modules

@Singleton
@Component(modules = [
    AppModule::class,
    NetworkModule::class,
    DataModule::class,
    RepositoryModule::class
])
interface AppComponent {
    val webservice: Webservice
    val userRepository: UserRepository
}

@Module
class AppModule(private val app: Application) {
    @Provides
    @Singleton
    fun provideApplication(): Application = app
}

@Module
class DataModule {
    @Provides
    @Singleton
    fun provideApplicationDatabase(app: Application, scope: CoroutineScope) =
        AppDatabase.getDatabase(app, scope)

    @Provides
    @Singleton
    fun provideUserDao(db: AppDatabase) = db.userDao()
}

@Module
class NetworkModule {
    @Provides
    @Singleton
    fun provideWebservice() = Webservice.create()
}

@Module
class RepositoryModule {
    @Provides
    @Singleton
    fun provideUserRepository(webservice: Webservice, userDao: UserDao) =
        UserRepository(webservice, userDao)
}

Got the AppComponent initilized in the application class

class App : Application() {
    companion object {
        lateinit var appComponent: AppComponent
    }
    override fun onCreate() {
        super.onCreate()
        appComponent = initDagger(this)
    }
    private fun initDagger(app: App): AppComponent =
        DaggerAppComponent.builder()
            .appModule(AppModule(app))
            .build()
}

And now I'm stuck.

The first question is: How do I make the ViewModel's inject constructor work?

Originally it was initialized from the HomeFragment like so

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    viewModel = ViewModelProviders.of(this).get(HomeViewModel::class.java)

How do I call the initializer now?

The second question is a bit harder.

The database constructor requies a Coroutine scope in order to prepopulate it in a background thread during creation. How do I pass in a scope now?

Here is the definition of the database and the callback

@Database(
    entities = [User::class],
    version = 1, exportSchema = false
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao

    companion object {
        @Volatile
        private var INSTANCE: AppDatabase? = null

        fun getDatabase(context: Context, scope: CoroutineScope): AppDatabase {
            val tempInstance =
                INSTANCE
            if (tempInstance != null) {
                return tempInstance
            }
            synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "database"
                )
                    .fallbackToDestructiveMigration()
                    .addCallback(AppDatabaseCallback(scope))
                    .build()
                INSTANCE = instance
                return instance
            }
        }
    }

    private class AppDatabaseCallback(
        private val scope: CoroutineScope
    ) : RoomDatabase.Callback() {RoomDatabase.Callback() {
        override fun onCreate(db: SupportSQLiteDatabase) {
            super.onCreate(db)
            INSTANCE?.let { database ->
                scope.launch(Dispatchers.IO) {
                    //inserts
                }
            }
        }
    }
}
yaugenka
  • 2,602
  • 2
  • 22
  • 41

2 Answers2

2

The second question is a bit harder.

The database constructor requies a Coroutine scope in order to prepopulate it in a background thread during creation. How do I pass in a scope now?

It's actually easier, don't pass in a CoroutineScope, use the GlobalScope for this operation.

The first question is: How do I make the ViewModel's inject constructor work?

You need to obtain the Provider<HomeViewModel> from Dagger, then invoke it inside a ViewModelProvider.Factory to create the instance of HomeViewModel via the provider registered in the Dagger component.

Alternately, if the Activity has its own subcomponent, then you can use @BindsInstance to get the Activity instance into the graph, then move ViewModelProviders.of(activity).get(HomeViewModel::class.java, object: ViewModelProvider.Factory { ... return homeViewModelProvider.get() as T ... }) into a module of that subcomponent. Then, from that subcomponent, it would be possible to obtain an actual instance of the HomeViewModel, and still obtain proper scoping + onCleared() callback.

EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
  • Thank for the answer. Is it really good practice to use `GlobalScope` in production builds particularly for this case? Can you please provide and example code of how to `obtain the Provider from Dagger, then invoke it inside a ViewModelProvider.Factory`? The activity which holds the HomeFragment does not have its own subcomponents. – yaugenka Jan 20 '20 at 16:53
  • You obtain the Provider just like you would obtain a ViewModel instance from Dagger, except you specify it as `Provider` instead of `ViewModel`. If GlobalScope was a liability in all cases, it would not exist. – EpicPandaForce Jan 20 '20 at 20:32
  • Is it the same approach as this answer? https://stackoverflow.com/a/48837581/5360898 – yaugenka Jan 20 '20 at 21:55
  • Just wanted to mention that GlobalScope is for sure permitted here, but there are better ways to go about using a GlobalScope by including an error handler, otherwise you will need to supply try catches to all of your GlobalScope coroutine blocks if they can error out. https://proandroiddev.com/coroutines-snags-6bf6fb53a3d1 Reffering to bottom "Logging Exceptions" – Aaron Smentkowski May 29 '20 at 19:58
0

You don't need to pass a coroutine scope just run a coroutine in IO dispacher like:

@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,
                "daily_accountant")
            // prepopulate the database after onCreate was called
            .addCallback(object : Callback() {
                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) {
                        // Pre-populate database operations
                    }
                }
            })
            .build()
    }
}

And remove coroutineScope from from function parameter.

Md. Yamin Mollah
  • 1,609
  • 13
  • 26