Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
Jean-Daniel opened this issue Aug 31, 2023 · 29 comments

Comments

@Jean-Daniel
Copy link

Jean-Daniel commented Aug 31, 2023

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
Copy link
Member

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 google/ksp#1388.

@JakeWharton
Copy link

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

@danysantiago
Copy link
Member

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
Copy link

ZOlbrys commented Sep 9, 2023

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
Copy link

Hey @danysantiago

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

@paladin952
Copy link

any updates on this one? thank you!

@danysantiago
Copy link
Member

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

@AmrAfifiy
Copy link

AmrAfifiy commented Oct 2, 2023

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
Copy link

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
Copy link

Goooler commented Nov 24, 2023

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 #4158 (comment)

@technoir42
Copy link

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
Copy link

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
Copy link

LeonRa commented Dec 3, 2023

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
Copy link

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
Copy link

LeonRa commented Dec 3, 2023

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
Copy link

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
Copy link

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
Copy link

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
Copy link

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
Copy link

egorikftp commented Feb 21, 2024

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

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

@rekaszeru
Copy link

rekaszeru commented Mar 1, 2024

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
Copy link

jpgpuyo commented Jul 2, 2024

I have the same error. Any updates on this?

@tinder-johnbuhanan2
Copy link

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

@bcorso
Copy link

bcorso commented Aug 23, 2024

AFAIK, this is not a Dagger issue.

However, there's a number of workarounds listed above, e.g. #4049 (comment).

@tinder-johnbuhanan2
Copy link

tinder-johnbuhanan2 commented Aug 23, 2024

@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
Copy link

bcorso commented Aug 23, 2024

@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 #4049 (comment) was that they tried upgrading to the new KSP version mentioned in #4049 (comment).

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 #4049 (comment), 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
Copy link

tinder-johnbuhanan2 commented Aug 26, 2024

@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?

@guness
Copy link

guness commented Oct 9, 2024

is this still valid after google/ksp#1739 ?

@Chang-Eric
Copy link
Member

is this still valid after google/ksp#1739 ?

@guness While that fix may fix some of the code generators, I believe this has to be fixed/checked on a generator by generator basis as it depends on where the outputs of those generators go. We can't really test with all of the different generators out there, so you'll probably want to check in your project for the ones you use, and if still running into issues, wire up the tasks as described above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests