google / dagger

A fast dependency injector for Android and Java.
https://dagger.dev
Apache License 2.0
17.42k stars 2.01k forks source link

[ksp] Dagger fails to resolved ViewBinding classes generated by the Android Gradle plugin #4049

Open Jean-Daniel opened 1 year ago

Jean-Daniel commented 1 year ago

I have a class that uses ViewBinding as a generic parameter:

@AndroidEntryPoint
public class DownloadsFragment : BindingFragment<DownloadsListBinding>(DownloadsListBinding::inflate) {

   @Inject
    internal lateinit var db: Database
}

DownloadsListBinding is a class generated from a xml layout resource by the Android Gradle Plugin, and everything is working fine with kapt, but when trying to use KSP, the compilation fails with the following error:

e: [ksp] InjectProcessingStep was unable to process 'db' because 'error.NonExistentClass' could not be resolved.

Dependency trace:
    => element (CLASS): mypackage.downloads.DownloadsFragment
    => type (DECLARED superclass): mypackage.ui.BindingFragment<error.NonExistentClass>
    => type (ERROR type argument): error.NonExistentClass

If type 'error.NonExistentClass' is a generated type, check above for compilation errors that may have prevented the type from being generated. Otherwise, ensure that type 'error.NonExistentClass' is on your classpath.

Versions: Gradle Plugin version: 8.1.1 Kotlin: 1.9.10 KSP: 1.9.10-1.0.13 Hilt: 2.48

danysantiago commented 1 year ago

ViewBinding is a KAPT processor and thus not compatible with KSP processors, essentially you are running into the issue described here: https://dagger.dev/dev-guide/ksp#interaction-with-javackapt-processors. See also https://github.com/google/ksp/issues/1388.

JakeWharton commented 1 year ago

ViewBinding does not rely on KAPT. It is its own code-generating task created by AGP.

danysantiago commented 1 year ago

Ups! I was thinking of DataBinding then which I believe has an annotation processor component that runs in KAPT for Kotlin sources. Sorry for the confusion. 😅

So, ViewBinding being a task in AGP then my guess is that it needs some extra wiring so that generates classes are part of KSP's inputs. I'll ask AGP team and investigate more...

ZOlbrys commented 1 year ago

I've made a simple minimum reproducible app, in case it helps: https://github.com/ZOlbrys/HiltKspViewBindingTestApp

The error only happens when you use view bindings in a generic manner, FWIW.

davidtrpchevski commented 1 year ago

Hey @danysantiago

Glad to hear that you are investigating this issue. Is there any progress on this one?

paladin952 commented 1 year ago

any updates on this one? thank you!

danysantiago commented 1 year ago

The AGP team is looking into it: https://issuetracker.google.com/301245705

AmrAfifiy commented 1 year ago

We're working on a proper fix for the issue by exposing an API for generated java sources by the android gradle plugin to be consumed by KSP gradle plugin. Meanwhile, you can add the following snippet to your build file as a workaround to wire databinding/viewbinding generated classes to the ksp task.

androidComponents {
    onVariants(selector().all(), { variant ->
        afterEvaluate {
            // This is a workaround for https://issuetracker.google.com/301245705 which depends on internal
            // implementations of the android gradle plugin and the ksp gradle plugin which might change in the future
            // in an unpredictable way.
            project.tasks.getByName("ksp" + variant.name.capitalize() + "Kotlin") {
                def dataBindingTask = (com.android.build.gradle.internal.tasks.databinding.DataBindingGenBaseClassesTask) project.tasks.getByName("dataBindingGenBaseClasses" + variant.name.capitalize())

                ((org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompileTool) it).setSource(
                        dataBindingTask.sourceOutFolder
                )
            }
        }
    })
}
findyourexit commented 1 year ago

We're working on a proper fix for the issue by exposing an API for generated java sources by the android gradle plugin to be consumed by KSP gradle plugin. Meanwhile, you can add the following snippet to your build file as a workaround to wire databinding/viewbinding generated classes to the ksp task.

androidComponents {
    onVariants(selector().all(), { variant ->
        afterEvaluate {
            // This is a workaround for https://issuetracker.google.com/301245705 which depends on internal
            // implementations of the android gradle plugin and the ksp gradle plugin which might change in the future
            // in an unpredictable way.
            project.tasks.getByName("ksp" + variant.name.capitalize() + "Kotlin") {
                def dataBindingTask = (com.android.build.gradle.internal.tasks.databinding.DataBindingGenBaseClassesTask) project.tasks.getByName("dataBindingGenBaseClasses" + variant.name.capitalize())

                ((org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompileTool) it).setSource(
                        dataBindingTask.sourceOutFolder
                )
            }
        }
    })
}

While this didn't quite work for me right away, it did highlight what I needed to do to get things working in my case. Sincere thanks for the stopgap solution @AmrAfifiy 🙏

Here's the modified code that fit my situation:

androidComponents {
        onVariants(selector().all(), { variant ->
            afterEvaluate {
                // This is a workaround for https://issuetracker.google.com/301245705 which depends on internal
                // implementations of the android gradle plugin and the ksp gradle plugin which might change in the future
                // in an unpredictable way.
                def dataBindingTask = (DataBindingGenBaseClassesTask) project.tasks.named("dataBindingGenBaseClasses" + variant.name.capitalize()).get()

                if (dataBindingTask != null) {
                    project.tasks.getByName("ksp" + variant.name.capitalize() + "Kotlin") {
                        ((AbstractKotlinCompileTool) it).setSource(dataBindingTask.sourceOutFolder)
                    }
                }
            }
        })
    }

The main difference included here was picked up by a user on IssueTracker, who raised a very valid point that not all modules that apply the ksp plugin use view binding. So with that in mind, it makes sense to optionally process DataBindingGenBaseClassesTask instances, as they could be null (when a module that applies the ksp plugin does not make use of view binding).

I hope that helps!

Goooler commented 10 months ago

There is a workaround for those who are facing a simular issue with AIDL using:

// Workaround for https://github.com/google/dagger/issues/4158
androidComponents {
    onVariants(selector().all(), { variant ->
        afterEvaluate {
            def capName = variant.name.capitalize()
            tasks.getByName("ksp${capName}Kotlin") {
                setSource(tasks.getByName("compile${capName}Aidl").outputs)
            }
        }
    })
}

Originally posted by @Goooler in https://github.com/google/dagger/issues/4158#issuecomment-1825399362

technoir42 commented 10 months ago

Facing the same issue with a custom plugin that generates Kotlin files and registers them using addGeneratedSourceDirectory:

val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
androidComponents.onVariants { variant ->
    val task = project.tasks.register("myTask${variant.name.capitalized()}", MyKotlinGeneratingTask::class.java)
    variant.sources.java!!.addGeneratedSourceDirectory(task) { it.outputDirectory }
}

Dagger KSP processor as of 2.48.1/KSP 1.0.13 cannot resolve any type generated by the custom task.

ArcherEmiya05 commented 10 months ago

Is there a version of snippet in Kotlin DSL? I am not really sure how to apply the Kotlin sample from the issue tracker. Thanks

LeonRa commented 10 months ago

Something like the following should work for you as a Kotlin DSL:

import com.android.build.gradle.internal.tasks.databinding.DataBindingGenBaseClassesTask
import org.gradle.api.UnknownTaskException
import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompileTool

extensions.configure<LibraryAndroidComponentsExtension> {
  onVariants { variant ->
    afterEvaluate {
      project.tasks.getByName("ksp${variant.name.capitalize()}Kotlin") {
        val dataBindingTask =
          try {
            val taskName = "dataBindingGenBaseClasses${variant.name.capitalize()}"
              project.tasks.getByName(taskName) as DataBindingGenBaseClassesTask
          } catch (e: UnknownTaskException) {
            return@getByName
          }

          project.tasks.getByName("ksp${variant.name.capitalize()}Kotlin") {
            (this as AbstractKotlinCompileTool<*>).setSource(dataBindingTask.sourceOutFolder)
          }
       }
    }
  }
}
ArcherEmiya05 commented 10 months ago

Something like the following should work for you as a Kotlin DSL:

import com.android.build.gradle.internal.tasks.databinding.DataBindingGenBaseClassesTask
import org.gradle.api.UnknownTaskException
import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompileTool

extensions.configure<LibraryAndroidComponentsExtension> {
  onVariants { variant ->
    afterEvaluate {
      project.tasks.getByName("ksp${variant.name.capitalize()}Kotlin") {
        val dataBindingTask =
          try {
            val taskName = "dataBindingGenBaseClasses${variant.name.capitalize()}"
              project.tasks.getByName(taskName) as DataBindingGenBaseClassesTask
          } catch (e: UnknownTaskException) {
            return@getByName
          }

          project.tasks.getByName("ksp${variant.name.capitalize()}Kotlin") {
            (this as AbstractKotlinCompileTool<*>).setSource(dataBindingTask.sourceOutFolder)
          }
       }
    }
  }
}

Which part of the build.gradle.kts should be placed? I tried it inside the app module build gradle right below dependencies block and I got syncing error. Extension of type 'LibraryAndroidComponentsExtension' does not exist. Currently registered extension types: [ExtraPropertiesExtension, LibrariesForAdmob, LibrariesForFirebase, LibrariesForGms, LibrariesForLibs, LibrariesForMaterial, VersionCatalogsExtension, BasePluginExtension, DefaultArtifactPublicationSet, SourceSetContainer, ReportingExtension, JavaToolchainService, JavaPluginExtension, BaseAppModuleExtension, ApplicationAndroidComponentsExtension, NamedDomainObjectContainer<BaseVariantOutput>, KotlinAndroidProjectExtension, KotlinTestsRegistry, AppSweepExtension, PlayPublisherExtension, KspExtension, GoogleServicesPlugin.GoogleServicesPluginConfig]

LeonRa commented 10 months ago

Ah, apologies. I have this configured as a plugin. As a pure DSL within your app's build.gradle.kts, you should be able to do the same thing as suggested by others above:

import com.android.build.gradle.internal.tasks.databinding.DataBindingGenBaseClassesTask
import org.gradle.api.UnknownTaskException
import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompileTool

androidComponents {
  onVariants(selector().all()) { variant ->
    afterEvaluate {
      project.tasks.getByName("ksp${variant.name.capitalize()}Kotlin") {
        val dataBindingTask =
          try {
            val taskName = "dataBindingGenBaseClasses${variant.name.capitalize()}"
            project.tasks.getByName(taskName) as DataBindingGenBaseClassesTask
          } catch (e: UnknownTaskException) {
            return@getByName
          }

        project.tasks.getByName("ksp${variant.name.capitalize()}Kotlin") {
          (this as AbstractKotlinCompileTool<*>).setSource(dataBindingTask.sourceOutFolder)
        }
      }
    }
  }
}
ArcherEmiya05 commented 10 months ago

Ah, apologies. I have this configured as a plugin. As a pure DSL within your app's build.gradle.kts, you should be able to do the same thing as suggested by others above:

import com.android.build.gradle.internal.tasks.databinding.DataBindingGenBaseClassesTask
import org.gradle.api.UnknownTaskException
import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompileTool

androidComponents {
  onVariants(selector().all()) { variant ->
    afterEvaluate {
      project.tasks.getByName("ksp${variant.name.capitalize()}Kotlin") {
        val dataBindingTask =
          try {
            val taskName = "dataBindingGenBaseClasses${variant.name.capitalize()}"
            project.tasks.getByName(taskName) as DataBindingGenBaseClassesTask
          } catch (e: UnknownTaskException) {
            return@getByName
          }

        project.tasks.getByName("ksp${variant.name.capitalize()}Kotlin") {
          (this as AbstractKotlinCompileTool<*>).setSource(dataBindingTask.sourceOutFolder)
        }
      }
    }
  }
}

This work, thanks a lot!

Antimonit commented 10 months ago

As others pointed out, this is not an issue only of View Binding. BuildConfig, SafeArgs, AIDL, etc. are affected just the same. Here's a script covering all of them.

/*
 * AGP tasks do not get properly wired to the KSP task at the moment.
 * As a result, KSP sees `error.NonExistentClass` instead of generated types.
 *
 * https://github.com/google/dagger/issues/4049
 * https://github.com/google/dagger/issues/4051
 * https://github.com/google/dagger/issues/4061
 * https://github.com/google/dagger/issues/4158
 */
androidComponents {
    onVariants(selector().all()) { variant ->
        afterEvaluate {
            val variantName = variant.name.capitalize()
            val ksp = "ksp${variantName}Kotlin"
            val viewBinding = "dataBindingGenBaseClasses$variantName"
            val buildConfig = "generate${variantName}BuildConfig"
            val safeArgs = "generateSafeArgs$variantName"
            val aidl = "compile${variantName}Aidl"

            val kspTask = project.tasks.findByName(ksp)
                as? org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompileTool<*>
            val viewBindingTask = project.tasks.findByName(viewBinding)
                as? com.android.build.gradle.internal.tasks.databinding.DataBindingGenBaseClassesTask
            val buildConfigTask = project.tasks.findByName(buildConfig)
                as? com.android.build.gradle.tasks.GenerateBuildConfig
            val aidlTask = project.tasks.findByName(aidl)
                as? com.android.build.gradle.tasks.AidlCompile
            val safeArgsTask = project.tasks.findByName(safeArgs)
                as? androidx.navigation.safeargs.gradle.ArgumentsGenerationTask

            kspTask?.run {
                viewBindingTask?.let { setSource(it.sourceOutFolder) }
                buildConfigTask?.let { setSource(it.sourceOutputDir) }
                aidlTask?.let { setSource(it.sourceOutputDir) }
                safeArgsTask?.let { setSource(it.outputDir) }
            }
        }
    }
}
pokerfaceCmy commented 9 months ago

As others pointed out, this is not an issue only of View Binding. BuildConfig, SafeArgs, AIDL, etc. are affected just the same. Here's a script covering all of them.

/*
 * AGP tasks do not get properly wired to the KSP task at the moment.
 * As a result, KSP sees `error.NonExistentClass` instead of generated types.
 *
 * https://github.com/google/dagger/issues/4049
 * https://github.com/google/dagger/issues/4051
 * https://github.com/google/dagger/issues/4061
 * https://github.com/google/dagger/issues/4158
 */
androidComponents {
    onVariants(selector().all()) { variant ->
        afterEvaluate {
            val variantName = variant.name.capitalize()
            val ksp = "ksp${variantName}Kotlin"
            val viewBinding = "dataBindingGenBaseClasses$variantName"
            val buildConfig = "generate${variantName}BuildConfig"
            val safeArgs = "generateSafeArgs$variantName"
            val aidl = "compile${variantName}Aidl"

            val kspTask = project.tasks.findByName(ksp)
                as? org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompileTool<*>
            val viewBindingTask = project.tasks.findByName(viewBinding)
                as? com.android.build.gradle.internal.tasks.databinding.DataBindingGenBaseClassesTask
            val buildConfigTask = project.tasks.findByName(buildConfig)
                as? com.android.build.gradle.tasks.GenerateBuildConfig
            val aidlTask = project.tasks.findByName(aidl)
                as? com.android.build.gradle.tasks.AidlCompile
            val safeArgsTask = project.tasks.findByName(safeArgs)
                as? androidx.navigation.safeargs.gradle.ArgumentsGenerationTask

            kspTask?.run {
                viewBindingTask?.let { setSource(it.sourceOutFolder) }
                buildConfigTask?.let { setSource(it.sourceOutputDir) }
                aidlTask?.let { setSource(it.sourceOutputDir) }
                safeArgsTask?.let { setSource(it.outputDir) }
            }
        }
    }
}

thanks,it work for me!

RobinPcrd commented 7 months ago

As others pointed out, this is not an issue only of View Binding. BuildConfig, SafeArgs, AIDL, etc. are affected just the same. Here's a script covering all of them.

/*
 * AGP tasks do not get properly wired to the KSP task at the moment.
 * As a result, KSP sees `error.NonExistentClass` instead of generated types.
 *
 * https://github.com/google/dagger/issues/4049
 * https://github.com/google/dagger/issues/4051
 * https://github.com/google/dagger/issues/4061
 * https://github.com/google/dagger/issues/4158
 */
androidComponents {
    onVariants(selector().all()) { variant ->
        afterEvaluate {
            val variantName = variant.name.capitalize()
            val ksp = "ksp${variantName}Kotlin"
            val viewBinding = "dataBindingGenBaseClasses$variantName"
            val buildConfig = "generate${variantName}BuildConfig"
            val safeArgs = "generateSafeArgs$variantName"
            val aidl = "compile${variantName}Aidl"

            val kspTask = project.tasks.findByName(ksp)
                as? org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompileTool<*>
            val viewBindingTask = project.tasks.findByName(viewBinding)
                as? com.android.build.gradle.internal.tasks.databinding.DataBindingGenBaseClassesTask
            val buildConfigTask = project.tasks.findByName(buildConfig)
                as? com.android.build.gradle.tasks.GenerateBuildConfig
            val aidlTask = project.tasks.findByName(aidl)
                as? com.android.build.gradle.tasks.AidlCompile
            val safeArgsTask = project.tasks.findByName(safeArgs)
                as? androidx.navigation.safeargs.gradle.ArgumentsGenerationTask

            kspTask?.run {
                viewBindingTask?.let { setSource(it.sourceOutFolder) }
                buildConfigTask?.let { setSource(it.sourceOutputDir) }
                aidlTask?.let { setSource(it.sourceOutputDir) }
                safeArgsTask?.let { setSource(it.outputDir) }
            }
        }
    }
}

Thanks! You might also want to add these lines if you use protobuf (with datastore for example)

androidComponents {
    onVariants(selector().all()) { variant ->
        afterEvaluate {
            //...
            val proto = "generate${variantName}Proto"

            //...
            val protoTask = project.tasks.findByName(proto)
                    as? com.google.protobuf.gradle.GenerateProtoTask

            kspTask?.run {
                //...
                protoTask?.let { setSource(it.outputSourceDirectorySet) }
            }
        }
    }
}
egorikftp commented 7 months ago

AGP issue fixed: https://issuetracker.google.com/301245705

Can confirm, issue not reproduce anymore with ksp 1.9.22-1.0.18

rekaszeru commented 7 months ago

I've just updated to this version to check out if it works, but unfortunately I still have the same error:

Dependency trace:
    => element (CLASS): com.example.auth.data.SessionStore
    => element (CONSTRUCTOR): SessionStore(kotlinx.coroutines.CoroutineDispatcher, androidx.datastore.core.DataStore<error.NonExistentClass>)
    => type (EXECUTABLE constructor): (kotlinx.coroutines.CoroutineDispatcher,androidx.datastore.core.DataStore<error.NonExistentClass>)void
    => type (DECLARED parameter type): androidx.datastore.core.DataStore<error.NonExistentClass>
    => type (ERROR type argument): error.NonExistentClass

My SessionStore constructor looks like this:

internal class SessionStore @Injec constructor(
    @BackgroundDispatcher
    private val backgroundDispatcher: CoroutineDispatcher,
    private val stateStore: DataStore<Session> = context.sessionStore,
)

and Session is generated from proto using squareup's wire library.

I've tried the workarounds too (adapted to wire task), still no dice.

jpgpuyo commented 3 months ago

I have the same error. Any updates on this?

tinder-johnbuhanan2 commented 1 month ago

I have the same error as @rekaszeru . Any updates on this?

bcorso commented 1 month ago

AFAIK, this is not a Dagger issue.

However, there's a number of workarounds listed above, e.g. https://github.com/google/dagger/issues/4049#issuecomment-1743321244.

tinder-johnbuhanan2 commented 1 month ago

@bcorso , like @rekaszeru said, we have tried the workarounds. Specifically this was what I put in my Convention plugin:

configure<LibraryAndroidComponentsExtension> {
    onVariants { variant ->
        afterEvaluate {
            val variantName = variant.name.capitalize()
            val kspTaskName = "ksp${variantName}Kotlin"
            println("kspTaskName: $kspTaskName")
            val wireTaskName = "generate${variantName}Protos"
            val kspTask = project.tasks.findByName(kspTaskName) as? AbstractKotlinCompileTool<*>
            val wireTask = project.tasks.findByName(wireTaskName) as? WireTask
            kspTask?.run {
                wireTask?.let {
                    // itSource: SomeClass.proto
                    println("itSource: ${it.source.files.joinToString { it.name }}")
                    setSource(it.source)
                }
            }
        }
    }
}

And yet dagger ksp chokes when it can't find "SomeClass".

Edit: Wait, why would .proto be in the source that we want to set for kspTask? Edit2: Well I tried it with setSource(it.outputDirectories), but that didn't work either.

bcorso commented 1 month ago

@bcorso , like @rekaszeru said, we have tried the workarounds. Specifically this was what I put in my Convention plugin:

AFAICT, @rekaszeru never directly confirmed if they tried the workaround or not. My interpretation of https://github.com/google/dagger/issues/4049#issuecomment-1974017840 was that they tried upgrading to the new KSP version mentioned in https://github.com/google/dagger/issues/4049#issuecomment-1955890325.

Like I said, this isn't really a Dagger issue. This is an AGP/KSP issue and it just fails in Dagger because they aren't getting the ordering/wiring of generation correct.

Edit: Wait, why would .proto be in the source that we want to set for kspTask?

As mentioned in https://github.com/google/dagger/issues/4049#issuecomment-1846912761, this issue shows up with a number of different libraries like Protobuf, View Binding, BuildConfig, SafeArgs, AIDL, etc. so you have to make sure the workaround you use includes the library you need.

tinder-johnbuhanan2 commented 1 month ago

@rekaszeru I was able to get Wire working with Dagger KSP with the following in my Convention plugin:

configure<LibraryAndroidComponentsExtension> {
    onVariants { variant ->
        afterEvaluate {
            val variantName = variant.name.capitalize()
            val kspTaskName = "ksp${variantName}Kotlin"
            val wireTaskName = "generate${variantName}Protos"
            val kspTask = project.tasks.findByName(kspTaskName) as? AbstractKotlinCompileTool<*>
            val wireTask = project.tasks.findByName(wireTaskName) as? WireTask
            kspTask?.run {
                wireTask?.let {
                    dependsOn(it) // It chokes if you try to setSource without `dependsOn`, so I guess we need both.
                    setSource(it.outputDirectories)
                }
            }
        }
    }
}

Are you able to verify?