# How To Win Friends And Influence Internal Visibility

# Intro

I recently learned about [friendPaths](https://kotlinlang.org/api/kotlin-gradle-plugin/kotlin-gradle-plugin-api/org.jetbrains.kotlin.gradle.tasks/-base-kotlin-compile/friend-paths.html) as part of the Kotlin Gradle Plugin while browsing an Android Slack group. From the documentation on `friendPaths`: "Paths to the output directories of the friend modules whose internal declarations should be visible."

This struck some joy in me when I thought about modules being 'friends' and allowing one friend to have visibility into internal members. Additionally, its use case within my project became immediately obvious. First, some backstory on the setup of the project.

We use a modularization strategy roughly outlined in [this](https://www.droidcon.com/2019/11/15/android-at-scale-square/) talk titled "Android At Scale @Square" from Ralf Wondratschek.

For the purpose of this post, the modularization strategy can be summarized by every logical module having 3 actual modules.

1. A public facing API module
    
2. An internal implementation module
    
3. A wiring / glue module to bind the API with the implementation
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1762836308392/1b654aa3-59bc-47f7-90e0-6baba48ee0d4.png align="left")

The wiring module depends on the `public` module via the `api` Gradle configuration and depends on the `impl` module via the `implementation` Gradle configuration. The app module then depends on the `wiring` module.

This simplified strategy is perfectly reasonable but has one caveat. In order for the `wiring` module to bind the `public` API and the internal `impl` modules, the members of `impl` actually can't be internal because the `wiring` module needs visibility into them.

# Befriending `wiring` and `impl`

In this setup, `wiring` and `impl` have a special relationship. One could describe this relationship as friendly even! The ideal state would allow `impl` to have its members be internal while still allowing `wiring` to peek at them to bind them in our DI graph.

## Show Me The Code

The context of the below code occurs in the assorted configuration of our Gradle Plugin.

To wire up this friendly relationship, I first created a new Gradle configuration that I've called `friendlyImplementation` (The idea for this was shamelessly borrowed from [Aurimas Liutikas](https://www.liutikas.net/2025/01/12/Kotlin-Library-Friends.html))

```kotlin
val friendlyImplementation =  
    configurations.create("friendlyImplementation") {  
        isCanBeResolved = true  
        isCanBeConsumed = false  
        isTransitive = false  
    }  
  
// Make implementation extend from friendlyImplementation so dependencies are available  
configurations.findByName("implementation")?.extendsFrom(friendlyImplementation)
```

Now that we have this new configuration, we can have our `wiring` module depend on our `impl` module via this new `friendlyImplementation` configuration

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1762836325569/cb1318fd-c9b0-451d-8746-abf1d3e9d15c.png align="left")

This will work to add a module as a dependency but won't actually do anything for us yet. If we try to access an internal property within `impl`, we will see both an error in Android Studio and we will see an error from the Kotlin compiler

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1762836526592/4a74a82d-cad6-409c-8487-f34d08dc280d.png align="center")

## Configuring `friendlyImplementation`

```kotlin
private fun Project.configureFriendModules() {  
    val friendlyImplementation =  
        configurations.create("friendlyImplementation") {  
            isCanBeResolved = true  
            isCanBeConsumed = false  
            isTransitive = false  
        }  
  
    // Make implementation extend from friendlyImplementation so dependencies are available  
    configurations.findByName("implementation")?.extendsFrom(friendlyImplementation)  
  
    friendlyImplementation.dependencies.configureEach {  
        // assume any friendlyImplementation dependency is within same parent project  
        val friendlyPath = this@configureFriendModules.path.substringBeforeLast(":") + ":$name"  
  
        val friendlyProjectPath =  
            rootProject  
                .findProject(friendlyPath)  
                ?.layout  
                ?.buildDirectory  
                ?.get()  
                ?.asFile  
                ?.absolutePath  
  
        if (friendlyProjectPath != null) {  
            this@configureFriendModules.tasks  
                .withType(KotlinCompile::class.java)  
                .configureEach { friendPaths.from(files(friendlyProjectPath)) }  
        } else {  
            logger.error(  
                "Attempting to configure friendly project failed for project ${this@configureFriendModules.name} with dependency $name"  
            )  
        }  
    }  
}
```

When going to configure our `friendlyImplementation` configuration, the goal is to look at all of the dependencies included under `friendlyImplementation`, get that dependency's build directory, and add it as a friend via `friendPaths`

## Battling Android Studio

After adding this to our Gradle plugin, this setup allows us to get successful compilations! But presents one annoying developer experience issue: Android Studio still presents the error in the screenshot above. This experience is not ideal for developers on the team who usually have high trust when Android Studio tells them something is wrong ([issue filed](https://youtrack.jetbrains.com/issue/KTIJ-30664))

Luckily for me, we have the infrastructure in place already for a custom Android Studio plugin that is distributed internally and installed on our developers machines. Having a distributed internal Android Studio plugin is a great tool in an arsenal to help with the developer experience of a team.

IntelliJ exposes the ability for us to interact with this error via [HighlightInfoFilter](https://plugins.jetbrains.com/docs/intellij/controlling-highlighting.html). The gist of the API of this filter is given some information about `HighlightInfo` should that highlight display. In our implementation, we will look at the `HighlightInfo`s description, do some fuzzy matching against our `INVISIBLE_REFERENCE` from earlier, and hide that error if its in a module that has `friendlyImplementation` dependencies (I'm waving my hand at the exactness of this algorithm. The eagle eyed amongst anyone reading this can point out a case in which this algorithm fails, because for example we don't match the member to a given `friendlyImplementation` dependency.)

```kotlin
public class FriendPathHighlightInfoFilter : HighlightInfoFilter {  
  
    override fun accept(highlightInfo: HighlightInfo, psiFile: PsiFile?): Boolean {  
        // Only process error-level highlights  
        if (highlightInfo.severity != HighlightSeverity.ERROR) {  
            return true  
        }  
  
        // Check if this is an internal visibility error  
        val description = highlightInfo.description ?: return true  
        val isInternalError = description.contains("[INVISIBLE_REFERENCE]", ignoreCase = true)  
  
        if (!isInternalError) {  
            return true  
        }  
  
        // Check if we're in a glue module (which has friend path access)  
        if (psiFile == null) {  
            return true  
        }  
  
        val module = ModuleUtilCore.findModuleForFile(psiFile.virtualFile, psiFile.project)  
            ?: return true  
  
        // Check if this module has friendlyImplementation dependencies  
        val detector = FriendModuleDetector.getInstance(psiFile.project)  
        if (detector.hasFriendlyImplementation(module)) {  
            // Return false to suppress (hide) this error  
            return false  
        }  
  
        // Allow the error to be shown in other modules  
        return true  
    }  
}
```

We then create a service that has a cache of module's build files so we can check to see if a module has dependencies via `friendlyImplementation`

```kotlin
@Service(Service.Level.PROJECT)  
public class FriendModuleDetector {  
  
    private val cache = ConcurrentHashMap<String, Boolean>()  
  
    public companion object {  
        public fun getInstance(project: Project): FriendModuleDetector = project.service()  
        
           private val FRIENDLY_IMPL_PATTERNS = listOf(  
            "friendlyImplementation",  
            "friendlyImplementation(",  
            "friendlyImplementation (",  
        )  
    }  
  public fun hasFriendlyImplementation(module: Module): Boolean {  
        val moduleName = module.name  
  
        // Check cache first  
        cache[moduleName]?.let { return it }  
  
        // Check if module name suggests it's a friend module (fast path)  
        if (moduleName.contains(".glue")) {  
            cache[moduleName] = true  
            return true        
            }  
  
        // Check the build file for friendlyImplementation  
        val hasFriendlyImpl = checkBuildFileForFriendlyImplementation(module)  
        cache[moduleName] = hasFriendlyImpl  
  
        return hasFriendlyImpl  
    }  
  
  private fun checkBuildFileForFriendlyImplementation(module: Module): Boolean {  
        val buildFile = GradleUtil.getGradleBuildScriptSource(module)  
  
        return buildFile != null && containsFriendlyImplementation(buildFile)  
    }  
  
private fun containsFriendlyImplementation(buildFile: VirtualFile): Boolean {  
        try {  
            val content = String(buildFile.contentsToByteArray())  
  
            return FRIENDLY_IMPL_PATTERNS.any { pattern ->  
                content.contains(pattern)  
            }  
        } catch (e: Exception) {  
            // If we can't read the file, assume no friendly implementation  
            return false  
        }  
    }  
  
   public fun clearCache() {  
        cache.clear()  
    }  
  
	public fun clearCacheForModule(moduleName: String) {  
	        cache.remove(moduleName)  
	    }  
	}
```

and lastly, a startup to listen for file changes on build files so we can invalidate our build file cache when a build file changes

```kotlin
public class FriendModuleStartupActivity : ProjectActivity {  
  
    override suspend fun execute(project: Project) {  
        // Register the build file listener  
        val connection = project.messageBus.connect()  
        connection.subscribe(  
            com.intellij.openapi.vfs.VirtualFileManager.VFS_CHANGES,  
            FriendModuleBuildFileListener(project),  
        )  
    }  
}
```

```kotlin
public class FriendModuleBuildFileListener(private val project: Project) : BulkFileListener {  
  
    override fun after(events: MutableList<out VFileEvent>) {  
        val detector = FriendModuleDetector.getInstance(project)  
  
        for (event in events) {  
            if (event is VFileContentChangeEvent) {  
                val file = event.file  
                if (isBuildFile(file)) {  
                    // Find the module for this build file and clear its cache  
                    val module = ModuleUtilCore.findModuleForFile(file, project)  
                    if (module != null) {  
                        detector.clearCacheForModule(module.name)  
                    }  
                }  
            }  
        }  
    }  
  
    private fun isBuildFile(file: VirtualFile): Boolean {  
        return file.name.endsWith(".gradle.kts")  
    }  
}
```

And after all of that, Android Studio no longer yells at us!

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1762836501107/28ddafca-7f55-4aa2-8b12-72c066e309d2.png align="center")

# Outro

After adding support for a new Gradle configuration in our Gradle plugin and coercing Android Studio to not care about `INVISIBLE_MEMBERS`, we have a solution that allows `wiring` and `glue` modules to be friends where there is minimal impact to the developer experience!

I also later learned this concept of `friendPaths` is what allows test source sets to see internal members inside the main source set. I had sort of just taken this functionality for granted and never thought about how or why this works.

Lastly, if this 3 module system is anything you're interested in, I have a shameless plug for an IntelliJ plugin I maintain called Module Maker that helps streamline the creation of this 3 module systems: [JetBrains Marketplace](https://plugins.jetbrains.com/plugin/21724-module-maker) and [Github](https://github.com/j-roskopf/ModuleMakerPlugin) and my [blog post](https://blog.joetr.com/simplifying-android-module-creation-enter-the-module-maker-plugin) on the topic

Cover photo by [fredrikwandem](https://unsplash.com/photos/a-forest-filled-with-lots-of-trees-covered-in-fog-SvhIF0Q8gO4_)
