Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Changelog
Unreleased
----------

- Fix `@AssistedFactory` code gen for Metro to use standard nested class semantics.

0.29.0
------

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,8 @@ import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSDeclaration
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.KSValueParameter
import com.google.devtools.ksp.symbol.Visibility
import com.slack.circuit.codegen.CodegenMode.KOTLIN_INJECT_ANVIL
import com.slack.circuit.codegen.CodegenMode.METRO
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FileSpec
Expand Down Expand Up @@ -427,7 +425,7 @@ private class CircuitSymbolProcessor(
val creatorOrConstructor: KSFunctionDeclaration?
val targetClass: KSClassDeclaration
if (isAssisted) {
if (codegenMode == KOTLIN_INJECT_ANVIL || codegenMode == METRO) {
if (codegenMode == KOTLIN_INJECT_ANVIL) {
creatorOrConstructor = injectableConstructor
targetClass = declaration
} else {
Expand Down Expand Up @@ -519,46 +517,6 @@ private class CircuitSymbolProcessor(
constructorParams.add(ParameterSpec.builder("factory", factoryLambda).build())
CodeBlock.of("factory(%L)", assistedParams)
}
METRO -> {
val assistedFactory =
declaration.declarations.filterIsInstance<KSClassDeclaration>().find { nestedClass
->
nestedClass.isAnnotationPresentWithLeniency(
codegenMode.runtime.assistedFactory!!
)
}
requireNotNull(assistedFactory) {
"No assisted factory found for ${declaration.qualifiedName?.asString()}"
}
val constructorAssistedParameters =
assistedKSParams.map { it.toAssistedParameterType("factory") }
val assistedFactoryCreate =
assistedFactory.getAllFunctions().find { assistedFactoryFunction ->
val assistedFunctionParameters =
assistedFactoryFunction.parameters
.filter { it.isAnnotationPresentWithLeniency(codegenMode.runtime.assisted) }
.map { it.toAssistedParameterType("factory") }
val assistedParamsMatch =
constructorAssistedParameters == assistedFunctionParameters
val numberOfFunctionParamsMatch =
constructorAssistedParameters.size == assistedFactoryFunction.parameters.size

assistedParamsMatch && numberOfFunctionParamsMatch
}
requireNotNull(assistedFactoryCreate) {
"No assisted factory create function found " +
"for ${declaration.qualifiedName?.asString()}"
}

constructorParams.add(
ParameterSpec.builder("factory", assistedFactory.toClassName()).build()
)
CodeBlock.of(
"factory.%L(%L)",
assistedFactoryCreate.simpleName.getShortName(),
assistedParams,
)
}
else -> {
constructorParams.add(
ParameterSpec.builder("factory", declaration.toClassName()).build()
Expand All @@ -582,9 +540,6 @@ private class CircuitSymbolProcessor(

private data class AssistedType(val factoryName: String, val type: TypeName, val name: String)

private fun KSValueParameter.toAssistedParameterType(factoryName: String) =
AssistedType(factoryName = factoryName, name = name!!.getShortName(), type = type.toTypeName())

/**
* Returns a [CodeBlock] representation of all named assisted parameters on this
* [KSFunctionDeclaration] to be used in generated invocation code.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ internal enum class CodegenMode {
/**
* The Anvil Codegen mode
*
* This mode annotates generated factory types with [ContributesMultibinding], allowing for Anvil
* This mode annotates generated factory types with `ContributesMultibinding`, allowing for Anvil
* to automatically wire the generated class up to Dagger's multibinding system within a given
* scope (e.g. AppScope).
*
Expand Down Expand Up @@ -60,7 +60,7 @@ internal enum class CodegenMode {
* The Hilt Codegen mode
*
* This mode provides an additional type, a Hilt module, which binds the generated factory, wiring
* up multibinding in the Hilt DI framework. The scope provided via [CircuitInject] is used to
* up multibinding in the Hilt DI framework. The scope provided via `CircuitInject` is used to
* define the dagger component the factory provider is installed in.
*
* ```kotlin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1483,14 +1483,15 @@ class CircuitSymbolProcessorTest {
import dev.zacsweers.metro.Inject

@Inject
@CircuitInject(FavoritesScreen::class, AppScope::class)
class FavoritesPresenter(
@Assisted private val navigator: Navigator
) : Presenter<FavoritesScreen.State> {
@CircuitInject(FavoritesScreen::class, AppScope::class)
@AssistedFactory
fun interface Factory {
fun create(@Assisted navigator: Navigator): FavoritesPresenter
}

@Composable
override fun present(): FavoritesScreen.State {
throw NotImplementedError()
Expand Down Expand Up @@ -1531,53 +1532,6 @@ class CircuitSymbolProcessorTest {
)
}

@Test
fun invalidAssistedInjection_metro() {
assertProcessingError(
sourceFile =
kotlin(
"FavoritesPresenter.kt",
"""
package test

import com.slack.circuit.codegen.annotations.CircuitInject
import com.slack.circuit.runtime.Navigator
import com.slack.circuit.runtime.presenter.Presenter
import androidx.compose.runtime.Composable
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.Inject

@Inject
@CircuitInject(FavoritesScreen::class, AppScope::class)
class FavoritesPresenter(
@Assisted private val navigator: Navigator
) : Presenter<FavoritesScreen.State> {

// No AssistedFactory

@Composable
override fun present(): FavoritesScreen.State {
throw NotImplementedError()
}
}
"""
.trimIndent(),
),
codegenMode = CodegenMode.METRO,
) { messages ->
assertThat(messages).contains("No assisted factory found")
}
}

private enum class CodegenMode {
ANVIL,
HILT,
KOTLIN_INJECT_ANVIL,
METRO,
}

private fun assertGeneratedFile(
sourceFile: SourceFile,
generatedFilePath: String,
Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ kotlin-plugin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", versio
kotlin-plugin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" }
metro = { id = "dev.zacsweers.metro", version = "0.4.0" }
roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" }
skie = { id = "co.touchlab.skie", version.ref = "skie" }
spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
Expand Down
3 changes: 2 additions & 1 deletion samples/bottom-navigation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ android {
namespace = "com.slack.circuit.sample.navigation"
defaultConfig {
minSdk = 21
targetSdk = 36
// TODO update once robolectric supports 36
targetSdk = 35
}
testOptions { unitTests { isIncludeAndroidResources = true } }
}
Expand Down
3 changes: 2 additions & 1 deletion samples/interop/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ android {
namespace = "com.slack.circuit.sample.interop"
defaultConfig {
minSdk = 28
targetSdk = 36
// TODO update once robolectric supports 36
targetSdk = 35
versionCode = 1
versionName = "1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
Expand Down
2 changes: 1 addition & 1 deletion samples/star/apk/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ plugins {
android {
namespace = "com.slack.circuit.sample.star.apk"
defaultConfig {
minSdk = 28
minSdk = 30
targetSdk = 36
versionCode = 1
versionName = "1"
Expand Down
30 changes: 3 additions & 27 deletions samples/star/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,29 +1,24 @@
// Copyright (C) 2022 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
import com.google.devtools.ksp.gradle.KspAATask
import java.util.Locale
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.compose)
alias(libs.plugins.kotlin.plugin.compose)
alias(libs.plugins.agp.library)
alias(libs.plugins.kotlin.kapt)
alias(libs.plugins.kotlin.plugin.parcelize)
alias(libs.plugins.kotlin.plugin.serialization)
alias(libs.plugins.roborazzi)
alias(libs.plugins.anvil)
alias(libs.plugins.ksp)
alias(libs.plugins.sqldelight)
alias(libs.plugins.emulatorWtf)
alias(libs.plugins.metro)
}

anvil { kspContributingAnnotations.add("com.slack.circuit.codegen.annotations.CircuitInject") }

kotlin {
jvm()
androidTarget {
Expand Down Expand Up @@ -94,17 +89,12 @@ kotlin {
}
maybeCreate("jvmCommonMain").apply {
dependencies {
api(libs.anvil.annotations)
api(libs.anvil.annotations.optional)
implementation(libs.compose.material.icons)
implementation(libs.dagger)
implementation(libs.jsoup)
implementation(libs.coil.network.okhttp)
implementation(libs.ktor.client.engine.okhttp)
implementation(libs.okhttp)
implementation(libs.okhttp.loggingInterceptor)
val kapt by configurations.getting
kapt.dependencies.addLater(libs.dagger.compiler)
}
}
maybeCreate("jvmCommonTest").apply {
Expand Down Expand Up @@ -210,7 +200,7 @@ android {
namespace = "com.slack.circuit.star"

defaultConfig {
minSdk = 28
minSdk = 30
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testApplicationId = "com.slack.circuit.star.apk.androidTest"
}
Expand Down Expand Up @@ -248,21 +238,7 @@ fun String.capitalizeUS() = replaceFirstChar {

val kspTargets = kotlin.targets.names.map { it.capitalizeUS() }

// Workaround for https://youtrack.jetbrains.com/issue/KT-59220
afterEvaluate {
for (target in kspTargets) {
if (target != "Android" && target != "Jvm") continue
val buildType = if (target == "Android") "Release" else ""
val kspTaskName = "ksp${buildType}Kotlin${target}"
val generatedKspKotlinFiles =
tasks.named<KspAATask>(kspTaskName).flatMap { it.kspConfig.kotlinOutputDir }
tasks.named<KotlinCompile>("kaptGenerateStubs${buildType}Kotlin${target}").configure {
source(generatedKspKotlinFiles)
}
}
}

ksp { arg("circuit.codegen.lenient", "true") }
ksp { arg("circuit.codegen.mode", "metro") }

dependencies {
for (target in kspTargets) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright (C) 2025 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
package com.slack.circuit.star

actual abstract class BasePresenterTest
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import com.slack.circuit.star.animation.HomeAnimatedScreenTransform
import com.slack.circuit.star.animation.PetDetailAnimatedScreenTransform
import com.slack.circuit.star.benchmark.ListBenchmarksScreen
import com.slack.circuit.star.di.ActivityKey
import com.slack.circuit.star.di.AppScope
import com.slack.circuit.star.home.HomeScreen
import com.slack.circuit.star.navigation.OpenUrlScreen
import com.slack.circuit.star.petdetail.PetDetailScreen
Expand All @@ -37,15 +36,18 @@ import com.slack.circuitx.android.AndroidScreen
import com.slack.circuitx.android.IntentScreen
import com.slack.circuitx.android.rememberAndroidScreenAwareNavigator
import com.slack.circuitx.gesturenavigation.GestureNavigationDecorationFactory
import com.squareup.anvil.annotations.ContributesMultibinding
import javax.inject.Inject
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.binding
import kotlinx.collections.immutable.persistentListOf
import okhttp3.HttpUrl.Companion.toHttpUrl

@OptIn(ExperimentalCircuitApi::class)
@ContributesMultibinding(AppScope::class, boundType = Activity::class)
@ContributesIntoMap(AppScope::class, binding = binding<Activity>())
@ActivityKey(MainActivity::class)
class MainActivity @Inject constructor(private val circuit: Circuit) : AppCompatActivity() {
@Inject
class MainActivity(private val circuit: Circuit) : AppCompatActivity() {

@OptIn(ExperimentalSharedTransitionApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@ import android.app.Application
import coil3.ImageLoader
import coil3.PlatformContext
import coil3.SingletonImageLoader
import com.slack.circuit.star.di.AppComponent
import com.slack.circuit.star.di.AppGraph
import dev.zacsweers.metro.createGraphFactory

class StarApp : Application(), SingletonImageLoader.Factory {

private val appComponent by lazy { AppComponent.create(this) }
val appGraph by lazy { createGraphFactory<AppGraph.Factory>().create(this) }

fun appComponent() = appComponent

override fun newImageLoader(context: PlatformContext): ImageLoader = appComponent.imageLoader
override fun newImageLoader(context: PlatformContext): ImageLoader = appGraph.imageLoader
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,22 @@
package com.slack.circuit.star.data

import android.content.Context
import com.slack.circuit.star.di.AppScope
import com.slack.circuit.star.di.ApplicationContext
import com.squareup.anvil.annotations.ContributesBinding
import com.squareup.anvil.annotations.optional.SingleIn
import javax.inject.Inject
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import okio.FileSystem
import okio.Path
import okio.Path.Companion.toOkioPath

@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class ContextStarAppDirs
@Inject
constructor(@ApplicationContext private val context: Context, override val fs: FileSystem) :
StarAppDirs {
class ContextStarAppDirs(
@ApplicationContext private val context: Context,
override val fs: FileSystem,
) : StarAppDirs {

override val userConfig: Path by lazy {
(context.filesDir.toOkioPath() / "config").also(fs::createDirectories)
Expand Down

This file was deleted.

Loading