Some Anvil code generators originally used in Pixnews Android application. Published as examples of possible recipes with Anvil generators.
Simplifies the creation of Dagger components and modules using the Anvil compiler plugin
Release and snapshot versions of the library are published to a temporary repository, since this library is currently used only in one project.
Add the following to your project's settings.gradle:
pluginManagement {
repositories {
maven {
url = uri("https://maven.pixnews.ru")
mavenContent {
includeGroup("ru.pixnews.anvil.codegen")
}
}
}
}
Below is a list of all implemented generators:
- Activity generator
- Viewmodel generator
- Workmanager generator
- Test generator
- Experiment generator
- Initializer generator
The following type aliases are used in the examples:
typealias DaggerSet<T> = Set<@JvmSuppressWildcards T>
typealias DaggerMap<K, V> = Map<@JvmSuppressWildcards K, @JvmSuppressWildcards V>
Generator that simplifies dependency injection to an Android Activities using the ContributesActivity
annotation.
Add the required dependencies
dependencies {
anvil("ru.pixnews.anvil.codegen:activity-generator:0.5")
api("ru.pixnews.anvil.codegen:activity-inject:0.5")
}
Create a subcomponent with an Activity scope and add it to your application component, as shown in the following example:
import ru.pixnews.anvil.codegen.activity.inject.ActivityScope
import ru.pixnews.anvil.codegen.activity.inject.wiring.ActivityInjector
import <your app scope>.AppScope
@SingleIn(ActivityScope::class)
@ContributesSubcomponent(scope = ActivityScope::class, parentScope = AppScope::class)
interface ActivitySubcomponent {
val activityInjector: ActivityInjector
@ContributesSubcomponent.Factory
fun interface Factory {
fun create(@BindsInstance activity: Activity): ActivitySubcomponent
}
@ContributesTo(AppScope::class)
interface ActivitySubcomponentFactoryHolder {
fun getActivitySubcomponentFactory(): ActivitySubcomponent.Factory
}
}
Add module for the subcomponent:
import dagger.MembersInjector
import ru.pixnews.anvil.codegen.activity.inject.ActivityScope
import ru.pixnews.anvil.codegen.activity.inject.wiring.ActivityInjector
import ru.pixnews.anvil.codegen.activity.inject.wiring.DefaultActivityInjector
@ContributesTo(ActivityScope::class)
@Module
@RestrictTo(RestrictTo.Scope.LIBRARY)
interface PixnewsActivityModule {
@Multibinds
fun activityInjectors(): DaggerMap<Class<out Activity>, MembersInjector<out Activity>>
companion object {
@Reusable
@Provides
fun provideActivityInjector(
injectors: DaggerMap<Class<out Activity>, MembersInjector<out Activity>>,
): ActivityInjector = DefaultActivityInjector(injectors)
}
}
Perform an injection in the base activity:
abstract class BaseActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
(appComponent as ActivitySubcomponentFactoryHolder)
.getActivitySubcomponentFactory()
.create(this)
.activityInjector
.inject(this)
super.onCreate(savedInstanceState)
}
}
Now your can annotate any activity with ContributesActivity
to add this activity to multibinding.
import ru.pixnews.anvil.codegen.activity.inject.ContributesActivity
@ContributesActivity
class MainActivity : BaseActivity() {
@Inject
internal lateinit var analytics: Analytics
}
The following module will be generated based on this annotation:
import ru.pixnews.anvil.codegen.activity.inject.ActivityScope
import ru.pixnews.anvil.codegen.activity.inject.wiring.ActivityMapKey
@Module
@ContributesTo(ActivityScope::class)
interface MainActivityModule {
@Binds
@IntoMap
@ActivityMapKey(MainActivity::class)
@SingleIn(ActivityScope::class)
fun bindsMainActivityInjector(target: MembersInjector<MainActivity>): MembersInjector<out Activity>
}
Code Generator to simplify adding view models to the dependency graph with the ContributesViewModel
annotation.
Add the required dependencies:
dependencies {
anvil("ru.pixnews.anvil.codegen:viewmodel-generator:0.5")
api("ru.pixnews.anvil.codegen:viewmodel-inject:0.5")
}
Create a subcomponent for ViewModels and add it to your application component, as shown in the following example:
import ru.pixnews.anvil.codegen.viewmodel.inject.ViewModelScope
import ru.pixnews.anvil.codegen.viewmodel.inject.wiring.ViewModelFactory
import <your app scope>.AppScope
@ContributesSubcomponent(scope = ViewModelScope::class, parentScope = AppScope::class)
@SingleIn(ViewModelScope::class)
interface ViewModelSubcomponent {
val viewModelMap: DaggerMap<Class<out ViewModel>, Provider<ViewModel>>
val viewModelFactoryMap: DaggerMap<Class<out ViewModel>, ViewModelFactory>
@ContributesSubcomponent.Factory
fun interface Factory {
fun create(@BindsInstance savedStateHandle: SavedStateHandle): ViewModelSubcomponent
}
@ContributesTo(AppScope::class)
public interface ViewModelSubcomponentFactoryHolder {
public fun getViewModelSubcomponentFactory(): ViewModelSubcomponent.Factory
public fun getViewModelFactory(): ViewModelProvider.Factory
}
}
Declaration of the multibindings in module:
import ru.pixnews.anvil.codegen.viewmodel.inject.ViewModelScope
@Module
@ContributesTo(ViewModelScope::class)
interface ViewModelModule {
@Multibinds
fun viewModelProviders(): DaggerMap<Class<out ViewModel>, ViewModel>
@Multibinds
fun viewModelFactoryProviders(): DaggerMap<Class<out ViewModel>, ViewModelFactory>
}
Create implementation of ViewModelProvider.Factory
and add it to app scope:
import <your app scope>.AppScope
@Reusable
@ContributesBinding(AppScope::class, boundType = ViewModelProvider.Factory::class)
@RestrictTo(RestrictTo.Scope.LIBRARY)
class ViewModelProviderFactory(
private val vmSubcomponentFactory: ViewModelSubcomponent.Factory,
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
val savedSateHandle = extras.createSavedStateHandle()
val viewModelComponent = vmSubcomponentFactory.create(savedSateHandle)
val viewModelMap = viewModelComponent.viewModelMap
val viewModelProvider = viewModelMap[modelClass]
if (viewModelProvider != null) {
return viewModelProvider.get() as T
} else {
val factory = viewModelComponent.viewModelFactoryMap[modelClass]
?: error("No factory for ${modelClass.name}")
return factory.create(extras) as T
}
}
}
You can use created getViewModelFactory()
of the application component as a view model factory.
import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.viewmodel.CreationExtras
inline fun <reified VM : ViewModel> ComponentActivity.injectedViewModel(
noinline extrasProducer: (() -> CreationExtras) = { defaultViewModelCreationExtras },
): Lazy<VM> = viewModels(extrasProducer, appComponent::viewModelFactory)
@Composable
fun <VM : ViewModel> injectedViewModel(
modelClass: Class<VM>,
viewModelStoreOwner: ViewModelStoreOwner = …,
key: String? = null,
extras: CreationExtras = …,
): VM = viewModel(
modelClass,
viewModelStoreOwner,
key,
appComponent.getViewModelFactory(),
extras,
)
Annotate your ViewModel with ContributesViewModel
to add it to the multibinding:
@ContributesViewModel
class MyViewModel(
featureManager: FeatureManager,
…
savedStateHandle: SavedStateHandle,
) : ViewModel() {
…
}
The following binding will be generated based on this annotation:
import ru.pixnews.anvil.codegen.viewmodel.inject.ViewModelScope
import ru.pixnews.anvil.codegen.viewmodel.inject.wiring.ViewModelFactory
import ru.pixnews.anvil.codegen.viewmodel.inject.wiring.ViewModelMapKey
@Module
@ContributesTo(ViewModelScope::class)
object MyFactoryModule {
@Provides
@IntoMap
@ViewModelMapKey(MyViewModel::class)
fun providesMyViewModelViewModelFactory(
featureManager: FeatureManager,
…
): ViewModelFactory = ViewModelFactory {
MyViewModel(
featureManager = featureManager,
…
savedStateHandle = it.createSavedStateHandle()
)
}
}
You can pass additional arguments to ViewModel in DEFAULT_ARGS_KEY of CreationExtras.
Code Generator to simplify adding CoroutineWorker of WorkManager to the dependency graph with the ContributesCoroutineWorker
annotation.
Add the required dependencies:
dependencies {
anvil("ru.pixnews.anvil.codegen:workmanager-generator:0.5")
api("ru.pixnews.anvil.codegen:workmanager-inject:0.5")
}
Create a subcomponent for workers and add it to your application component, as shown in the following example:
import ru.pixnews.anvil.codegen.workmanager.inject.WorkManagerScope
import ru.pixnews.anvil.codegen.workmanager.inject.wiring.CoroutineWorkerFactory
import <your app scope>.AppScope
@SingleIn(WorkManagerScope::class)
@ContributesSubcomponent(scope = WorkManagerScope::class, parentScope = AppScope::class)
interface WorkManagerSubcomponent {
val workerFactories: DaggerMap<Class<out CoroutineWorker>, CoroutineWorkerFactory>
@ContributesSubcomponent.Factory
fun interface Factory {
fun create(): WorkManagerSubcomponent
}
}
@ContributesTo(WorkManagerScope::class)
@Module
interface WorkManagerSubcomponentModule {
@Multibinds
fun workerFactories(): DaggerMap<Class<out CoroutineWorker>, CoroutineWorkerFactory>
}
Create WorkerFactory:
internal class AppWorkerFactory(
private val workerSubcomponentFactory: WorkManagerSubcomponent.Factory,
) : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters,
): ListenableWorker? {
val workerComponent = workerSubcomponentFactory.create()
val factory = workerComponent.workerFactories.firstNotNullOfOrNull {
if (it.key.canonicalName == workerClassName) it.value else null
}
return if (factory != null) {
factory.create(appContext, workerParameters)
} else {
null
}
}
}
Setup WorkManager to use our custom WorkerFactory:
import androidx.work.Configuration
import <yout app scope>.AppScope
@ContributesTo(AppScope::class)
@Module
object WorkManagerModule {
@Reusable
@Provides
fun providesWorkMangerConfiguration(
workerFactory: WorkerFactory,
): Configuration {
return with(Configuration.Builder()) {
setWorkerFactory(workerFactory)
build()
}
}
@Reusable
@Provides
fun providesWorkManager(
@ApplicationContext applicationContext: Context,
): WorkManager {
return WorkManager.getInstance(applicationContext)
}
@Reusable
@Provides
fun providesWorkerFactory(
subcomponentFactory: WorkManagerSubcomponentModule.Factory,
): WorkerFactory {
return AppWorkerFactory(subcomponentFactory)
}
}
Application class:
class TheApplication : Application(), Configuration.Provider {
override val workManagerConfiguration
get() = localWorkManagerConfiguration
@field:Inject
lateinit var localWorkManagerConfiguration: Configuration
override fun onCreate() {
super.onCreate()
appComponent.inject(this)
}
}
See developer.android.com for more up-to-date instruction.
Annotate your Coroutine Worker with ContributesCoroutineWorker
to add it to the multibinding:
import ru.pixnews.anvil.codegen.workmanager.inject.ContributesCoroutineWorker
@ContributesCoroutineWorker
class MyCoroutineWorker @AssistedInject constructor(
logger: Logger,
@Assisted appContext: Context,
@Assisted params: WorkerParameters
) : CoroutineWorker(appContext, params) {
override suspend fun doWork(): Result = Result.success()
}
The following binding will be generated based on this annotation:
import ru.pixnews.anvil.codegen.workmanager.inject.WorkManagerScope
import ru.pixnews.anvil.codegen.workmanager.inject.wiring.CoroutineWorkerFactory
import ru.pixnews.anvil.codegen.workmanager.inject.wiring.CoroutineWorkerMapKey
import ru.pixnews.foundation.di.base.qualifiers.ApplicationContext
@AssistedFactory
@ContributesMultibinding(scope = WorkManagerScope::class)
@CoroutineWorkerMapKey(MyCoroutineWorker::class)
interface MyCoroutineWorker_AssistedFactory : CoroutineWorkerFactory {
override fun create(
@ApplicationContext context: Context,
workerParameters: WorkerParameters
): MyCoroutineWorker
}
A generator that simplifies dependency injection to an junit4 instrumented Android tests with the ContributesTest
annotation.
Add the required dependencies:
dependencies {
anvil("ru.pixnews.anvil.codegen:test-generator:0.5")
api("ru.pixnews.anvil.codegen:test-inject:0.5")
}
Add InstrumentedTestInjector
to your application component:
import ru.pixnews.anvil.codegen.test.inject.wiring.InstrumentedTestInjector
import ru.pixnews.foundation.di.base.scopes.AppScope
@ContributesTo(AppScope::class)
interface InstrumentedTestInjectorHolder {
val instrumentedTestInjector: InstrumentedTestInjector
}
Add multibinding to the application module:
import ru.pixnews.anvil.codegen.test.inject.wiring.DefaultInstrumentedTestInjector
import ru.pixnews.anvil.codegen.test.inject.wiring.InstrumentedTestInjector
import ru.pixnews.anvil.codegen.test.inject.wiring.SingleInstrumentedTestInjector
import ru.pixnews.foundation.di.base.DaggerMap
import ru.pixnews.foundation.di.base.scopes.AppScope
@ContributesTo(AppScope::class)
@Module
interface InstrumentedTestsInjectorsModule {
@Multibinds
fun instrumentedTestInjectors(): DaggerMap<Class<out Any>, SingleInstrumentedTestInjector>
companion object {
@Reusable
@Provides
fun provideInstrumentedTestInjector(
injectors: DaggerMap<Class<out Any>, SingleInstrumentedTestInjector>,
): InstrumentedTestInjector {
return DefaultInstrumentedTestInjector(injectors)
}
}
}
Create Junit4 TestRule with dependency injection:
class InjectDependenciesRule(
private val instance: Any,
) : TestRule {
override fun apply(base: Statement, description: Description): Statement {
injectDependencies()
return base
}
private fun injectDependencies() {
(appComponent as InstrumentedTestInjectorHolder)
.instrumentedTestInjector
.inject(instance)
}
}
Annotate your test with ContributesTest
and use InjectDependenciesRule:
import ru.pixnews.anvil.codegen.test.inject.ContributesTest
@ContributesTest
class MyTest {
@get:Rule(order = 10)
val injectDependencies = InjectDependenciesRule(this)
@Inject
lateinit var appConfig: AppConfig
}
The following binding will be generated based on this annotation:
import ru.pixnews.anvil.codegen.test.inject.wiring.SingleInstrumentedTestInjector
import ru.pixnews.foundation.di.base.scopes.AppScope
@Module
@ContributesTo(AppScope::class)
object MyTest_TestModule {
@Provides
@IntoMap
@ClassKey(MyTest::class)
@SingleIn(AppScope::class)
fun provideMyTestInjector(
injector: MembersInjector<CalendarFeedWidthOnMediumSizeTest>
): SingleInstrumentedTestInjector = SingleInstrumentedTestInjector(injector)
}
ContributesExperiment
and ContributesExperimentVariantSerializer
annotations used to simplify creation of feature flags in different application modules and adding them to a feature manager.
Add the required dependencies:
dependencies {
anvil("ru.pixnews.anvil.codegen:experiment-generator:0.5")
api("ru.pixnews.anvil.codegen:experiment-inject:0.5")
}
Create component with experiments:
import ru.pixnews.anvil.codegen.experiment.inject.ExperimentScope
@SingleIn(ExperimentScope::class)
@MergeComponent(scope = ExperimentScope::class)
interface ExperimentsComponent {
fun appExperiments(): DaggerSet<Experiment>
fun appExperimentVariantSerializers(): DaggerMap<ExperimentKey, ExperimentVariantSerializer>
companion object {
operator fun invoke(): ExperimentsComponent = DaggerExperimentsComponent.create()
}
}
Module:
import ru.pixnews.anvil.codegen.experiment.inject.ExperimentScope
@Module
@ContributesTo(ExperimentScope::class)
abstract class ExperimentsModule {
@Multibinds
abstract fun appExperiments(): DaggerSet<Experiment>
@Multibinds
abstract fun appExperimentSerializers(): DaggerMap<String, ExperimentVariantSerializer>
}
Annotate experiment with @ContributesExperiment
and serializer with @ContributesExperimentSerializer
:
@ContributesExperiment
object DarkModeExperiment : Experiment {
…
@ContributesExperimentVariantSerializer("ui.dark_mode")
object Serializer : BooleanVariantSerializer(…)
}
The following binding will be generated based on this annotation:
@Module
@ContributesTo(ExperimentScope::class)
object DarkModeExperiment_Experiments_Module {
@Provides
@IntoSet
fun provideDarkModeExperiment(): Experiment = DarkModeExperiment
@Provides
@IntoMap
@ExperimentVariantMapKey(key = "ui.dark_mode")
fun provideSerializer(): ExperimentVariantSerializer = DarkModeExperiment.Serializer
}
ContributesInitializer
annotation used to simplify the aggregation of application initializers from application modules.
Add the required dependencies:
dependencies {
anvil("ru.pixnews.anvil.codegen:initializer-generator:0.5")
api("ru.pixnews.anvil.codegen:initializer-inject:0.5")
}
Create component to collect all initializers into multibinding set:
import ru.pixnews.anvil.codegen.initializer.inject.AppInitializersScope
@SingleIn(AppInitializersScope::class)
@MergeComponent(
scope = AppInitializersScope::class,
dependencies = […],
)
interface AppInitializerComponent {
fun inject(initializer: GlobalAndroidxStartupAppInitializer)
@Component.Factory
fun interface Factory {
…
}
}
Module:
import ru.pixnews.anvil.codegen.initializer.inject.AppInitializersScope
import ru.pixnews.foundation.initializers.AppInitializer
import ru.pixnews.foundation.initializers.AsyncInitializer
import ru.pixnews.foundation.initializers.Initializer
@ContributesTo(AppInitializersScope::class)
@Module
abstract class AppInitializersModule {
@Multibinds
abstract fun appInitializers(): DaggerSet<Initializer>
@Multibinds
abstract fun appAsyncInitializers(): DaggerSet<AsyncInitializer>
companion object {
@Provides
fun providesAppInitializer(
initializers: DaggerSet<Initializer>,
asyncInitializers: DaggerSet<AsyncInitializer>,
…
): AppInitializer {
// AppInitializer used in GlobalAndroidxStartupAppInitializer (androidx.startup.Initializer)
// to execute collected initializers
return AppInitializer(initializers, asyncInitializers, …)
}
}
}
Implement ru.pixnews.foundation.initializers.AsyncInitializer
or ru.pixnews.foundation.initializers.Initializer
and annotate it with ContributesInitializer
to add it to multibinding:
@ContributesInitializer(replaces = [DebugStrictModeInitializerModule::class])
class TestStrictModeInitializer @Inject constructor(plogger: Logger) : Initializer {
private val logger = logger.withTag("TestStrictModeInitializer")
override fun init() {
logger.v { "Setting up StrictMode" }
StrictMode.setThreadPolicy(
…
)
}
}
The following binding will be generated based on this annotation:
@Module
@ContributesTo(
AppInitializersScope::class,
replaces = [DebugStrictModeInitializerModule::class],
)
object TestStrictModeInitializer_InitializerModule {
@Provides
@IntoSet
@Reusable
fun provideTestStrictModeInitializer(logger: Logger): Initializer = TestStrictModeInitializer(
logger = logger
)
}
Some other good reposotories with Anvil generators
- https://github.com/deliveryhero/whetstone
Whetstone, DI framework for Android that greatly simplifies working with Dagger 2 using Anvil - https://github.com/RBusarow/Tangle
Tangle, android injection using the Anvil compiler plugin - https://github.com/duckduckgo/Android/tree/develop/anvil/anvil-compiler/src/main/java/com/duckduckgo/anvil/compiler
Generators from the DuckDuckGo Android App - https://github.com/SteinerOk/sealant/
Some code generators - https://slackhq.github.io/circuit/code-gen/
Sample of the KSP-based code generator that works both with Anvil and Hilt
Any type of contributions are welcome. Please see the contribution guide.
These services are licensed under Apache 2.0 License. Authors and contributors are listed in the Authors file.
Copyright 2024 pixnews-anvil-codegen project authors and contributors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.