Skip to content

Commit

Permalink
Add more docs about technical design (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
ZacSweers authored Jul 23, 2024
1 parent 088e90e commit b6eb756
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 0 deletions.
115 changes: 115 additions & 0 deletions FORK.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,118 @@ In the medium term, I plan to tune the gradle plugin to be able to consume this
easily and have the gradle plugin automatically replace dependencies with the fork's.

Long term, this will ideally move back upstream to Anvil main.

## Technical Design

In K1, Anvil contribution merging worked by _modifying_ the original component during IR
transformation.

This was convenient and allowed for a fairly simple implementation. However, this had two major
issues:

1. This is no longer possible in K2. We didn't learn of this until K2 testing, as the APIs we used
in IR were not annotated as obsolete. This means that the original Anvil implementation is not
compatible with K2.

- Annotation generation in kapt stub generation still sort of works but is not recommended nor is
it
compatible with IC. The Kotlin compiler team advised migrating to a supported API in FIR
instead.
- Interface merging completely breaks in K2, as new interfaces added are quietly ignored. The
Kotlin
compiler team also declined to support such a case in the future.

2. This was not compatible with dagger-KSP, for various reasons.

- In K1/KSP1, KSP never runs the IR backend, so Anvil never has a chance to perform merging.
- In K2/KSP2, KSP never even runs a compiler plugin, so Anvil never has a chance to perform
merging.

Rewriting the merging logic to run in KSP solves both of these issues, but required rethinking the
structure of generated code to work in a world where we cannot modify sources.

### "Merged" Classes

To solve this, we now generate a new class that acts as a "merged" version of the original
component. This class is more or less a source representation what the IR-transformed class would
have looked like in K1. From here, different things are done depending on the type of component and
if you're using dagger-ksp or kapt.

### Components

For components, we generate a new interface that extends the original component and includes all the
contributed modules and interfaces. This merged component is then what is processed by Dagger. If
running in kapt, kapt will just see this as any other source and process it as normal.

If running dagger-ksp, KSP's support for multiple rounds means that dagger's processor will see this
generated file in a subsequent round and process it then.

![ksp](docs/img_ksp_merging.png)

To ease adoption, we generate source-compatible shims for the original component name. I.e., you can
still write `DaggerAppComponent.factory()` like before, rather than needing to know about the new
`DaggerMergedAppComponent`.

### Subcomponents

Subcomponents largely work the same, but with small extra additions. Namely, they automatically
include a binding module that binds the `MergedExampleSubcomponent` to the original
`@ExampleSubcomponent` and expose a `ParentComponent` interface that exposes either its creator
factory or the subcomponent itself to consuming component classes. During component merging, Anvil
KSP automatically includes and implements these as needed.

`@ContributesSubcomponent` is implemented on top of this system.

### `@MergeModules`

These work largely the same as the above, except that the generated module obviously doesn't extend
the original module. Instead, Anvil KSP automatically includes the generated merged module when
including the original `@MergeModules`-annotated module.

**NOTE**: If you include a `@MergeModules`-annotated module in a non-anvil component, you must also
manually include the merged module in the component now.

### `@MergeInterfaces`

These also work the same as above. Similar to `@MergeModules`, these will automatically be included
by Anvil KSP when including the original `@MergeInterfaces`-annotated interface, but you will need
to manually include the merged interface in any components or interfaces that aren't processed by
Anvil KSP.

### A note about subcomponent factory methods

One special case that Anvil KSP has to support is a component that exposes a subcomponent factory
method (note: _not_ the subcomponent's `@Subcomponent.Factory` creator).

```kotlin
@MergeComponent(...)
interface AppComponent {
fun userComponent(): UserComponent
}

@MergeSubcomponent(...)
interface UserComponent
```

In this case, Anvil KSP makes this system work by _implementing_ the `userComponent` function in the
merged component to just invoke the contributed subcomponent `ParentComponent` function. This is
also why Anvil KSP
requires `-Xjvm-default` to be set to `all` or `all-compatibility`, as it relies on Dagger's support
of
`default` methods.

```kotlin
// Generated by Anvil
@Component
interface MergedAppComponent : AppComponent, UserComponent.ParentComponent {
// This is implemented to default to the contributed subcomponent's parent component
override fun userComponent(): UserComponent = createUserComponent()
}

@Subcomponent
interface MergedUserComponent : UserComponent {
interface ParentComponent {
fun createUserComponent(): MergedUserComponent
}
}
```
Binary file added docs/img_ksp_merging.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit b6eb756

Please sign in to comment.