Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scope annotation doesn't work on a provider functions #464

Open
terrakok opened this issue Feb 9, 2025 · 8 comments
Open

Scope annotation doesn't work on a provider functions #464

terrakok opened this issue Feb 9, 2025 · 8 comments

Comments

@terrakok
Copy link

terrakok commented Feb 9, 2025

Maybe I missed something, but the code:

@Scope
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER)
annotation class AppSingleton

@AppSingleton
@Component
abstract class AppComponent {
    @AppSingleton
    abstract fun provideAuthService(): AuthService
}

generates a regular getter:

 override fun provideAuthService(): AuthService = AuthService(...)

but if I mark the AuthService class

@Inject
@AppSingleton
class AuthService(...) {...}

it works as expected

override fun provideAuthService(): AuthService =
      _scoped.get("com.package.AuthService") { AuthService(...) }
@malianto
Copy link

You're right!

In the code example, the provideAuthService function is missing the @Provides annotation. According to the documentation, when creating a scoped provider, you need both the scope annotation (@AppSingleton in your case) and @Provides annotation on the provider function.

The correct code should be:

@AppSingleton
@Component
abstract class AppComponent {
    @AppSingleton
    @Provides // <-- Required annotation
    abstract fun provideAuthService(): AuthService
}

@terrakok
Copy link
Author

It doesn't help because @Provide annotation cannot be applied to an abstract function

@tiper
Copy link

tiper commented Feb 21, 2025

Hey @terrakok

I believe that might be the intended behavior or a known limitation. When compiling, you should see a KSP warning about it, something like:

[ksp] Scope: @AppSingleton has no effect. Place on @Provides function or @Inject constructor instead.

@terrakok
Copy link
Author

My cases are:

  1. I'd like to manage scopes in the component only
  2. I'd like to have the same class instances with different lifecycles

@tiper
Copy link

tiper commented Feb 22, 2025

You might be able to achieve your use case using typealias. Something like:

typealias SingletonAuthService = AuthService
typealias NonSingletonAuthService = AuthService

@Provides
@AppSingleton
fun provideSingletonAuthService(service: AuthService): SingletonAuthService = service

@Provides
fun provideNonSingletonAuthService(service: AuthService): NonSingletonAuthService = service

Then, in your consumers, you just need to use the typealias so they receive the expected scoped instance.

MyClass1(auth: SingletonAuthService)
MyClass2(auth: NonSingletonAuthService)

Hope this helps!

@terrakok
Copy link
Author

Yes, thank you for the idea. but it seems like a boilerplate. I wish the DI framework would help with that

@tiper
Copy link

tiper commented Feb 22, 2025

I'm not sure if my example was misleading, but you don’t actually need typealias NonSingletonAuthService = AuthService. I only included it to make the explanation clearer.

In your case, the only thing you really need is:

  • Adding the singleton typealias
  • Providing it with a @Provides function
  • Using that typealias in the classes that require the singleton instance
typealias SingletonAuthService = AuthService

@Provides
@AppSingleton
fun provideSingletonAuthService(service: AuthService): SingletonAuthService = service

Everything else can most likely stay as it is.

If you're familiar with Dagger, it's a similar approach to using a custom annotation or the @Named annotation, but in Kotlin-Inject, typealias serves that purpose instead.

To me, it doesn't really feel like boilerplate 😅. What kind of approach do you think would help reduce this even more?

@bddckr
Copy link

bddckr commented Feb 22, 2025

tl;dr: It seems you're confusing abstract properties/functions in components which are for retrieval with the side that provides instances. Scope annotations only seem to be supported at the providing side.

I'm new to kotlin-inject, but my understanding is that

  • we either write a method with @Provides to return an instance ourselves,
  • or we don't provide it ourselves by letting it up to the generated code we get - by annotation the class with @Inject.

That's for providing instances.

We then use abstract properties or functions in components to retrieve instances.

The example in the original post is confusing (to me at least), because it's using an abstract function to retrieve instances while at the same time using the provide method naming pattern.

With that out of the way: Scope annotations seem to be supported only at the providing side, not the retrieving side.

So...

If you're in control of the class to inject, you

  1. Annotate it with @Inject.
  2. Annotate it with the scope annotation @AppSingleton.

That's it.

Alternatively you must provide the instance yourself, so you

  1. Write an @Provides method in which you return an instance of that class that you instantiate yourself somehow.
  2. Annotate this method with the scope annotation @AppSingleton.

If anything here's wrong, I apologize in advance. Using this to fact-check my understanding 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants