WorkManager - #HiltWorkManager - android

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
}
}

Related

Inject Worker in AndroidTest class by Hilt

I am trying to test Worker but i can't inject it
Error while gradle building:
Dagger does not support injecting #AssistedInject type,
com.hussien.quoty.sync.NotificationWorker. Did you mean to inject its assisted factory type instead?
public com.hussien.quoty.sync.NotificationWorker notificationWorker;
Test Class
#ExperimentalCoroutinesApi
#MediumTest
#RunWith(AndroidJUnit4::class)
#HiltAndroidTest
class NotificationWorkerTest {
private lateinit var notificationManager: NotificationManager
#get:Rule
var hiltRule = HiltAndroidRule(this)
//NOTE: HERE AM TRYING TO INJECT IT
#Inject
lateinit var notificationWorker: NotificationWorker
#Before
fun setUp() {
hiltRule.inject()
val context = ApplicationProvider.getApplicationContext<Context>()
notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}
#Test
fun doWork() = runTest {
notificationWorker.doWork()
assertThat(
notificationManager.activeNotifications.map { it.id },
hasItem(NotificationUtils.QUOTES_NOTIFICATION_ID)
)
}
}
Worker Class
#HiltWorker
class NotificationWorker #AssistedInject constructor(
#Assisted context: Context,
#Assisted workerParams: WorkerParameters,
private val repository: QuotesRepository,
private val settingsDataStoreManager: SettingsDataStoreManager,
private val notificationUtils: NotificationUtils,
): CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
val settings = settingsDataStoreManager.settingsPreferencesFlow.first()
val randomQuote = repository.getRandomQuote(settings.tags, settings.languages)
notificationUtils.sendQuoteNotification(randomQuote)
return Result.success()
}
companion object {
const val TAG = "notification_worker"
}
}

How to inject Worker class?

So first of all I created my worker class called UpdaterWorker.
After that I overrided getWorkManagerConfiguration inside my application class.
And I created a WorkerFactory class.
Is that all I have to do, or did I miss something?
How can I create an one time request?
When I try to inject updater worker to main activity, I get this error: Dagger does not support injecting #AssistedInject type, .workmanager.UpdaterWorker. Did you mean to inject its assisted factory type instead?
**Updater worker**
#HiltWorker
class UpdaterWorker #AssistedInject constructor(
private val api: ReservationApi,
private val updaterUrl: String,
private val prefManager: PrefManager,
#Assisted private val context: Context,
#Assisted workerParams: WorkerParameters
) : Worker(context, workerParams) {
...
}
** Worker Factory**
class WorkerFactory #Inject constructor(
private val api: ReservationApi,
#param:Named("update_url") private val updaterUrl: String,
private val prefManager: PrefManager,
) : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters
): ListenableWorker? {
return when (workerClassName) {
WorkerEnums.UpdaterWorker.name -> {
UpdaterWorker(api, updaterUrl, prefManager, appContext, workerParameters)
}
else ->
throw Resources.NotFoundException("Worker not found")
}
}
}
** My application class**
override fun getWorkManagerConfiguration(): Configuration {
return Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
}
**Main Activity**
#Inject
lateinit var updaterWorker: UpdaterWorker
A WorkerFactory is not needed and furthermore, you don't need to (and can't) inject the Worker into any other class. Here is the right approach:
Worker
#HiltWorker
class UpdaterWorker #AssistedInject constructor(
private val api: ReservationApi,
private val updaterUrl: String,
private val prefManager: PrefManager,
#Assisted private val context: Context,
#Assisted workerParams: WorkerParameters
) : Worker(context, workerParams) {
...
}
Usage of Worker (e.g here in viewmodel)
class MyTestViewModelForWorker(#ApplicationContext private val context: Context) : ViewModel() {
private val myTestWork = OneTimeWorkRequestBuilder<UpdateWorker>
.build()
fun startManager() {
WorkManager.getInstance(context).enqueue(myTestWork)
}
}
Then, it is important to override the default workerfactory with the hiltfactory in you app class
Overriding default WorkerFactory
#HiltAndroidApp
class App : Application(), Configuration.Provider {
#Inject lateinit var workerFactory: HiltWorkerFactory
override fun getWorkManagerConfiguration(): Configuration =
Configuration.Builder().setWorkerFactory(workerFactory).build()
}
And finally, you need to remove the default workmanagerinitalizator inside your manifest:
AppManifest
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
tools:node="remove" />
Next time, please read the documentation of hilt, because everything I wrote here is perfectly explained there and second of all, the approach I am showing here will change with the release of androidx.work:work-runtime-ktx:2.6.0

How to fix "error: [Dagger/MissingBinding] androidx.room.RoomDatabase.Callback cannot be provided without an #Provides-annotated method"

AppModule.kt
#Module
#InstallIn(SingletonComponent::class)
object AppModule {
#Provides
#Singleton
fun provideTaskDatabase(
app: Application,
callback: RoomDatabase.Callback
) = Room.databaseBuilder(app,TaskDatabase::class.java,"task_db")
.fallbackToDestructiveMigration()
.addCallback(callback)
.build()
#Provides
#Singleton
fun provideTaskDao(appDatabase: TaskDatabase): TaskDao {
return appDatabase.getDao()
}
#ApplicationScope
#Provides
#Singleton
fun provideApplicationScope() = CoroutineScope(SupervisorJob())
#Retention(AnnotationRetention.RUNTIME)
#Qualifier
annotation class ApplicationScope
}
TaskViewModel.kt
#HiltViewModel
class TaskViewModel #Inject constructor(
private val taskDao: TaskDao
): ViewModel() {
val searchQuery = MutableStateFlow("")
val sortOrder = MutableStateFlow(SortOrder.BY_DATE)
val hideComplited = MutableStateFlow(false)
#ExperimentalCoroutinesApi
private val taskFlow = combine(
searchQuery,
sortOrder,
hideComplited){
query,sortorder,hidecomplited ->
Triple(query,sortorder,hidecomplited)
}
.flatMapLatest {(query,sortorder,hidecomplicated) ->
taskDao.getTasks(query,sortorder,hidecomplicated)
}
#ExperimentalCoroutinesApi
val tasks = taskFlow.asLiveData()
}
enum class SortOrder{ BY_DATE,BY_NAME}
Callback.class
#Database(entities = [Task::class],version = 1)
abstract class TaskDatabase: RoomDatabase() {
abstract fun getDao(): TaskDao
class CallBack #Inject constructor(
private val database: Provider<TaskDatabase>,
#AppModule.ApplicationScope private val applicationScope: CoroutineScope
): RoomDatabase.Callback(){
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
val dao = database.get().getDao()
applicationScope.launch {
dao.insert(Task("Go to shopping"))
dao.insert(Task("Go to job"))
dao.insert(Task("playing football",important = true))
dao.insert(Task("Programming",checked = true))
dao.insert(Task("buy something"))
}
}
}
}
build
C:\Users\User\AndroidStudioProjects\ToDoList\app\build\generated\source\kapt\debug\com\example\todolist\TodoApplication_HiltComponents.java:127: error: [Dagger/MissingBinding] androidx.room.RoomDatabase.Callback cannot be provided without an #Provides-annotated method.
public abstract static class SingletonC implements TodoApplication_GeneratedInjector,
^
androidx.room.RoomDatabase.Callback is injected at
com.example.todolist.di.AppModule.provideTaskDatabase(�, callback)
com.example.todolist.room.TaskDatabase is injected at
com.example.todolist.di.AppModule.provideTaskDao(appDatabase)
com.example.todolist.room.TaskDao is injected at
com.example.todolist.ui.task.TaskViewModel(taskDao)
com.example.todolist.ui.task.TaskViewModel is injected at
com.example.todolist.ui.task.TaskViewModel_HiltModules.BindsModule.binds(vm)
#dagger.hilt.android.internal.lifecycle.HiltViewModelMap java.util.Map<java.lang.String,javax.inject.Provider<androidx.lifecycle.ViewModel>> is requested at
dagger.hilt.android.internal.lifecycle.HiltViewModelFactory.ViewModelFactoriesEntryPoint.getHiltViewModelMap() [com.example.todolist.TodoApplication_HiltComponents.SingletonC ? com.example.todolist.TodoApplication_HiltComponents.ActivityRetainedC ? com.example.todolist.TodoApplication_HiltComponents.ViewModelC]warning: The following options were not recognized by any
For you use callback: RoomDatabase.Callback as parameter in fun provideTaskDatabase you need to tell dagger how to create such object.
#Provides
fun provideCallback() = object : RoomDatabase.Callback() {
//override and implement methods
}

Application dependency to ViewModel with HILT

I was wondering how can I pass application dependency to ViewModel using Hilt?
I was trying with AndroidViewModel, but I couldn't make it. Can someone help me? Some short sample could will mean a lot to me.
This is my ViewModel:
class MainViewModel #ViewModelInject constructor(
private val application: Application,
private val repository: Repository,
#Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
This is my hilt module
#Module
#InstallIn(ApplicationComponent::class)
object DatabaseModule {
#Singleton
#Provides
fun provideDatabase(
#ApplicationContext context: Context
) = Room.databaseBuilder(
context,
MyDatabase::class.java,
"my_database"
).build()
#Singleton
#Provides
fun provideDao(database: MyDatabase) = database.myDao()
#Singleton
#Provides
fun provideRepository(myDao: MyDao) = Repository(myDao)
#Singleton
#Provides
fun provideApplicationContext() = MyApplication()
}
Everything else is fine, and I got the error message:
Caused by: java.lang.RuntimeException: Cannot create an instance of
class com.example.example.viewmodel.MainViewModel
Caused by: java.lang.InstantiationException:
java.lang.Class<com.example.example.viewmodel.MainViewModel> has
no zero argument constructor
You can see full source https://github.com/Kotlin-Android-Open-Source/MVI-Coroutines-Flow/tree/dagger_hilt
Repository:
#Singleton
class UserRepositoryImpl #Inject constructor(
private val userApiService: UserApiService,
private val dispatchers: CoroutineDispatchers,
...
) : UserRepository { ... }
Usecases:
class AddUserUseCase #Inject constructor(private val userRepository: UserRepository) {
suspend operator fun invoke(user: User) = userRepository.add(user)
}
class RemoveUserUseCase #Inject constructor(private val userRepository: UserRepository) {
suspend operator fun invoke(user: User) = userRepository.remove(user)
}
class RefreshGetUsersUseCase #Inject constructor(private val userRepository: UserRepository) {
suspend operator fun invoke() = userRepository.refresh()
}
...
ViewModel:
class MainVM #ViewModelInject constructor(
private val getUsersUseCase: GetUsersUseCase,
private val refreshGetUsers: RefreshGetUsersUseCase,
private val removeUser: RemoveUserUseCase,
) : ViewModel() { ... }
Activity:
#AndroidEntryPoint
class MainActivity : AppCompatActivity(), View {
private val mainVM by viewModels<MainVM>()
...
}
Edited:
To inject application context:
First, remove this definition, because Hilt already provides application context:
#Singleton
#Provides
fun provideApplicationContext() = MyApplication()
Second, Use #ApplicationContext annotation on your context parameter.
class MainViewModel #ViewModelInject constructor(
#ApplicationContext private val context: Context,
private val repository: Repository,
#Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
Use #ApplicationContext Context context as a parameter in the constructor.

Pre-Populate Room Database using WorkManager and Dagger

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.

Categories

Resources