I am trying to use WorkManager to populate the Room Database while creating the database. I am using dagger to initialize the database and its Dao's.
While seeding the database, it gives the following error.
Could not instantiate *.*.*.SeedDatabaseWorker
java.lang.NoSuchMethodException: <init> [class android.content.Context, class androidx.work.WorkerParameters]
at java.lang.Class.getConstructor0(Class.java:2204)
at java.lang.Class.getDeclaredConstructor(Class.java:2050)
at androidx.work.WorkerFactory.createWorkerWithDefaultFallback(WorkerFactory.java:91)
at androidx.work.impl.WorkerWrapper.runWorker(WorkerWrapper.java:233)
at androidx.work.impl.WorkerWrapper.run(WorkerWrapper.java:127)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
at java.lang.Thread.run(Thread.java:761)
2019-03-16 16:22:29.135 18035-18051/*.* E/WM-WorkerWrapper: Could not create Worker *.*.*.SeedDatabaseWorker
Here's how i have setup the application.
AppModule.Kt
#Provides
#Singleton
fun provideAppDatabase(context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"garden.db"
)
.addCallback(object : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
val request = OneTimeWorkRequestBuilder<SeedDatabaseWorker>().build()
WorkManager.getInstance().enqueue(request)
}
})
.build()
}
#Provides
#Singleton
fun providePlantDao(db: AppDatabase): PlantDao =
db.plantDao()
PlantDao
#Dao
interface PlantDao {
#Query("SELECT * FROM plants ORDER BY name")
fun getPlants(): LiveData<List<Plant>>
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(plants: List<Plant>)
}
AppDatabase.kt
#Database(
entities = [
Plant::class
],
version = 1,
exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
abstract fun plantDao(): PlantDao
}
SeedDatabaseWorker.kt
class SeedDatabaseWorker #Inject constructor(
context: Context,
workerParameters: WorkerParameters
): CoroutineWorker(context, workerParameters) {
private val TAG by lazy { SeedDatabaseWorker::class.java.simpleName }
#Inject
lateinit var database: AppDatabase
override val coroutineContext = Dispatchers.IO
override suspend fun doWork(): Result = coroutineScope {
try {
applicationContext.assets.open("plants.json").use { inputStream ->
JsonReader(inputStream.reader()).use { jsonReader ->
val plantType = object : TypeToken<List<Plant>>() {}.type
val plantList: List<Plant> = Gson().fromJson(jsonReader, plantType)
database.plantDao().insertAll(plantList)
Result.success()
}
}
} catch (ex: Exception) {
Log.e(TAG, "Error seeding database", ex)
Result.failure()
}
}
}
Can anyone help me to populate database using WorkManager?
Note: I am trying this google sample
For now, Dagger 2 does not officially support injection with androidx.worker.Worker, this issue is still open, check here. One solution is proposed by this blog. Basically you could inject any object in the constructor like:
class HelloWorldWorker #AssistedInject constructor(
#Assisted private val appContext: Context,
#Assisted private val params: WorkerParameters,
private val foo: Foo
) : Worker(appContext, params) {
private val TAG = "HelloWorldWorker"
override fun doWork(): Result {
Log.d(TAG, "Hello world!")
Log.d(TAG, "Injected foo: $foo")
return Result.success()
}
#AssistedInject.Factory
interface Factory : ChildWorkerFactory
}
Here the object Foo is injected.
I have same problem in my project, inspired by the blog, I solve the problem.
You could check the my solution to see how to pre-populate data with WorkManager.
Related
I have viewmodel:
#HiltViewModel
class SplashViewMode #Inject constructor(
private val repository: DataStoreRepository,
private val workManager: PeriodicNotificationWorkManager
) : ViewModel() {
init {
workManager.startPeriodicNotifications()
}
}
and a class where I start my periodic work
class PeriodicNotificationWorkManager #Inject constructor(
private val context: Context,
private val workManager: WorkManager
) {
private val WORK_TAG = "my_work"
private val repeatInterval = 15L
private val repeatIntervalTimeUnit: TimeUnit = TimeUnit.MINUTES
fun startPeriodicNotifications() {
val workRequest = PeriodicWorkRequestBuilder<ShowNotificationWorker>(
repeatInterval,
repeatIntervalTimeUnit
)
.addTag(WORK_TAG)
.build()
workManager.enqueueUniquePeriodicWork(
WORK_TAG,
ExistingPeriodicWorkPolicy.KEEP,
workRequest
)
}
}
and finally my worker:
#HiltWorker
class ShowNotificationWorker #AssistedInject constructor(
#Assisted val context: Context,
#Assisted val workerParams: WorkerParameters,
//private val evenDao: EventDao
) :
CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
NotificationDisplayer(context).showNotification("Test")
return Result.success()
}
}
so far It works fine.
But I need access to EventDao so if I uncomment "private val evenDao: EventDao" in last file I get:
2022-12-31 12:22:03.314 6789-6936/com.rachapps.botanica E/WM-WorkerFactory: Could not instantiate com.rachapps.notification_feature.ShowNotificationWorker
java.lang.NoSuchMethodException: com.rachapps.notification_feature.ShowNotificationWorker.<init> [class android.content.Context, class androidx.work.WorkerParameters]
Solution provided by BrianMwas worked for me finally
https://github.com/google/dagger/issues/2601
#Module
#InstallIn(SingletonComponent::class)
object WorkManagerInitializer: Initializer<WorkManager> {
#Provides
#Singleton
override fun create(#ApplicationContext context: Context): WorkManager {
val entryPoint = EntryPointAccessors.fromApplication(
context,
WorkManagerInitializerEntryPoint::class.java
)
val configuration = Configuration
.Builder()
.setWorkerFactory(entryPoint.hiltWorkerFactory())
.setMinimumLoggingLevel(if (BuildConfig.DEBUG) Log.DEBUG else Log.INFO)
.build()
WorkManager.initialize(context, configuration)
return WorkManager.getInstance(context)
}
override fun dependencies(): MutableList<Class<out Initializer<*>>> {
return mutableListOf()
}
#InstallIn(SingletonComponent::class)
#EntryPoint
interface WorkManagerInitializerEntryPoint {
fun hiltWorkerFactory(): HiltWorkerFactory
}
}
I get an error (MainViewModel has no zero argument constructor).
It seems to me that mistake is in misusing Hilt, but I can't find. There are similar questions on SA, but they don't fit my case.
I can't find where did I go wrong and will be gratifeul for any help.
Error:
java.lang.RuntimeException: Cannot create an instance of class mypackage.main.MainViewModel
/* bla bla bla */
Caused by: java.lang.InstantiationException: java.lang.Class<mypackage.main.MainViewModel> has no zero argument constructor
at java.lang.Class.newInstance(Native Method)
at androidx.lifecycle.ViewModelProvider$NewInstanceFactory.create(ViewModelProvider.java:219)
... 39 more
ViewModel begins like that:
#HiltViewModel
class MainViewModel #Inject constructor(
private val repo: MainRepository,
private val dispatchers: DispatcherProvider
) : ViewModel() {
// body
}
In MainActivity:
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: MainViewModel by viewModels()
// etc
AppModule:
#Module
#InstallIn(SingletonComponent::class)
object AppModule {
#Singleton
#Provides
fun provideCurrencyApi() : CurrencyApi = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(CurrencyApi::class.java)
#Singleton
#Provides
fun provideMainRepository(api: CurrencyApi): MainRepository = DefaultMainRepository(api)
#Singleton
#Provides
fun provideDispatchers(): // blablabla
}
}
MainRepository:
interface MainRepository {
suspend fun getRates(base: String) : Resource<CurrencyResponse>
}
DefaultMainRepository
class DefaultMainRepository #Inject constructor(
private val api: CurrencyApi
) : MainRepository {
override suspend fun getRates(base: String): Resource<CurrencyResponse> {
return try {
val response = api.getRates(base)
val result = response.body()
if (response.isSuccessful && result != null) {
Resource.Success(result)
} else {
Resource.Error(response.message())
}
} catch (e: Exception) {
Resource.Error(e.message ?: "An error occurred")
}
}
}
I solved this problem by changing Dagger Hilt dependencies versions to earlier. I think there was mismatch in those versions. The rest of the code turned out to be correct, it seems..
I tried to create a registration using MVVM + Repository pattern with DI, and I used #ViewModelInject and everything was OK, but now #ViewModelInject is deprecated and I changed #ViewModelInject to #HiltViewModel + #Inject constructor() and faced with the error: [Dagger/MissingBinding] *.AuthRepository cannot be provided without an #Provides-annotated method. I tried to add a #Provides annotation for the register function in the interface but faced with another error
Execution failed for task ':app:kaptDebugKotlin'.
A failure occurred while executing org.jetbrains.kotlin.gradle.internal.KaptExecution
java.lang.reflect.InvocationTargetException (no error message)
AuthViewModel
#HiltViewModel
class AuthViewModel #Inject constructor(
private val repository: AuthRepository,
private val applicationContext: Context,
private val dispatcher: CoroutineDispatcher = Dispatchers.Main
) : ViewModel() {
private val _registerStatus = MutableLiveData<Event<Resource<AuthResult>>>()
val registerStatus: LiveData<Event<Resource<AuthResult>>> = _registerStatus
private val _loginStatus = MutableLiveData<Event<Resource<AuthResult>>>()
val loginStatus: LiveData<Event<Resource<AuthResult>>> = _loginStatus
fun login(email: String, password: String) {
if(email.isEmpty() || password.isEmpty()) {
val error = applicationContext.getString(R.string.error_input_empty)
_loginStatus.postValue(Event(Resource.Error(error)))
} else {
_loginStatus.postValue(Event(Resource.Loading()))
viewModelScope.launch(dispatcher) {
val result = repository.login(email, password)
_loginStatus.postValue(Event(result))
}
}
}
fun register(email: String, username: String, password: String, repeatedPassword: String) {
val error = if(email.isEmpty() || username.isEmpty() || password.isEmpty()) {
applicationContext.getString(R.string.error_input_empty)
} else if(password != repeatedPassword) {
applicationContext.getString(R.string.error_incorrectly_repeated_password)
} else if(username.length < MIN_USERNAME_LENGTH) {
applicationContext.getString(R.string.error_username_too_short, MIN_USERNAME_LENGTH)
} else if(username.length > MAX_USERNAME_LENGTH) {
applicationContext.getString(R.string.error_username_too_long, MAX_USERNAME_LENGTH)
} else if(password.length < MIN_PASSWORD_LENGTH) {
applicationContext.getString(R.string.error_password_too_short, MIN_PASSWORD_LENGTH)
} else if(!Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
applicationContext.getString(R.string.error_not_a_valid_email)
} else null
error?.let {
_registerStatus.postValue(Event(Resource.Error(it)))
return
}
_registerStatus.postValue(Event(Resource.Loading()))
viewModelScope.launch(dispatcher) {
val result = repository.register(email, username, password)
_registerStatus.postValue(Event(result))
}
}
}
AppModule
#Module
#InstallIn(SingletonComponent::class)
object AppModule {
#Singleton
#Provides
fun provideMainDispatcher() = Dispatchers.Main as CoroutineDispatcher
#Singleton
#Provides
fun provideApplicationContext(#ApplicationContext context: Context) = context
#Singleton
#Provides
fun provideGlideInstance(#ApplicationContext context: Context) =
Glide.with(context).setDefaultRequestOptions(
RequestOptions()
.placeholder(R.drawable.ic_image)
.error(R.drawable.ic_error)
.diskCacheStrategy(DiskCacheStrategy.DATA)
)
}
AuthModule
#Module
#InstallIn(ActivityComponent::class)
object AuthModule {
#ActivityScoped
#Provides
fun provideAuthRepository() = DefaultAuthRepository() as AuthRepository
}
AuthRepository
interface AuthRepository {
suspend fun register(email: String, username: String, password: String): Resource<AuthResult>
suspend fun login(email: String, password: String): Resource<AuthResult>
}
DefaultAuthRepository
class DefaultAuthRepository : AuthRepository {
val auth = FirebaseAuth.getInstance()
val users = FirebaseFirestore.getInstance().collection("users")
override suspend fun register(
email: String,
username: String,
password: String
): Resource<AuthResult> {
return withContext(Dispatchers.IO) {
safeCall {
val result = auth.createUserWithEmailAndPassword(email, password).await()
val uid = result.user?.uid!!
val user = User(uid, username)
users.document(uid).set(user).await()
Resource.Success(result)
}
}
}
override suspend fun login(email: String, password: String): Resource<AuthResult> {
TODO("Not yet implemented")
}
}
//Dagger - Hilt
implementation 'com.google.dagger:hilt-android:2.31.2-alpha'
kapt 'com.google.dagger:hilt-android-compiler:2.31.2-alpha'
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03'
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha03'
enter image description here
#Module
#InstallIn(ActivityComponent::class)
abstract class AuthModule{
#Binds
abstract fun bindAuthRepository(impl: DefaultAuthRepository): AuthRepository
}
With new hilt version lots of stuff has been changed.
You also have to upgrade your hilt android, hilt compiler and hilt gradle plugin to:2.31-alpha
I made mock sample exactly the way you did i had same issue, after going through hilt's docs i found new way to inject dependencies to viewModels, you have to make separate module for dependencies which are going to inject in the viewModel with special component called ViewModelComponent:
#Module
#InstallIn(ViewModelComponent::class) // this is new
object RepositoryModule{
#Provides
#ViewModelScoped // this is new
fun providesRepo(): ReposiotryIMPL { // this is just fake repository
return ReposiotryIMPL()
}
}
here is what docs says about ViewModelComponent and ViewModelScoped
All Hilt View Models are provided by the ViewModelComponent which follows the same lifecycle as a ViewModel, i.e. it survives configuration changes. To scope a dependency to a ViewModel use the #ViewModelScoped annotation.
A #ViewModelScoped type will make it so that a single instance of the scoped type is provided across all dependencies injected into the Hilt View Model.
link: https://dagger.dev/hilt/view-model.html
then your viewModel:
#HiltViewModel
class RepoViewModel #Inject constructor(
application: Application,
private val reposiotryIMPL: ReposiotryIMPL
) : AndroidViewModel(application) {}
I am trying to implement Realm Version management with dagger2, with this link Adavis Realm Migration,
but getting following error,
[Dagger/MissingBinding] java.util.Map> cannot be provided without an #Provides-annotated method.
public abstract interface CoreComponent {
^
The codes are as follows,
interface VersionMigration {
fun migrate(realm: DynamicRealm, oldVersion: Long)
}
class RealmVersion1Migration #Inject constructor(): VersionMigration {
override fun migrate(realm: DynamicRealm, oldVersion: Long) {
val schema = realm.schema
if (0 == oldVersion.toInt()) {
schema.create("RealmAlbumEntity")
.addField("album_id", String::class.java, FieldAttribute.PRIMARY_KEY)
Timber.e("migration V1 complete")
}
}
}
#Reusable
class ERealmMigration #Inject constructor(
private val versionMigrations:
Map<Int, Provider<VersionMigration>>
) : RealmMigration {
init {
Timber.e("IN Realm Migration")
}
override fun migrate(
realm: DynamicRealm,
oldVersion: Long,
newVersion: Long
) {
for (i in oldVersion.toInt() until newVersion) {
val provider: Provider<VersionMigration> = versionMigrations.getValue(i.toInt())
val versionMigration = provider.get()
versionMigration.migrate(realm, i)
}
}
}
This is my included module in realmConfig,
private const val REALM_SCHEMA = 1L
#Module(includes = [MigrationsModule::class])
class RealmConfigModule {
#Provides
#AppScope
fun provideRealm(realmConfiguration: RealmConfiguration): Realm {
return Realm.getInstance(realmConfiguration)
}
#Provides
#AppScope
fun provideRealmConfiguration(
#Named(CONTEXT_APPLICATION) application: Application,
realmMigration: ERealmMigration
): RealmConfiguration {
val config = RealmConfiguration.Builder().name(application.getString(R.string.real_db_name))
.schemaVersion(REALM_SCHEMA)
.migration(realmMigration)
.modules(UserRealmModule(), EventRealmModule())
.build()
Realm.setDefaultConfiguration(config)
return config
}
}
And this is my AppComponent,
#Component(modules = [RealmConfigModule::class])
#AppScope
interface CoreComponent {
#Component.Factory
interface Factory {
fun create(#BindsInstance application: Application): CoreComponent
}
}
I had tried to get through following links, but not able to gather any usefull contents out of it.
Link 1
Link 2
Link 3
It's a simple mistake, i just have to add "#JvmSuppressWildcards",
#Reusable
class ERealmMigration #Inject constructor(
private val versionMigrations:
Map<Int, #JvmSuppressWildcards Provider<VersionMigration>>
) : RealmMigration
, thanks to this answer, from this user,
kotlin + Dagger2 : cannot be provided without an #Provides-annotated method
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()
}
}