v2.0.0-RC
Pre-releaseYatagan 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-compiledon the classpath, generated implementations are loaded; - with
api-dynamicon 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
@Componenttakes 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 includesDaggerMyComponent.create()and
DaggerMyComponent.builder()/.factory(); nested components use_, as inDaggerOuter_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
@Providesno longer acceptsConditionalarguments.- 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. NormalYatagan.*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.
DynamicValidationDelegateimplementations 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-commonis removed,api-compiledis 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/yataganThe 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 AOrBNon-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=trueRemoved 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 aDynamicValidationDelegateimplementation.
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:
publicorinternal;static, or a member of a Kotlinobject;- 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.RichStringtoString. - Give the implementation class a public no-argument constructor so it can be used through
validationDelegateClassinparameters.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
createconvention; - consumer ProGuard/R8 rules shipped inside
api-publicatMETA-INF/proguard/yatagan.prokeep both the
new**.Yatagan*and legacy**.Yatagan$*names.
Update artifacts
Affected if: the build depends on api-common or api-compiled.
- The
api-commonartifact, which contained internal implementation-loading utilities, is removed. Its
functionality was absorbed byapi-public; depend onapi-publicinstead. api-compiledis now an alias that re-exportsapi-public. It is deprecated and will be removed in a
future release. Migrate dependencies tocom.yandex.yatagan:api-publicfor 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.moduleschanged fromSet<ModuleModel>, containing all transitive modules, to
List<ModuleModel>, containing directly included modules only. The full transitive set is available as
the newComponentModel.allModulesproperty.SubComponentBindingwas renamed toSubComponentFactoryBinding, and
Binding.Visitor.visitSubComponenttovisitSubComponentFactory.lang-apiwas reworked around a newLexicalScopeconcept, replacing theObjectCache-based
machinery, and theExtensibleAPI was reworked.- Parts of the API are now explicitly annotated
@Internal/@Incubatingand may change without notice.
The checked-in .api files are the reference for these surfaces.