Yatagan DI Framework

repository·main·Indexed 42 days ago

https://github.com/yandex/yatagan

A high-performance Dependency Injection framework built on the Google Dagger2 API. It optimizes build speeds in large projects by reducing annotation processing scope and supports dynamic dependency graphs via runtime Conditions and Component Variants. It offers multiple backends, including KSP, KAPT/APT, and a Reflection backend for fast local debug builds.

Tokens
10.5K
Snippets
23
Records
41
Agent score
53%

What's inside Yatagan

  1. How Yatagan improves build performance

    main

    Yatagan achieves faster builds (potentially up to 2x faster incremental builds) by reducing the scope of annotation processing.

    In a standard Dagger setup, every module with DI code requires an annotation processor. In Yatagan, you only apply processing to modules containing root components (@Component(isRoot = true)).

    Optimization Strategy:

    • Apply Yatagan processing only to root-component modules.
    • For all other modules, use only the api-public dependency (no KAPT/KSP).
    • In debug environments, use the Reflection backend to remove KAPT/KSP from the build pipeline entirely in many modules.
  2. How condition evaluation and caching works

    main

    To ensure consistency across the component hierarchy, Yatagan follows these evaluation rules:

    • Single Evaluation: Each unique condition (boolean variable) is evaluated only once per component hierarchy. The value is cached and reused.
    • Provider Lifecycle: Condition provider classes are queried from the graph like standard dependencies. If a provider is used in multiple branches, it is queried and cached in the highest (closest to root) component.
    • Static vs Non-static:
      • If a condition is accessible via a static context, Yatagan queries it directly (potentially during component construction).
      • For non-static conditions, evaluation is lazy: it occurs strictly upon the first construction of a conditional binding that uses that condition.
  3. How dynamic optional dependencies work in Yatagan

    main

    Yatagan provides first-class support for dynamic optional dependencies, which are dependencies that may or may not be present in the DI graph based on runtime conditions. This is more maintainable and scalable than manually writing @Provides methods for Optional<T> in Dagger.

    Example of using an optional dependency in a class constructor:

    @Singleton
    class MyClass @Inject constructor(
        /**
         * Dynamic optional dependency, that is present/absent in DI-graph based on declared runtime condition.
         */
        val myOptionalDependency: Optional<MyClassUnderRuntimeCondition>,
    )
  4. Extend Yatagan via Validation Plugins

    main

    You can implement custom graph inspections by writing a plugin for Yatagan's validation pipeline. This is achieved via the Service Provider Interface (SPI), similar to Dagger.

    Constraints:

    • Plugins cannot perform additional code generation.
    • Plugins cannot modify the graph being inspected.
  5. Use Conditional Bindings and Variants API

    main

    Yatagan provides APIs for conditional logic and component variations, which can replace Dagger's @BindsOptionalOf and SPI-based models.

    Conditional Bindings

    Use @ConditionExpression to declare a runtime condition. Then, apply the @Conditional annotation to a binding or a class with an @Inject-annotated constructor to determine if that binding should be present.

    Variants API

    Similar to Android flavors, you can declare components with specific variants. This allows you to include or exclude bindings based on the component's variant.

    • Use @Component(variant = ...) to define the variant on a component.
    • Use @Conditional(.., onlyIn = ...) to restrict a binding to a specific variant.
  6. Understand Yatagan backends

    main

    Yatagan provides three primary backends that are designed to behave identically, allowing you to switch between them based on your environment:

    1. KAPT/APT (processor-jap): The classic, stable code generation mode. Use this for mixed Java/Kotlin projects or when stability is the priority.
    2. KSP (processor-ksp): A modern code generation mode leveraging Google's KSP. It works best for pure-Kotlin projects. Because Yatagan operates on the Java type system, KSP support for modeling Java code can be inconsistent.
    3. Reflection (api-dynamic): A runtime backend designed for extremely fast local debug builds. It eliminates the need for annotation processing in many modules.
      • Warning: Reflection mode is broken by code minifiers like Proguard or R8 and should generally not be used in production.
  7. How runtime conditions work with @ConditionExpression and @Conditional

    main

    Yatagan provides native support for runtime conditions to prevent accidental direct injection of classes that should only be available under specific features.

    1. Declare a feature: Use @ConditionExpression to define a boolean expression that queries a condition provider class.
    2. Mark a binding: Use @Conditional on a class or binding to indicate it is only available when the declared feature is active.

    If you attempt to inject a @Conditional class without using a com.yandex.yatagan.Optional wrapper, Yatagan will throw an error. However, you can inject the class directly into other bindings that are protected by the same or a stricter condition.

    // 1. Declare the feature via an annotation
    @ConditionExpression("getMyFeature.isEnabled", Features::class)
    annotation class MyFeature
    
    // 2. Mark the class as conditional
    @Conditional(MyFeature::class)
    class MyClass @Inject constructor()
  8. Annotation retention requirements for Reflection

    main

    When using the reflection backend, annotations used for Scopes, Qualifiers, or other DI metadata must have RUNTIME retention.

    If an annotation has non-runtime retention, it will work with code generation backends but will fail silently or cause inconsistencies with the reflection backend. To ensure compatibility between both modes, Yatagan's code generation backends will report non-runtime retention as an error/warning to maintain parity with reflection's requirements.

  9. Migrate @Provides annotations from 1.x to 2.0

    main

    In Yatagan 2.0, @Provides is a plain marker and no longer accepts Conditional arguments directly. You must move each Conditional(...) argument into its own separate @Conditional(...) annotation on the same method.

    Migration Rules:

    • Positional arguments: @Provides(Conditional(A::class)) $\rightarrow$ @Provides\n@Conditional(A::class).
    • Multiple arguments: @Provides(Conditional(A::class), Conditional(B::class)) $\rightarrow$ repeated @Conditional annotations.
    • Named/Array arguments: @Provides(value = [Conditional(A::class)]) $\rightarrow$ repeated @Conditional annotations.
    • Conditionals(...) container arguments inside @Provides should be handled similarly (moved to separate @Conditional annotations).
    • Note: The class-level @Conditionals annotation is unchanged and should not be touched.

    Formatting: After hoisting annotations, check your project's line-length limits. Adding @ symbols or dedenting multi-line arguments may push lines over the limit.

    // 1.x
    @Provides(Conditional(MyFeature::class))
    fun provideImpl(): MyApi = MyApiImpl()
    
    // 2.0
    @Provides
    @Conditional(MyFeature::class)
    fun provideImpl(): MyApi = MyApiImpl()
  10. Migrate from Yatagan 1.x to 2.0

    main

    Migrating to Yatagan 2.0 involves several breaking changes regarding how conditions are handled, how child components are injected, and how the reflection backend is configured.

    Unchanged APIs

    The following elements remain unchanged and do not require modification:

    • Yatagan.builder(), autoBuilder(), and create() call sites.
    • @Component, @Module, @Binds, @BindsInstance.
    • @IntoList, @IntoSet, @IntoMap, @Multibinds.
    • @AssistedInject, @AssistedFactory.
    • Lazy<T>, Optional<T>, Provider<T>.
    • Scopes.
    • @Conditional and @Conditionals on classes.
    • Compiler markers: com.yandex.yatagan.ConditionsApi and com.yandex.yatagan.VariantApi.

    Migration Steps

    1. Bump the version to 2.0.
    2. Code Migrations:
      • @Provides: No longer accepts Conditional arguments directly.
      • Legacy Conditions: Replace with @ConditionExpression.
      • Child Component Injection: Direct injection of child components is now forbidden. You must use a builder or a parent factory method (do not use both).
      • Thread Asserter: Yatagan.setThreadAsserter() is replaced by a build-time thread checker.
      • Reflection Backend: Programmatic setup is replaced by parameters.properties located in src/<sourceSet>/resources/META-INF/com.yandex.yatagan.reflection/parameters.properties.
    3. Build and Verify: Run the build check and triage errors.
  11. Verify Yatagan 2.0 Migration Completeness

    main

    A successful migration is not just a green build. To ensure full coverage, follow these verification steps:

    1. Exercise all code paths: A standard build check might skip flag-gated code. You must explicitly build variants that include:
      • Reflection-mode source sets.
      • R8/ProGuard processing.
      • Bytecode transforms that construct generated class names.
    2. Check for stale artifacts: Do not assume missing Yatagan$... files are errors if they belong to other variants' output directories; they may be leftovers from the pre-migration build.
    3. Confirm build directory location: If <module>/build is missing, check the actual build directory using:
      ./gradlew -q :<module>:properties | grep buildDir

    Definition of Done:

    • Build is green.
    • Zero Yatagan deprecation warnings remain.
    • Every item from your Step 0 inventory is either migrated or confirmed absent via re-scan.
    • Every migrated area has been exercised by a build or explicitly reported as unverified.
  12. Configure Yatagan for Android projects

    main

    For Android, it is recommended to use the Reflection backend in debug builds to maximize build speed, and a codegen backend (KSP or KAPT) in release builds.

    Note on Reflection: Reflection mode requires minSdk = 24 to safely support static methods in interfaces without performance penalties from AGP desugaring. If your minSdk is lower, use reflection only for debug builds where the target is at least 24.

    // Use reflection in debug builds.
    debugApi("com.yandex.yatagan:api-dynamic:${yataganVer}")
    
    // Use codegen in releases
    releaseApi("com.yandex.yatagan:api-public:${yataganVer}")
    if (kspEnabled) {
        kspRelease("com.yandex.yatagan:processor-ksp:${yataganVer}")
    } else {
        kaptRelease("com.yandex.yatagan:processor-jap:${yataganVer}")
    }