Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2026 Harry Timothy Tumalewa
*
* 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
*
* https://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.
*/

package com.harrytmthy.stitch.core

import com.harrytmthy.stitch.annotations.Qualifier

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Production

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Staging
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,14 @@ package com.harrytmthy.stitch.feature.home

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.harrytmthy.stitch.annotations.Named
import com.harrytmthy.stitch.api.StitchInjector
import com.harrytmthy.stitch.api.get
import com.harrytmthy.stitch.api.named
import com.harrytmthy.stitch.api.typed
import com.harrytmthy.stitch.core.Logger
import com.harrytmthy.stitch.core.Production
import com.harrytmthy.stitch.core.Staging
import javax.inject.Inject

class HomeActivity : AppCompatActivity() {
Expand All @@ -30,11 +36,47 @@ class HomeActivity : AppCompatActivity() {
@Inject
lateinit var viewModel: HomeViewModel

@Production
@Inject
lateinit var homeService: HomeService

@Production
@Inject
lateinit var homeServiceImpl: HomeServiceImpl

@Staging
@Inject
lateinit var homeServiceStaging: HomeService

@Staging
@Inject
lateinit var homeServiceImplStaging: HomeServiceStaging

@Named("dev")
@Inject
lateinit var homeServiceDev: HomeService

@Named("dev")
@Inject
lateinit var homeServiceDevImpl: HomeServiceDev

override fun onCreate(savedInstanceState: Bundle?) {
StitchInjector.getSingletonGraph()
val activityInjector = StitchInjector.getSingletonGraph()
.createInjectorForChildScope("activity")
.inject(this)
activityInjector.inject(this)
super.onCreate(savedInstanceState)
setContentView(android.R.layout.list_content)
assert(homeService === homeServiceImpl)
assert(viewModel.homeService === homeServiceImpl)
assert(homeServiceStaging === homeServiceImplStaging)
assert(homeService !== homeServiceStaging)
assert(homeServiceDev === homeServiceDevImpl)
assert(homeService !== homeServiceDev)

val stagingQualifier = typed<Staging>()
assert(activityInjector.get<HomeService>(stagingQualifier) === homeServiceStaging)

val devQualifier = named("dev")
assert(activityInjector.get<HomeService>(devQualifier) === homeServiceDev)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
package com.harrytmthy.stitch.feature.home

import com.harrytmthy.stitch.annotations.Binds
import com.harrytmthy.stitch.annotations.Named
import com.harrytmthy.stitch.core.Logger
import com.harrytmthy.stitch.core.Production
import com.harrytmthy.stitch.core.Staging
import javax.inject.Inject
import javax.inject.Singleton

Expand All @@ -27,10 +30,33 @@ interface HomeService {

@Singleton
@Binds(aliases = [HomeService::class])
@Production
class HomeServiceImpl @Inject constructor(private val logger: Logger) : HomeService {

override fun fetch(): Result<Unit> {
logger.log("Fetch success!")
return Result.success(Unit)
}
}

@Singleton
@Binds(aliases = [HomeService::class])
@Staging
class HomeServiceStaging @Inject constructor(private val logger: Logger) : HomeService {

override fun fetch(): Result<Unit> {
logger.log("Fetch on staging success!")
return Result.success(Unit)
}
}

@Singleton
@Binds(aliases = [HomeService::class])
@Named("dev")
class HomeServiceDev @Inject constructor(private val logger: Logger) : HomeService {

override fun fetch(): Result<Unit> {
logger.log("Fetch on dev success!")
return Result.success(Unit)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@ package com.harrytmthy.stitch.feature.home

import com.harrytmthy.stitch.core.Activity
import com.harrytmthy.stitch.core.Logger
import com.harrytmthy.stitch.core.Production
import javax.inject.Inject

@Activity
class HomeViewModel @Inject constructor(
private val logger1: Logger,
private val logger2: Logger, // logger1 === logger2
private val homeService: HomeService,
@param:Production val homeService: HomeService,
) {

fun fetchHomeData() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ package com.harrytmthy.stitch.annotations
*/
@Qualifier
@Target(
AnnotationTarget.CLASS,
AnnotationTarget.FUNCTION,
AnnotationTarget.VALUE_PARAMETER,
AnnotationTarget.FIELD,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,7 @@ object ScopedGraphGenerator {
private fun TypeSpec.Builder.addUniversalGetterFunction(plan: InjectorPlan) {
val qualifierClass = com.harrytmthy.stitch.api.Qualifier::class.asClassName()
val namedClass = com.harrytmthy.stitch.api.Named::class.asClassName()
val typedClass = com.harrytmthy.stitch.api.Typed::class.asClassName()
val kClassClass = kotlin.reflect.KClass::class.asClassName()

addFunction(
Expand All @@ -420,18 +421,21 @@ object ScopedGraphGenerator {
.build(),
)
.returns(TypeVariableName("T"))
.addCode(buildUniversalGetterBody(plan, namedClass))
.addCode(buildUniversalGetterBody(plan, namedClass, typedClass))
.build(),
)
}

private fun buildUniversalGetterBody(plan: InjectorPlan, namedClass: ClassName): CodeBlock {
private fun buildUniversalGetterBody(
plan: InjectorPlan,
namedClass: ClassName,
typedClass: ClassName,
): CodeBlock {
val bindingsByType = linkedMapOf<String, List<ValidatedBinding>>()
for (binding in plan.ancestorBindings) {
bindingsByType.getOrPut(binding.type) { ArrayList() }
}
for (binding in plan.ancestorBindings) {
@Suppress("UNCHECKED_CAST")
(bindingsByType.getValue(binding.type) as MutableList<ValidatedBinding>).add(binding)
}

Expand All @@ -444,11 +448,15 @@ object ScopedGraphGenerator {

val unqualified = bindings.firstOrNull { it.qualifier == null }
val namedBindings = bindings.mapNotNull { binding ->
val qualifier = binding.qualifier
if (qualifier is Qualifier.Named) {
qualifier.value to binding
} else {
null
when (val qualifier = binding.qualifier) {
is Qualifier.Named -> qualifier.value to binding
else -> null
}
}
val typedBindings = bindings.mapNotNull { binding ->
when (val qualifier = binding.qualifier) {
is Qualifier.Custom -> qualifier.qualifiedName to binding
else -> null
}
}

Expand All @@ -465,22 +473,60 @@ object ScopedGraphGenerator {
endControlFlow()
}

if (namedBindings.isNotEmpty()) {
beginControlFlow("if (qualifier is %T)", namedClass)
beginControlFlow("when (qualifier.value)")
for ((name, binding) in namedBindings) {
addStatement("%S -> return %L as T", name, dependencyAccess(plan.scope, binding))
if (namedBindings.isNotEmpty() || typedBindings.isNotEmpty()) {
beginControlFlow("when (qualifier)")
if (namedBindings.isNotEmpty()) {
beginControlFlow("is %T ->", namedClass)
beginControlFlow("when (qualifier.value)")
for ((name, binding) in namedBindings) {
addStatement(
"%S -> return %L as T",
name,
dependencyAccess(plan.scope, binding),
)
}
addStatement(
$$"else -> error(\"Binding with type '${type.simpleName}' has no qualifier with name '${qualifier.value}'\")",
)
endControlFlow()
endControlFlow()
}
addStatement($$"else -> error(\"Binding with type '${type.simpleName}' has no qualifier with name '${qualifier.value}'\")")
endControlFlow()

if (typedBindings.isNotEmpty()) {
beginControlFlow("is %T ->", typedClass)
beginControlFlow("when (qualifier.value)")
for ((qualifiedName, binding) in typedBindings) {
addStatement(
"%T::class -> return %L as T",
ClassName.bestGuess(qualifiedName),
dependencyAccess(plan.scope, binding),
)
}
addStatement(
$$"else -> error(\"Binding with type '${type.simpleName}' has no qualifier with type '${qualifier.value.qualifiedName}'\")",
)
endControlFlow()
endControlFlow()
}

addStatement(
$$"else -> error(\"Binding with type '${type.simpleName}' has no matching qualifier '$qualifier'\")",
)
endControlFlow()
} else {
addStatement(
$$"error(\"Binding with type '${type.simpleName}' has no matching qualifier '$qualifier'\")",
)
}
addStatement($$"error(\"Binding with type '${type.simpleName}' has no matching qualifier '$qualifier'\")")

endControlFlow()
}
}
}
addStatement($$"else -> error(\"Binding with type '${type.simpleName}' is not found in $currentScope scope and its ancestors.\")")

addStatement(
$$"else -> error(\"Binding with type '${type.simpleName}' is not found in $currentScope scope and its ancestors.\")",
)
endControlFlow()
}.build()
}
Expand All @@ -494,6 +540,11 @@ object ScopedGraphGenerator {
when (val qualifier = binding.qualifier) {
null -> Unit

is Qualifier.Custom -> {
append("_custom_")
append(sanitizeName(qualifier.qualifiedName))
}

is Qualifier.Named -> {
append("_named_")
append(sanitizeName(qualifier.value))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,14 @@ sealed class Qualifier {

abstract fun encode(): String

data class Custom(val qualifiedName: String) : Qualifier() {
override fun encode(): String = "Custom:$qualifiedName"
override fun toString(): String = qualifiedName
}

data class Named(val value: String) : Qualifier() {
override fun encode(): String = "Named:$value"
override fun toString(): String = "Named '$value'"
}

companion object {
Expand All @@ -118,6 +124,7 @@ sealed class Qualifier {
error("Should not happen")
}
return when {
parts[0] == "Custom" -> Custom(parts[1])
parts[0] == "Named" -> Named(parts[1])
else -> error("Should not happen")
}
Expand Down
Loading
Loading