Skip to content
This repository was archived by the owner on Sep 3, 2023. It is now read-only.

Commit 7aa36f5

Browse files
committed
Qualifiers should be annotation based. Resolves #16.
1 parent 480f758 commit 7aa36f5

File tree

19 files changed

+127
-128
lines changed

19 files changed

+127
-128
lines changed

README.md

+55-66
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,10 @@
99
> remain unknown to the Vikings' rivals for centuries, the Ulfberht was a revolutionary high-tech
1010
> tool as well as a work of art.
1111
12-
*A little more bad-ass than a Dagger, huh?*
13-
14-
Dependency injection is a technique in which an application supplies dependencies of an object.
15-
"Dependencies" in this context are not dependencies like in a Gradle file. Dependencies are services
16-
(i.e. APIs, classes) that are needed in certain parts of your code.
17-
18-
Dependency injection enables you to pass around services without manual construction - it keeps
19-
track of everything for you and injects things where they are needed. The need for this is more
20-
apparent when you deal with very large applications.
12+
*A little more bad-ass than a Dagger.* Dependency injection is a technique in which an application
13+
supplies dependencies of an object. "Dependencies" in this context are not dependencies like in a
14+
Gradle file. Dependencies are services (i.e. APIs, classes) that are needed in certain parts of
15+
your code.
2116

2217
---
2318

@@ -51,9 +46,9 @@ based rather than reflection-based, while still being written in Kotlin. And I w
5146
scoping support, especially on Android. _This is the result._
5247

5348
You may be wondering what makes this library different than KOIN or other Kotlin "DI" libraries?
54-
Ulfberht uses annotation processing to do actual dependency injection vs. plain service location.
55-
A big example of this difference is that you don't need to manually tell this library how to fill
56-
constructor parameters, they are filled for you by generated code.
49+
Libraries like KOIN are just service locators - you need to build the dependency graph manually,
50+
filling in constructors, etc. Annotation processor based DI libraries handle this for you with
51+
code generation, so you *don't* need to write boilerplate and you *don't* need to use reflection.
5752

5853
---
5954

@@ -387,77 +382,66 @@ This code assumes that one of the modules going up the graph from `Component5` c
387382

388383
# Qualifiers
389384

390-
Qualifiers are simple identifiers that associate a type that may be broad, like a string, with a
391-
very specific bound or provided instance. There are a few places where you can specify a qualifier.
392-
393-
First, the `@Binds` and `@Provides` annotations take a qualifier:
385+
Qualifiers are simple identifiers that associate a type with a very specific bound or provided
386+
instance. A qualifier is a special type of annotation, which is defined like this:
394387

395388
```kotlin
396-
const val QUALIFIER_ONE = "one"
397-
const val QUALIFIER_TWO = "two"
389+
@Qualifier
390+
annotation class DemoQualifier1
398391

392+
@Qualifier
393+
annotation class DemoQualifier2
394+
```
395+
396+
You use it to mark `@Binds` and `@Provides` functions:
397+
398+
```kotlin
399399
@Module
400400
interface DemoModule1 {
401-
@Binds(QUALIFIER_ONE)
402-
fun demoClass1(impl: Demo1Impl): Demo1
403-
404-
@Binds(QUALIFIER_ONE)
405-
fun demoClass2(impl: Demo2Impl): Demo2
401+
@Binds @Singleton @DemoQualifier1
402+
fun demoClass(impl: Demo1Impl): Demo1
406403
}
407404

408405
@Module
409406
abstract class DemoModule2 {
410-
@Provides(QUALIFIER_TWO)
411-
fun demoClass1(): Demo1 {
407+
@Provides @Singleton @DemoQualifier2
408+
fun demoClass(): Demo1 {
412409
return Demo1Impl()
413410
}
414-
415-
@Provides(QUALIFIER_TWO)
416-
fun demoClass2(): Demo2 {
417-
return Demo2Impl()
418-
}
419411
}
420412
```
421413

422-
Second, constructor parameters take a qualifier via a `@Param` annotation:
414+
Then, you can mark constructor parameters with it...
423415

424416
```kotlin
425417
class SomeInjectedClass(
426-
@Param(QUALIFIER_ONE) val someDependency: Demo1,
427-
@Param(QUALIFIER_TWO) val anotherDependency: Demo1
418+
@DemoQualifier1 val someDependency: Demo1,
419+
@DemoQualifier2 val anotherDependency: Demo1
428420
) {
429421
...
430422
}
431423
```
432424

433-
Third, `@Provides` method parameters take a qualifier also via the `@Param` annotation:
434-
435-
```kotlin
436-
@Module
437-
abstract class DemoModule2 {
438-
@Provides
439-
fun demoClass1(
440-
@Param(QUALIFIER_ONE) neededDependency: Demo2
441-
): Demo1 {
442-
return Demo1Impl()
443-
}
444-
}
445-
```
446-
447-
Finally, the `@Inject` annotation takes a qualifier as well:
425+
...along with `@Inject` targets (the `field:` prefix on the annotation name is important in Kotlin):
448426

449427
```kotlin
450428
class SomeClass {
451-
@Inject(QUALIFIER_ONE)
452-
lateinit var someDependency: Demo1
453-
@Inject(QUALIFIER_TWO)
454-
lateinit var anotherDependency: Demo1
429+
@Inject @field:DemoQualifier1
430+
lateinit var someDependency1: Demo1
431+
@Inject @field:DemoQualifier2
432+
lateinit var someDependency2: Demo1
455433

456434
init {
457435
component<SomeComponent>().inject(this)
458436
}
459437
}
460-
```
438+
```
439+
440+
You will get two completely separate instances of `Demo1`, since two different qualifiers are being
441+
used. `@Singleton` was applied for demo purposes to show that it'll store two different instances.
442+
But even without that, you're providing two different things. This could be useful if you were
443+
providing primitives, like strings, or an interface for something like preferences. There's a lot of
444+
possibilities.
461445

462446
---
463447

@@ -467,43 +451,48 @@ Sometimes your app may need to be able to inject something that is defined at ru
467451
that cannot be constructed in a module. A good example of when this would be necessary is in
468452
an Android application, like if you needed to inject the Application context.
469453

470-
First, you tag constructor parameters that need to be provided at runtime with the `@Param`
471-
annotation, which is discussed in [Qualifiers](#qualifiers) above.
454+
First, you tag constructor parameters or fields that need to be provided at runtime with a
455+
qualifier annotation, which is discussed in [Qualifiers](#qualifiers) above.
472456

473457
```kotlin
474-
const val APP_CONTEXT: String = "app_context"
458+
@Qualifier
459+
annotation class AppContext
460+
461+
@Qualifier
462+
annotation class ApiKey
475463

476464
class StringRetriever(
477-
@Param(APP_CONTEXT) val appContext: Context
465+
@AppContext val appContext: Context,
466+
@ApiKey val apiKey: String
478467
) {
479468
fun getString(@IdRes res: Int): String {
480469
return appContext.resources.getString(res)
481470
}
482471
}
483472
```
484473

485-
At injection time, you pass mapped runtime dependencies into the `component` method. They are available
486-
for injection until the component is destroyed, or its parents destroy it.
474+
At injection time, you pass mapped runtime dependencies into the `component<>()` method. They are
475+
available for injection until the component is destroyed, or its parents destroy it.
487476

488477
```kotlin
489-
// Should ideally be the same constant above, rather than being defined twice
490-
const val APP_CONTEXT: String = "app_context"
491-
492478
class LoginActivity : AppCompatActivity() {
493479
@Inject
494-
lateinit var stringRetriever: StringRetriever
480+
lateinit var stringRetriever: StringRetriever
495481

496482
override fun onCreate(savedInstanceState: Bundle?) {
497483
super.onCreate(savedInstanceState)
498484

499485
component<LoginComponent>(
500-
APP_CONTEXT to applicationContext
486+
AppContext::class to applicationContext,
487+
ApiKey::class to "hello, world!"
501488
).inject(this)
502489
}
503490
}
504491
```
505492

506-
Runtime dependencies in a component are made available to all of the component's children too. In an Android application, providing the application context at the `Application` level will make it available to all Activities and Fragments that use child components.
493+
Runtime dependencies in a component are made available to all of the component's children too.
494+
In an Android application, providing the application context at the `Application` level will make
495+
it available to all Activities and Fragments that use child components.
507496

508497
---
509498

@@ -579,4 +568,4 @@ just inject the `ViewModel` as you would inject anything else.
579568
However, you can only inject a `ViewModel` into an `androidx.fragment.app.Fragment` or
580569
`androidx.fragment.app.FragmentActivity` (includes `AppCompatActivity` and descendants). Why?
581570
Internally, Ulfberht's generated code delegates through `ViewModelProviders` which must attach to
582-
an Activity or Fragment.
571+
an Activity or Fragment.

annotations/src/main/kotlin/com/afollestad/ulfberht/annotation/Binds.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@ package com.afollestad.ulfberht.annotation
2424
*/
2525
@Retention(AnnotationRetention.SOURCE)
2626
@Target(AnnotationTarget.FUNCTION)
27-
annotation class Binds(val qualifier: String = "")
27+
annotation class Binds

annotations/src/main/kotlin/com/afollestad/ulfberht/annotation/Inject.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@ package com.afollestad.ulfberht.annotation
2424
*/
2525
@Retention(AnnotationRetention.SOURCE)
2626
@Target(AnnotationTarget.FIELD)
27-
annotation class Inject(val qualifier: String = "")
27+
annotation class Inject

annotations/src/main/kotlin/com/afollestad/ulfberht/annotation/Provides.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@ package com.afollestad.ulfberht.annotation
2424
*/
2525
@Retention(AnnotationRetention.SOURCE)
2626
@Target(AnnotationTarget.FUNCTION)
27-
annotation class Provides(val qualifier: String = "")
27+
annotation class Provides

annotations/src/main/kotlin/com/afollestad/ulfberht/annotation/Param.kt annotations/src/main/kotlin/com/afollestad/ulfberht/annotation/Qualifier.kt

+4-4
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818
package com.afollestad.ulfberht.annotation
1919

2020
/**
21-
* Used to add an injection qualifier to a constructor or function parameter.
21+
* Marks another annotation class as a qualifier.
2222
*
2323
* @author Aidan Follestad (@afollestad)
2424
*/
25-
@Retention(AnnotationRetention.RUNTIME)
26-
@Target(AnnotationTarget.TYPE_PARAMETER, AnnotationTarget.VALUE_PARAMETER)
27-
annotation class Param(val qualifier: String)
25+
@Retention(AnnotationRetention.SOURCE)
26+
@Target(AnnotationTarget.ANNOTATION_CLASS)
27+
annotation class Qualifier

core/src/main/kotlin/com/afollestad/ulfberht/Components.kt

+4-3
Original file line numberDiff line numberDiff line change
@@ -95,15 +95,16 @@ internal object Components {
9595
* @author Aidan Follestad (@afollestad)
9696
*/
9797
inline fun <reified T : Any> component(
98-
vararg runtimeDependencies: Pair<String?, Any>
98+
vararg runtimeDependencies: Pair<KClass<*>, Any>
9999
): T {
100100
return Components.get(T::class)
101-
.withRuntimeDependencies(runtimeDependencies.toMap())
101+
.withRuntimeDependencies(
102+
runtimeDependencies.toMap().mapKeys { "@${it.key.qualifiedName}" })
102103
}
103104

104105
@PublishedApi
105106
internal inline fun <reified T : Any> Any.withRuntimeDependencies(
106-
runtimeDependencies: Map<String?, Any>
107+
runtimeDependencies: Map<String, Any>
107108
): T {
108109
return if (runtimeDependencies.isEmpty()) {
109110
this as T

core/src/main/kotlin/com/afollestad/ulfberht/common/BaseComponent.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ interface BaseComponent : ScopeObserver {
3434
val parentType: KClass<*>?
3535
val children: MutableSet<BaseComponent>
3636
val modules: Set<BaseModule>
37-
var runtimeDependencies: Map<String?, Any>?
37+
var runtimeDependencies: Map<String, Any>?
3838

3939
@Suppress("UNCHECKED_CAST")
4040
fun <T : Any> get(

processor/src/main/kotlin/com/afollestad/ulfberht/components/ComponentBuilder.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ internal class ComponentBuilder(
246246

247247
private fun propertyRuntimeDependencies(): PropertySpec {
248248
val propertyType = MAP
249-
.parameterizedBy(NULLABLE_KOTLIN_STRING, ANY)
249+
.parameterizedBy(STRING, ANY)
250250
.copy(nullable = true)
251251
return PropertySpec.builder(RUNTIME_DEPS_NAME, propertyType, OVERRIDE)
252252
.mutable()

processor/src/main/kotlin/com/afollestad/ulfberht/util/ProcessorUtil.kt

+11-20
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,9 @@
1616
package com.afollestad.ulfberht.util
1717

1818
import com.afollestad.ulfberht.Provider
19-
import com.afollestad.ulfberht.annotation.Binds
2019
import com.afollestad.ulfberht.annotation.Inject
21-
import com.afollestad.ulfberht.annotation.Param
22-
import com.afollestad.ulfberht.annotation.Provides
20+
import com.afollestad.ulfberht.annotation.Qualifier
2321
import com.afollestad.ulfberht.util.Names.MODULES_LIST_NAME
24-
import com.afollestad.ulfberht.util.Names.QUALIFIER
2522
import com.afollestad.ulfberht.util.Types.VIEW_MODEL
2623
import com.squareup.kotlinpoet.BOOLEAN
2724
import com.squareup.kotlinpoet.ClassName
@@ -71,6 +68,13 @@ internal object ProcessorUtil {
7168
return getAnnotationMirror<T>() != null
7269
}
7370

71+
private fun Element.getQualifier(): String? {
72+
return annotationMirrors.singleOrNull { ann ->
73+
ann.annotationType.asElement()
74+
.hasAnnotationMirror<Qualifier>()
75+
}?.toString()
76+
}
77+
7478
fun TypeMirror.asTypeAndArgs(
7579
env: ProcessingEnvironment,
7680
nullable: Boolean = false,
@@ -124,23 +128,20 @@ internal object ProcessorUtil {
124128
fun VariableElement.asTypeAndArgs(
125129
env: ProcessingEnvironment
126130
): TypeAndArgs {
127-
val qualifier = getAnnotationMirror<Param>().qualifier
128131
return asType().asTypeAndArgs(
129132
env = env,
130133
nullable = isNullable(),
131-
qualifier = qualifier
134+
qualifier = getQualifier()
132135
)
133136
}
134137

135138
fun ExecutableElement.returnTypeAsTypeAndArgs(
136139
env: ProcessingEnvironment
137140
): TypeAndArgs {
138-
val qualifier = (getAnnotationMirror<Provides>() ?: getAnnotationMirror<Binds>())
139-
.qualifier
140141
return returnType.asTypeAndArgs(
141142
env = env,
142143
nullable = isNullable(),
143-
qualifier = qualifier
144+
qualifier = getQualifier()
144145
)
145146
}
146147

@@ -166,7 +167,7 @@ internal object ProcessorUtil {
166167
fun Collection<Element>.injectedFieldsAndQualifiers(): Sequence<Pair<VariableElement, String?>> {
167168
return filterFields()
168169
.filter { it.getAnnotationMirror<Inject>() != null }
169-
.map { Pair(it, it.getAnnotationMirror<Inject>()!!.getParameter<String>("qualifier")) }
170+
.map { Pair(it, it.getQualifier()) }
170171
}
171172

172173
fun Collection<Element>.filterMethods(): Sequence<ExecutableElement> {
@@ -317,22 +318,12 @@ internal object ProcessorUtil {
317318
asTypeElement().getSuperClass(env).isViewModel(env)
318319
}
319320

320-
// fun TypeMirror?.isActivityOrFragment(): Boolean {
321-
// return
322-
// }
323-
324321
val AnnotationMirror?.name: String?
325322
get() {
326323
val qualifier: String = this?.getParameter("name") ?: return null
327324
return if (qualifier.isEmpty()) null else qualifier
328325
}
329326

330-
private val AnnotationMirror?.qualifier: String?
331-
get() {
332-
val qualifier: String = this?.getParameter(QUALIFIER) ?: return null
333-
return if (qualifier.isEmpty()) null else qualifier
334-
}
335-
336327
private fun Element.getSuperClass(env: ProcessingEnvironment): TypeMirror? {
337328
return env.typeUtils.directSupertypes(this.asType())
338329
.asSequence()

sample-android/src/main/kotlin/com/afollestad/ulfberhtsample/ScopeNames.kt

-4
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,3 @@ object ScopeNames {
1919
const val LOGIN = "login_scope"
2020
const val MAIN = "main_scope"
2121
}
22-
23-
object ParamNames {
24-
const val APP_CONTEXT = "param_app_context"
25-
}

sample-android/src/main/kotlin/com/afollestad/ulfberhtsample/api/Authenticator.kt

+2-3
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616
package com.afollestad.ulfberhtsample.api
1717

1818
import android.util.Log
19-
import com.afollestad.ulfberht.annotation.Param
20-
import com.afollestad.ulfberhtsample.Qualifiers.API_KEY
19+
import com.afollestad.ulfberhtsample.app.ApiKey
2120

2221
interface Authenticator {
2322
fun login(
@@ -27,7 +26,7 @@ interface Authenticator {
2726
}
2827

2928
class RealAuthenticator(
30-
@Param(API_KEY) private val apiKey: String,
29+
@ApiKey private val apiKey: String,
3130
private val session: Session
3231
) : Authenticator {
3332
override fun login(

0 commit comments

Comments
 (0)