Skip to content

v2.0.0-RC

Pre-release
Pre-release

Choose a tag to compare

@bacecek bacecek released this 14 Aug 09:27
· 1 commit to main since this release
f34a02c

Yatagan 2.0

Yatagan 2.0 is the first major release since 1.x. The runtime entry point is now backend-agnostic, there
is an experimental Dagger compatibility mode, and the Conditions API is cleaned up. Configuration that
used to live in global runtime state is now declarative, set at compile time or on the classpath.

This document describes changes relative to Yatagan 1.6.2.

Highlights

Unified, backend-agnostic entry point

com.yandex.yatagan.Yatagan now lives in api-public and serves both backends through an internal
ImplementationLoader SPI:

  • with only api-public/api-compiled on the classpath, generated implementations are loaded;
  • with api-dynamic on the classpath, the reflection backend is picked up automatically, but a generated
    implementation, if present, is still preferred. This lets libraries that ship pre-generated Yatagan
    components coexist with applications that use reflection mode.

Yatagan.builder(...), Yatagan.autoBuilder(...), and Yatagan.create(...) signatures are unchanged.
Existing call sites compile as-is.

Dagger compatibility mode (experimental)

Yatagan can now recognize Dagger annotations and generate Dagger<ComponentName> facades. A project that
uses only the supported core functionality can switch from Dagger to Yatagan without rewriting its call
sites. The mode is enabled with the yatagan.experimental.enableDaggerCompatibility processor option, or
the
enableDaggerCompatibility property for the reflection backend, which additionally requires the Dagger
API jar on the runtime classpath.

Yatagan supports the core Dagger functionality, including components, modules, bindings, scopes,
subcomponents, multibindings, and assisted injection. The following extensions are not supported:

  • Hilt;
  • dagger.android;
  • Dagger Producers;
  • Dagger-gRPC;
  • Dagger SPI plugins.

Within the supported core:

  • Dagger annotations and types with supported functionality (@dagger.Component, @dagger.Module,
    @dagger.Provides, dagger.Lazy, ...) are recognized alongside Yatagan ones.
  • Dagger and Yatagan annotations may be mixed in the same codebase. For component annotations, Yatagan's
    @Component takes priority over Dagger's when both are present on one declaration; avoid annotating a
    single declaration with both frameworks' annotations otherwise.
  • While the mode is enabled, a Dagger<ComponentName> facade class is generated for every root
    component, both Dagger- and Yatagan-annotated. This includes DaggerMyComponent.create() and
    DaggerMyComponent.builder()/.factory(); nested components use _, as in DaggerOuter_Inner.
  • Yatagan.* entry points remain available for components annotated with
    com.yandex.yatagan.Component.

For example, enable the mode with KSP as follows:

// build.gradle.kts
ksp {
    arg("yatagan.experimental.enableDaggerCompatibility", "enabled")
}

With the mode on, existing DaggerMyComponent.create() and DaggerMyComponent.builder() call sites work
against Yatagan-generated facades. The feature is experimental. Validate your graph before shipping.

Conditions API cleanup

The legacy @Condition/@AllConditions/@AnyCondition/@AnyConditions API is deprecated in favor of
@ConditionExpression. Conditional provisions move from @Provides(Conditional(...)) to a separate
@Conditional annotation on the method.

Declarative runtime configuration

Yatagan.setThreadAsserter() is now a deprecated no-op, and the programmatic reflection-backend setup API
is gone. They are replaced by the yatagan.threadCheckerClassName processor option and a
parameters.properties classpath resource, respectively.

Important changes

  • @Provides no longer accepts Conditional arguments.
  • Direct injection of child component instances is forbidden, and every abstract component method that
    returns a non-root component is now treated as a component factory method.
  • Generated component implementation names no longer use the $ separator. Normal Yatagan.* entry
    points and bundled ProGuard/R8 rules remain compatible with old generated names.
  • The reflection backend is discovered automatically and no longer has a programmatic setup or reset API.
  • Thread checking is configured at build time instead of through global mutable state.
  • DynamicValidationDelegate implementations must be updated for the new reflection configuration and
    reporting API.
  • The published SPI modules are source- and binary-incompatible with 1.6.2; plugins must be adapted and
    recompiled.
  • api-common is removed, api-compiled is deprecated, and two experimental processor options are
    removed.

Migration guide

An AI agent can apply most of this guide for you. The repository ships an agent skill at
skills/yatagan-2-migration/SKILL.md: it finds the affected call sites and applies the changes described
below. It then builds the project to check the result.

Install it into your project with skills.sh:

npx skills add yandex/yatagan

The rest of this section is the manual version of the same migration.

The following commonly used APIs are unchanged and need no migration:
Yatagan.builder()/autoBuilder()/create() call sites, @Component, @Module, @Binds,
@BindsInstance, @IntoList/@IntoSet/@IntoMap/@Multibinds, @AssistedInject/@AssistedFactory,
Lazy<T>, Optional<T>, Provider<T>, scopes, and @Conditional on classes.

Expand only the sections that apply to your project.

Common migrations

Conditional @Provides methods

Affected if: a @Provides annotation contains Conditional(...) arguments.

In 1.6.2, @Provides had a vararg value: Conditional parameter. In 2.0 it is a plain marker annotation.
@Conditional/@Conditionals can now target functions and property getters directly, so conditionals for
provisions are written as separate annotations on the same method:

// 1.6.2
@Module
object FeatureModule {
    @Provides(Conditional(AliceProEnabled::class))
    fun provideController(deps: Deps): ProController = ProController(deps)
}

// 2.0
@Module
object FeatureModule {
    @Provides
    @Conditional(AliceProEnabled::class)
    fun provideController(deps: Deps): ProController = ProController(deps)
}

This is a mechanical change; onlyIn = [...] and multiple/repeated @Conditional annotations work the
same way on methods as they do on classes. @Conditional usage on classes, such as
@Conditional(MyFeature::class) class Foo @Inject constructor(), is unchanged and requires no migration.

Child component injection and factory methods

Affected if: a parent graph injects a child component instance directly, or a parent component has an
abstract method that returns a non-root component.

In 1.6.2, if a child component did not declare a builder, its instance could be injected directly into
bindings of the parent graph. This behavior, which is not supported by vanilla Dagger, is removed.
Attempting it now produces Missing binding for ... with the note
"A dependency seems to be a child component, try injecting its factory instead."

Find injections of child component instances and replace them with builder injections:

// 1.6.2
@Component(isRoot = false)
interface SettingsComponent { /* entry points */ }

class SettingsRouter @Inject constructor(
    private val settings: Lazy<SettingsComponent>, // injected the component directly
)

// 2.0
@Component(isRoot = false)
interface SettingsComponent {
    /* entry points */

    @Component.Builder
    interface Builder {
        fun create(): SettingsComponent
    }
}

class SettingsRouter @Inject constructor(
    private val settingsBuilder: SettingsComponent.Builder,
) {
    private val settings by lazy { settingsBuilder.create() }
}

Also, every abstract method of a component interface that returns a non-root component is now
treated as a component factory method. In 1.6.2, only methods with arguments were treated as factories;
parameterless ones were treated as entry points.

As a consequence, the pre-existing error
"Child components can't have a factory methods declared for them in their parents, if they have an
explicit @Component.Builder declared"
now fires in more cases. A child component must expose either
an explicit @Component.Builder or factory methods in its parent, but not both. If the child declares an
explicit builder, remove the parent-side factory method, or drop the explicit builder.

Legacy conditions to @ConditionExpression

Affected if: the project uses @Condition, @AllConditions, @AnyCondition, or @AnyConditions.

These annotations are deprecated with the message
"Legacy Conditions API, use @ConditionExpression instead". They still work in 2.0, so this migration
can be done at your own pace.

object Features {
    @JvmStatic fun isAliceProEnabled(): Boolean = /* ... */ false
}

// legacy (deprecated in 2.0)
@Condition(Features::class, condition = "isAliceProEnabled")
annotation class AliceProEnabled

// modern
@ConditionExpression("isAliceProEnabled", Features::class)
annotation class AliceProEnabled

@ConditionExpression already exists in 1.6.2 with the same signature and expression syntax, so this
migration can be done before upgrading to 2.0; only the deprecation of the legacy annotations is new
in 2.0.

@ConditionExpression supports full boolean expressions in one annotation: &, |, !, feature
references (@OtherFeature), and multiple imports with optional aliases (importAs). This replaces
repeated @Condition/@AnyCondition stacking:

// legacy (CNF via repeated annotations)
@AnyCondition(
    Condition(Features::class, "isA"),
    Condition(Features::class, "isB"),
)
annotation class AOrB

// modern
@ConditionExpression("isA | isB", Features::class)
annotation class AOrB

Non-static condition providers, whose instances are resolved as regular graph dependencies, are supported
the same way as in 1.6.2.

Reflection backend migrations

Replace programmatic reflection-backend setup

Affected if: the project calls setupReflectionBackend(), resetReflectionBackend(), or methods on
the reflection Initializer API.

The Yatagan object shipped in api-dynamic, including setupReflectionBackend(),
resetReflectionBackend(), and the Initializer fluent API, is removed. The reflection backend is now
discovered automatically by the common com.yandex.yatagan.Yatagan entry point when api-dynamic is on
the runtime classpath. Configure it with this classpath resource:

META-INF/com.yandex.yatagan.reflection/parameters.properties

Supported keys are listed below. All are optional; unknown keys are an error.

Property Type Replaces (1.6.2 Initializer call)
validationDelegateClass FQN of a DynamicValidationDelegate impl with a public no-arg constructor .validation(...)
maxIssueEncounterPaths int .maxIssueEncounterPaths(...)
enableStrictMode boolean .strictMode(...)
usePlainOutput boolean (new)
enableDaggerCompatibility boolean (new)
threadCheckerClassName FQN of a thread checker class (new, see thread assertions)

For example:

// 1.6.2 — programmatic setup at app startup:
Yatagan.setupReflectionBackend()
    .validation(MyValidationDelegate())
    .maxIssueEncounterPaths(3)
    .strictMode(true)
    .apply()
# 2.0 — src/<sourceSet>/resources/META-INF/com.yandex.yatagan.reflection/parameters.properties
validationDelegateClass=com.example.MyValidationDelegate
maxIssueEncounterPaths=3
enableStrictMode=true

Removed without replacement:

  • resetReflectionBackend(): the engine now keeps a per-class-loader cache whose entries are softly
    referenced, so cached reflection data can be reclaimed under memory pressure; there is no manual reset
    API anymore. Delete the calls.
  • useCompiledImplementationIfAvailable(...): the unified entry point always prefers a generated
    implementation and falls back to reflection automatically.
  • reportDuplicateAliasesAsErrors(...): conflicting bindings are now always errors.
  • logger(...) / allConditionsLazy(...): there is no reflection-mode equivalent in 2.0. A logger can
    be supplied through a DynamicValidationDelegate implementation.

com.example.MyValidationDelegate must implement
com.yandex.yatagan.rt.support.DynamicValidationDelegate and have a public no-arg constructor. Since
setup is no longer programmatic, per-build-type source-set tricks for reflection initialization code can
be replaced by placing the properties file in the desired source set, such as debug.

The reflection parameters are loaded asynchronously on a background daemon thread, so backend
initialization no longer blocks on classpath I/O.

Replace Yatagan.setThreadAsserter()

Affected if: the project calls Yatagan.setThreadAsserter(...) or sets
yatagan.experimental.omitThreadChecks.

The global mutable thread asserter is gone. Yatagan.setThreadAsserter(...) still exists for ABI
compatibility, but it is @Deprecated(level = ERROR) and does nothing. Thread checking for
single-threaded components is now configured at build time with the
yatagan.threadCheckerClassName processor option. For the reflection backend, use the
threadCheckerClassName property in parameters.properties.

// 1.6.2 — somewhere early in app startup:
Yatagan.setThreadAsserter {
    check(Looper.myLooper() == Looper.getMainLooper()) { "Not on the main thread!" }
}
// 2.0 — declare a checker class...
package com.example

object MainThreadChecker {
    @JvmStatic
    fun assertThreadAccess() {
        check(Looper.myLooper() == Looper.getMainLooper()) { "Not on the main thread!" }
    }
}
// ...and pass its name to the processor (build.gradle.kts, KAPT example):
kapt {
    arguments {
        arg("yatagan.threadCheckerClassName", "com.example.MainThreadChecker")
    }
}

The specified class must have a method named assertThreadAccess that is:

  • public or internal;
  • static, or a member of a Kotlin object;
  • parameterless.

With code-generating backends, these requirements are validated at compile time. Violations are reported
as errors on the option value, such as "Invalid value: Unable to find class ...", "... must be
public/internal"
, "... must be static or be in a Kotlin object", or "... must have no parameters".
With the reflection backend, the same validation happens at runtime when the first component is created
and is reported through the configured DynamicValidationDelegate.

For the reflection backend, set
threadCheckerClassName=com.example.MainThreadChecker in parameters.properties. Then delete all
Yatagan.setThreadAsserter(...) calls.

The related experimental option yatagan.experimental.omitThreadChecks is removed. If you do not need
thread checks, do not set yatagan.threadCheckerClassName.

Update DynamicValidationDelegate

Affected if: the project implements
com.yandex.yatagan.rt.support.DynamicValidationDelegate.

  • Implement the new val logger: Logger? property. It replaces the removed
    Initializer.logger(...) setup call.
  • Change ReportingDelegate.reportError(...)/reportWarning(...) parameters from
    com.yandex.yatagan.validation.RichString to String.
  • Give the implementation class a public no-argument constructor so it can be used through
    validationDelegateClass in parameters.properties.

Build and integration migrations

Update generated component names, keep rules, and reflection lookups

Affected if: the project refers directly to generated Yatagan implementation names in custom
ProGuard/R8 rules, reflection lookups, baseline profiles, serialized class names, or tooling.

Generated implementation class names no longer use the $ separator:

Component 1.6.2 implementation 2.0 implementation
MyComponent (top-level) Yatagan$MyComponent YataganMyComponent
Outer.Inner (nested) Yatagan$Outer$Inner YataganOuter_Inner

Search the build for the Yatagan$ pattern and update direct references to the new scheme:
YataganMyComponent and YataganOuter_Inner.

If you only use Yatagan.builder()/Yatagan.autoBuilder()/Yatagan.create(), no action is needed:

  • the loader in 2.0 falls back to legacy Yatagan$... class names, so component implementations generated
    by older Yatagan versions still load, including code generated with 1.1.x and earlier that uses the
    create convention;
  • consumer ProGuard/R8 rules shipped inside api-public at META-INF/proguard/yatagan.pro keep both the
    new **.Yatagan* and legacy **.Yatagan$* names.
Update artifacts

Affected if: the build depends on api-common or api-compiled.

  • The api-common artifact, which contained internal implementation-loading utilities, is removed. Its
    functionality was absorbed by api-public; depend on api-public instead.
  • api-compiled is now an alias that re-exports api-public. It is deprecated and will be removed in a
    future release
    . Migrate dependencies to com.yandex.yatagan:api-public for code-generation setups.
Update compiler options

Affected if: the build sets yatagan.experimental.omitThreadChecks or
yatagan.experimental.reportDuplicateAliasesAsErrors.

1.6.2 option 2.0 action
yatagan.experimental.omitThreadChecks Remove; do not set yatagan.threadCheckerClassName if checks are not needed
yatagan.experimental.reportDuplicateAliasesAsErrors Remove; the check is always on and reports an error
New: yatagan.threadCheckerClassName (string)
New: yatagan.experimental.enableDaggerCompatibility (boolean, default off)

All other options (yatagan.enableStrictMode, yatagan.usePlainOutput, yatagan.maxIssueEncounterPaths,
yatagan.experimental.allConditionsLazy, yatagan.experimental.omitProvisionNullChecks,
yatagan.experimental.maxSlotsPerSwitch) are unchanged.

Update SPI and validation plugins

Affected if: the project implements a ValidationPluginProvider or otherwise consumes the published
SPI modules.

The published SPI modules (lang-api, core-model-api, core-graph-api, validation-api,
validation-spi) are source- and binary-incompatible with 1.6.2. Validation plugins and other SPI
consumers must be recompiled and adapted.

Notable changes:

  • ComponentModel.modules changed from Set<ModuleModel>, containing all transitive modules, to
    List<ModuleModel>, containing directly included modules only. The full transitive set is available as
    the new ComponentModel.allModules property.
  • SubComponentBinding was renamed to SubComponentFactoryBinding, and
    Binding.Visitor.visitSubComponent to visitSubComponentFactory.
  • lang-api was reworked around a new LexicalScope concept, replacing the ObjectCache-based
    machinery, and the Extensible API was reworked.
  • Parts of the API are now explicitly annotated @Internal/@Incubating and may change without notice.

The checked-in .api files are the reference for these surfaces.