<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joe's Android Blog]]></title><description><![CDATA[Joe's Android Blog]]></description><link>https://blog.joetr.com</link><generator>RSS for Node</generator><lastBuildDate>Sat, 11 Apr 2026 01:16:02 GMT</lastBuildDate><atom:link href="https://blog.joetr.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How To Win Friends And Influence Internal Visibility]]></title><description><![CDATA[Intro
I recently learned about friendPaths 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 ...]]></description><link>https://blog.joetr.com/how-to-win-friends-and-influence-internal-visibility</link><guid isPermaLink="true">https://blog.joetr.com/how-to-win-friends-and-influence-internal-visibility</guid><category><![CDATA[Android]]></category><category><![CDATA[Kotlin]]></category><dc:creator><![CDATA[Joe Roskopf]]></dc:creator><pubDate>Tue, 11 Nov 2025 15:54:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1762836667118/a84bbd76-5a1a-49a4-83b8-677bd83d2a2e.avif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-intro">Intro</h1>
<p>I recently learned about <a target="_blank" href="https://kotlinlang.org/api/kotlin-gradle-plugin/kotlin-gradle-plugin-api/org.jetbrains.kotlin.gradle.tasks/-base-kotlin-compile/friend-paths.html">friendPaths</a> as part of the Kotlin Gradle Plugin while browsing an Android Slack group. From the documentation on <code>friendPaths</code>: "Paths to the output directories of the friend modules whose internal declarations should be visible."</p>
<p>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.</p>
<p>We use a modularization strategy roughly outlined in <a target="_blank" href="https://www.droidcon.com/2019/11/15/android-at-scale-square/">this</a> talk titled "Android At Scale @Square" from Ralf Wondratschek.</p>
<p>For the purpose of this post, the modularization strategy can be summarized by every logical module having 3 actual modules.</p>
<ol>
<li><p>A public facing API module</p>
</li>
<li><p>An internal implementation module</p>
</li>
<li><p>A wiring / glue module to bind the API with the implementation</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762836308392/1b654aa3-59bc-47f7-90e0-6baba48ee0d4.png" alt /></p>
<p>The wiring module depends on the <code>public</code> module via the <code>api</code> Gradle configuration and depends on the <code>impl</code> module via the <code>implementation</code> Gradle configuration. The app module then depends on the <code>wiring</code> module.</p>
<p>This simplified strategy is perfectly reasonable but has one caveat. In order for the <code>wiring</code> module to bind the <code>public</code> API and the internal <code>impl</code> modules, the members of <code>impl</code> actually can't be internal because the <code>wiring</code> module needs visibility into them.</p>
<h1 id="heading-befriending-wiring-and-impl">Befriending <code>wiring</code> and <code>impl</code></h1>
<p>In this setup, <code>wiring</code> and <code>impl</code> have a special relationship. One could describe this relationship as friendly even! The ideal state would allow <code>impl</code> to have its members be internal while still allowing <code>wiring</code> to peek at them to bind them in our DI graph.</p>
<h2 id="heading-show-me-the-code">Show Me The Code</h2>
<p>The context of the below code occurs in the assorted configuration of our Gradle Plugin.</p>
<p>To wire up this friendly relationship, I first created a new Gradle configuration that I've called <code>friendlyImplementation</code> (The idea for this was shamelessly borrowed from <a target="_blank" href="https://www.liutikas.net/2025/01/12/Kotlin-Library-Friends.html">Aurimas Liutikas</a>)</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> friendlyImplementation =  
    configurations.create(<span class="hljs-string">"friendlyImplementation"</span>) {  
        isCanBeResolved = <span class="hljs-literal">true</span>  
        isCanBeConsumed = <span class="hljs-literal">false</span>  
        isTransitive = <span class="hljs-literal">false</span>  
    }  

<span class="hljs-comment">// Make implementation extend from friendlyImplementation so dependencies are available  </span>
configurations.findByName(<span class="hljs-string">"implementation"</span>)?.extendsFrom(friendlyImplementation)
</code></pre>
<p>Now that we have this new configuration, we can have our <code>wiring</code> module depend on our <code>impl</code> module via this new <code>friendlyImplementation</code> configuration</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762836325569/cb1318fd-c9b0-451d-8746-abf1d3e9d15c.png" alt /></p>
<p>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 <code>impl</code>, we will see both an error in Android Studio and we will see an error from the Kotlin compiler</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762836526592/4a74a82d-cad6-409c-8487-f34d08dc280d.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-configuring-friendlyimplementation">Configuring <code>friendlyImplementation</code></h2>
<pre><code class="lang-kotlin"><span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> Project.<span class="hljs-title">configureFriendModules</span><span class="hljs-params">()</span></span> {  
    <span class="hljs-keyword">val</span> friendlyImplementation =  
        configurations.create(<span class="hljs-string">"friendlyImplementation"</span>) {  
            isCanBeResolved = <span class="hljs-literal">true</span>  
            isCanBeConsumed = <span class="hljs-literal">false</span>  
            isTransitive = <span class="hljs-literal">false</span>  
        }  

    <span class="hljs-comment">// Make implementation extend from friendlyImplementation so dependencies are available  </span>
    configurations.findByName(<span class="hljs-string">"implementation"</span>)?.extendsFrom(friendlyImplementation)  

    friendlyImplementation.dependencies.configureEach {  
        <span class="hljs-comment">// assume any friendlyImplementation dependency is within same parent project  </span>
        <span class="hljs-keyword">val</span> friendlyPath = <span class="hljs-keyword">this</span><span class="hljs-symbol">@configureFriendModules</span>.path.substringBeforeLast(<span class="hljs-string">":"</span>) + <span class="hljs-string">":<span class="hljs-variable">$name</span>"</span>  

        <span class="hljs-keyword">val</span> friendlyProjectPath =  
            rootProject  
                .findProject(friendlyPath)  
                ?.layout  
                ?.buildDirectory  
                ?.<span class="hljs-keyword">get</span>()  
                ?.asFile  
                ?.absolutePath  

        <span class="hljs-keyword">if</span> (friendlyProjectPath != <span class="hljs-literal">null</span>) {  
            <span class="hljs-keyword">this</span><span class="hljs-symbol">@configureFriendModules</span>.tasks  
                .withType(KotlinCompile::<span class="hljs-keyword">class</span>.java)  
                .configureEach { friendPaths.from(files(friendlyProjectPath)) }  
        } <span class="hljs-keyword">else</span> {  
            logger.error(  
                <span class="hljs-string">"Attempting to configure friendly project failed for project <span class="hljs-subst">${this@configureFriendModules.name}</span> with dependency <span class="hljs-variable">$name</span>"</span>  
            )  
        }  
    }  
}
</code></pre>
<p>When going to configure our <code>friendlyImplementation</code> configuration, the goal is to look at all of the dependencies included under <code>friendlyImplementation</code>, get that dependency's build directory, and add it as a friend via <code>friendPaths</code></p>
<h2 id="heading-battling-android-studio">Battling Android Studio</h2>
<p>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 (<a target="_blank" href="https://youtrack.jetbrains.com/issue/KTIJ-30664">issue filed</a>)</p>
<p>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.</p>
<p>IntelliJ exposes the ability for us to interact with this error via <a target="_blank" href="https://plugins.jetbrains.com/docs/intellij/controlling-highlighting.html">HighlightInfoFilter</a>. The gist of the API of this filter is given some information about <code>HighlightInfo</code> should that highlight display. In our implementation, we will look at the <code>HighlightInfo</code>s description, do some fuzzy matching against our <code>INVISIBLE_REFERENCE</code> from earlier, and hide that error if its in a module that has <code>friendlyImplementation</code> 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 <code>friendlyImplementation</code> dependency.)</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FriendPathHighlightInfoFilter</span> : <span class="hljs-type">HighlightInfoFilter {  </span></span>

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">accept</span><span class="hljs-params">(highlightInfo: <span class="hljs-type">HighlightInfo</span>, psiFile: <span class="hljs-type">PsiFile</span>?)</span></span>: <span class="hljs-built_in">Boolean</span> {  
        <span class="hljs-comment">// Only process error-level highlights  </span>
        <span class="hljs-keyword">if</span> (highlightInfo.severity != HighlightSeverity.ERROR) {  
            <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>  
        }  

        <span class="hljs-comment">// Check if this is an internal visibility error  </span>
        <span class="hljs-keyword">val</span> description = highlightInfo.description ?: <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>  
        <span class="hljs-keyword">val</span> isInternalError = description.contains(<span class="hljs-string">"[INVISIBLE_REFERENCE]"</span>, ignoreCase = <span class="hljs-literal">true</span>)  

        <span class="hljs-keyword">if</span> (!isInternalError) {  
            <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>  
        }  

        <span class="hljs-comment">// Check if we're in a glue module (which has friend path access)  </span>
        <span class="hljs-keyword">if</span> (psiFile == <span class="hljs-literal">null</span>) {  
            <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>  
        }  

        <span class="hljs-keyword">val</span> module = ModuleUtilCore.findModuleForFile(psiFile.virtualFile, psiFile.project)  
            ?: <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>  

        <span class="hljs-comment">// Check if this module has friendlyImplementation dependencies  </span>
        <span class="hljs-keyword">val</span> detector = FriendModuleDetector.getInstance(psiFile.project)  
        <span class="hljs-keyword">if</span> (detector.hasFriendlyImplementation(module)) {  
            <span class="hljs-comment">// Return false to suppress (hide) this error  </span>
            <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>  
        }  

        <span class="hljs-comment">// Allow the error to be shown in other modules  </span>
        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>  
    }  
}
</code></pre>
<p>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 <code>friendlyImplementation</code></p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Service(Service.Level.PROJECT)</span>  
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FriendModuleDetector</span> </span>{  

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> cache = ConcurrentHashMap&lt;String, <span class="hljs-built_in">Boolean</span>&gt;()  

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">companion</span> <span class="hljs-keyword">object</span> {  
        <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getInstance</span><span class="hljs-params">(project: <span class="hljs-type">Project</span>)</span></span>: FriendModuleDetector = project.service()  

           <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> FRIENDLY_IMPL_PATTERNS = listOf(  
            <span class="hljs-string">"friendlyImplementation"</span>,  
            <span class="hljs-string">"friendlyImplementation("</span>,  
            <span class="hljs-string">"friendlyImplementation ("</span>,  
        )  
    }  
  <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">hasFriendlyImplementation</span><span class="hljs-params">(module: <span class="hljs-type">Module</span>)</span></span>: <span class="hljs-built_in">Boolean</span> {  
        <span class="hljs-keyword">val</span> moduleName = module.name  

        <span class="hljs-comment">// Check cache first  </span>
        cache[moduleName]?.let { <span class="hljs-keyword">return</span> it }  

        <span class="hljs-comment">// Check if module name suggests it's a friend module (fast path)  </span>
        <span class="hljs-keyword">if</span> (moduleName.contains(<span class="hljs-string">".glue"</span>)) {  
            cache[moduleName] = <span class="hljs-literal">true</span>  
            <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>        
            }  

        <span class="hljs-comment">// Check the build file for friendlyImplementation  </span>
        <span class="hljs-keyword">val</span> hasFriendlyImpl = checkBuildFileForFriendlyImplementation(module)  
        cache[moduleName] = hasFriendlyImpl  

        <span class="hljs-keyword">return</span> hasFriendlyImpl  
    }  

  <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">checkBuildFileForFriendlyImplementation</span><span class="hljs-params">(module: <span class="hljs-type">Module</span>)</span></span>: <span class="hljs-built_in">Boolean</span> {  
        <span class="hljs-keyword">val</span> buildFile = GradleUtil.getGradleBuildScriptSource(module)  

        <span class="hljs-keyword">return</span> buildFile != <span class="hljs-literal">null</span> &amp;&amp; containsFriendlyImplementation(buildFile)  
    }  

<span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">containsFriendlyImplementation</span><span class="hljs-params">(buildFile: <span class="hljs-type">VirtualFile</span>)</span></span>: <span class="hljs-built_in">Boolean</span> {  
        <span class="hljs-keyword">try</span> {  
            <span class="hljs-keyword">val</span> content = String(buildFile.contentsToByteArray())  

            <span class="hljs-keyword">return</span> FRIENDLY_IMPL_PATTERNS.any { pattern -&gt;  
                content.contains(pattern)  
            }  
        } <span class="hljs-keyword">catch</span> (e: Exception) {  
            <span class="hljs-comment">// If we can't read the file, assume no friendly implementation  </span>
            <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>  
        }  
    }  

   <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">clearCache</span><span class="hljs-params">()</span></span> {  
        cache.clear()  
    }  

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">clearCacheForModule</span><span class="hljs-params">(moduleName: <span class="hljs-type">String</span>)</span></span> {  
            cache.remove(moduleName)  
        }  
    }
</code></pre>
<p>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</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FriendModuleStartupActivity</span> : <span class="hljs-type">ProjectActivity {  </span></span>

    <span class="hljs-keyword">override</span> <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">execute</span><span class="hljs-params">(project: <span class="hljs-type">Project</span>)</span></span> {  
        <span class="hljs-comment">// Register the build file listener  </span>
        <span class="hljs-keyword">val</span> connection = project.messageBus.connect()  
        connection.subscribe(  
            com.intellij.openapi.vfs.VirtualFileManager.VFS_CHANGES,  
            FriendModuleBuildFileListener(project),  
        )  
    }  
}
</code></pre>
<pre><code class="lang-kotlin"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FriendModuleBuildFileListener</span></span>(<span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> project: Project) : BulkFileListener {  

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">after</span><span class="hljs-params">(events: <span class="hljs-type">MutableList</span>&lt;<span class="hljs-type">out</span> <span class="hljs-type">VFileEvent</span>&gt;)</span></span> {  
        <span class="hljs-keyword">val</span> detector = FriendModuleDetector.getInstance(project)  

        <span class="hljs-keyword">for</span> (event <span class="hljs-keyword">in</span> events) {  
            <span class="hljs-keyword">if</span> (event <span class="hljs-keyword">is</span> VFileContentChangeEvent) {  
                <span class="hljs-keyword">val</span> file = event.file  
                <span class="hljs-keyword">if</span> (isBuildFile(file)) {  
                    <span class="hljs-comment">// Find the module for this build file and clear its cache  </span>
                    <span class="hljs-keyword">val</span> module = ModuleUtilCore.findModuleForFile(file, project)  
                    <span class="hljs-keyword">if</span> (module != <span class="hljs-literal">null</span>) {  
                        detector.clearCacheForModule(module.name)  
                    }  
                }  
            }  
        }  
    }  

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">isBuildFile</span><span class="hljs-params">(file: <span class="hljs-type">VirtualFile</span>)</span></span>: <span class="hljs-built_in">Boolean</span> {  
        <span class="hljs-keyword">return</span> file.name.endsWith(<span class="hljs-string">".gradle.kts"</span>)  
    }  
}
</code></pre>
<p>And after all of that, Android Studio no longer yells at us!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762836501107/28ddafca-7f55-4aa2-8b12-72c066e309d2.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-outro">Outro</h1>
<p>After adding support for a new Gradle configuration in our Gradle plugin and coercing Android Studio to not care about <code>INVISIBLE_MEMBERS</code>, we have a solution that allows <code>wiring</code> and <code>glue</code> modules to be friends where there is minimal impact to the developer experience!</p>
<p>I also later learned this concept of <code>friendPaths</code> 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.</p>
<p>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: <a target="_blank" href="https://plugins.jetbrains.com/plugin/21724-module-maker">JetBrains Marketplace</a> and <a target="_blank" href="https://github.com/j-roskopf/ModuleMakerPlugin">Github</a> and my <a target="_blank" href="https://blog.joetr.com/simplifying-android-module-creation-enter-the-module-maker-plugin">blog post</a> on the topic</p>
<p>Cover photo by <a target="_blank" href="https://unsplash.com/photos/a-forest-filled-with-lots-of-trees-covered-in-fog-SvhIF0Q8gO4_">fredrikwandem</a></p>
]]></content:encoded></item><item><title><![CDATA[Bridging the Gap: Making Jetpack Compose Row Behave Like SwiftUi & Web]]></title><description><![CDATA[If you've ever tried to compare the behavior of Row in Google's Jetpack Compose, HStack in Apple's SwiftUi, and something like display: flex in the web world, you'll see some interesting results.
First, let's take a look at how HStack renders two lon...]]></description><link>https://blog.joetr.com/bridging-the-gap-making-jetpack-compose-row-behave-like-swiftui-and-web</link><guid isPermaLink="true">https://blog.joetr.com/bridging-the-gap-making-jetpack-compose-row-behave-like-swiftui-and-web</guid><category><![CDATA[Android]]></category><category><![CDATA[Jetpack Compose]]></category><dc:creator><![CDATA[Joe Roskopf]]></dc:creator><pubDate>Sun, 13 Apr 2025 14:50:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1744555790261/8e709902-f6cc-4104-b60e-ce4de92a0b66.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you've ever tried to compare the behavior of <code>Row</code> in Google's Jetpack Compose, <code>HStack</code> in Apple's SwiftUi, and something like <code>display: flex</code> in the web world, you'll see some interesting results.</p>
<p>First, let's take a look at how <code>HStack</code> renders two long text elements.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744554897303/2eca1dca-2301-4b8e-90ba-970816420659.png" alt class="image--center mx-auto" /></p>
<p>We can see SwiftUi has a fairly sane implementation. <code>HStack</code> does its best to try and render the two long text elements.</p>
<p>Now, let's take a look at how web handles the same situation.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744555328619/73d42e74-131c-471f-875f-52c6a00bc830.png" alt class="image--center mx-auto" /></p>
<p>Also, a fairly sane implementation. We can see that our similar implementation in web does its best to render both text elements.</p>
<p>Lastly, let's take a look at how Jetpack Compose handles a similar situation</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744555349757/bd1c557b-7f58-4482-b591-4a95b2edcd96.png" alt class="image--center mx-auto" /></p>
<p>Hmm, that's not similar to other platforms out of the box behavior. We see the first text image completely render, but the second one is pushed off the screen and has a width of 0.</p>
<p>The expert Android developers out there are probably saying, just apply a weight to each element!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744555394978/1da25ab7-afa5-4a72-9263-a47d493ec72b.png" alt class="image--center mx-auto" /></p>
<p>Okay, now we are getting somewhere. But what if the text elements aren't roughly the same size?</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744555443026/c022815a-704a-4317-9ee8-55acc705eb4e.png" alt class="image--center mx-auto" /></p>
<p>Obviously that doesn't work the way we want. The reality is, in most scenarios with static layouts, using weights will be fine enough to get you the layouts you want.</p>
<p>What if our layouts are dynamic though? How can we achieve a similar behavior to SwiftUi and web?</p>
<h2 id="heading-custom-layouts-for-fun-and-for-profit">Custom Layouts For Fun And For Profit</h2>
<p>Before we dive into the solution that worked for me in my situation, let's examine prior art.</p>
<p><a target="_blank" href="https://github.com/HedvigInsurance/android/blob/develop/app%2Fdesign-system%2Fdesign-system-hedvig%2Fsrc%2Fmain%2Fkotlin%2Fcom%2Fhedvig%2Fandroid%2Fdesign%2Fsystem%2Fhedvig%2FHorizontalItems.kt#L10-L100">Here</a> is an example from Hedvig Insurance of a custom layout that allows two items to take up the amount of space that they want while still respecting the other element.</p>
<p>From the documentation:</p>
<blockquote>
<p>When two items need to be laid out horizontally in a row, they can't know how much space they need to take out, which more often than not results in the starting item taking up all the width it needs, squeezing the end item. This layout makes sure to measure their max intrinsic width and give as much space as possible to each item, without squeezing the other one. If both of them were to need more than half of the space, or less than half of the space, they're simply given half of the width each.</p>
</blockquote>
<p>This solution works perfectly fine if that is exactly your requirement, but in my particular use case, I still needed to support all of the fun things a normal <code>Row</code> did: weight, alignment, arrangement, etc.</p>
<p>So, for better or worse, the solution I landed on was to copy the <code>Row</code> implementation and make changes to the measure policy of <code>Row</code> to add the behavior I wanted to create.</p>
<h3 id="heading-examine-the-problem">Examine The Problem</h3>
<p>The behavior that is problematic lies in <a target="_blank" href="https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.kt#135">this</a> line of code.</p>
<p>When a child element in the Row is asking for how much room it can have, it takes the remaining amount (forcing it to be at least 0). This exactly describes the problematic behavior from the examples in our intro. When the first element lays out, it takes up as much room as it wants, and then when the second element gets its constraints, the remaining amount of space left is 0, so it doesn't have anywhere to go in the layout.</p>
<p>This area will be our main focus point for our custom solution.</p>
<p>The logic I wanted to follow was something like this:</p>
<p>Grab the max width the current element want to use</p>
<p>Then, calculate the largest part an element could possibly occupy, which is proportional to the max width of the container divided by the number of elements. For example, if you have a Row and there are 5 elements, then assuming all 5 elements want to render at their max width, the largest part an element could take up is 1/5 of the container size (also accounts for elements with a weight)</p>
<p>If the max width that I want to take up is larger than the largest part that I could take up, then I need to calculate how much space I can take up (assuming the other elements don't want max space)</p>
<p>This is done by subtracting the remaining desired width of the other elements, and the elements that have already been laid out, from the max width I want to take up.</p>
<p>In code form, that translates to the following:</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// grab the max intrinsix width of elements</span>
<span class="hljs-keyword">val</span> results = IntArray(measurables.size) { <span class="hljs-number">0</span> }
<span class="hljs-keyword">for</span> (i <span class="hljs-keyword">in</span> startIndex until endIndex) {
    results[i] = measurables[i].maxIntrinsicWidth(<span class="hljs-number">0</span>).coerceAtMost(mainAxisMax)
}

<span class="hljs-keyword">val</span> maxWidthIWant = results[i]
<span class="hljs-keyword">val</span> largestPartDividend =
    <span class="hljs-keyword">if</span> (results.all { it == <span class="hljs-number">0</span> }) {
        <span class="hljs-comment">// if row is all images or something that has no intrinsic size</span>
        <span class="hljs-comment">// and we don't know it ahead of time</span>
        measurables.size.coerceAtLeast(<span class="hljs-number">1</span>)
    } <span class="hljs-keyword">else</span> {
        measurables.filter { it.rowColumnParentData.weight &lt;= <span class="hljs-number">0f</span> }.size.coerceAtLeast(<span class="hljs-number">1</span>)
    }
<span class="hljs-comment">// largest part is determined by the max width of the container divided by the number of elements that don't have weight</span>
<span class="hljs-keyword">val</span> largestPart = mainAxisMax / largestPartDividend

<span class="hljs-keyword">if</span> (maxWidthIWant &gt; largestPart) {
    <span class="hljs-keyword">val</span> remainingDesiredWidthAfterThisElement = results.drop(i + <span class="hljs-number">1</span>).sum()
    <span class="hljs-keyword">val</span> alreadyConsumedWidth = mainAxisMax - remaining
    <span class="hljs-keyword">val</span> maxWidthICanTakeUp =
        (
            maxWidthIWant - remainingDesiredWidthAfterThisElement -
                alreadyConsumedWidth
        ).coerceAtLeast(
            largestPart,
        )
    remaining.coerceAtLeast(<span class="hljs-number">0</span>).coerceAtMost(maxWidthICanTakeUp)
} <span class="hljs-keyword">else</span> {
    remaining.coerceAtLeast(<span class="hljs-number">0</span>).coerceAtMost(largestPart)
}
</code></pre>
<p>Please note, I haven't fully benchmarked this solution (yet). At best, it's the same as row, but at worse, it's performing additional <code>maxIntrinsicWidth</code>() calls, so I'd imagine there is a performance hit that I haven’t had a chance to fully benchmark yet. If performance is critical, I'd verify it works for your use case first.</p>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p>After implementing my custom row which I called <code>AdaptiveRow</code>, we are now able to layout elements in our row with behavior similar to other platforms.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744555517296/31c438b8-51de-4f74-b986-e65085baedf8.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-prior-art">Prior Art</h2>
<ol>
<li><a target="_blank" href="https://github.com/HedvigInsurance/android/blob/develop/app%2Fdesign-system%2Fdesign-system-hedvig%2Fsrc%2Fmain%2Fkotlin%2Fcom%2Fhedvig%2Fandroid%2Fdesign%2Fsystem%2Fhedvig%2FHorizontalItems.kt#L10-L100">https://github.com/HedvigInsurance/android/blob/develop/app%2Fdesign-system%2Fdesign-system-hedvig%2Fsrc%2Fmain%2Fkotlin%2Fcom%2Fhedvig%2Fandroid%2Fdesign%2Fsystem%2Fhedvig%2FHorizontalItems.kt#L10-L100</a></li>
</ol>
<p>Cover Photo by <a target="_blank" href="https://unsplash.com/@karsten116?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Karsten Winegeart</a> on <a target="_blank" href="https://unsplash.com/photos/mountains-reflect-in-a-dark-wet-landscape-24h4vWNHjJE?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Unsplash</a></p>
]]></content:encoded></item><item><title><![CDATA[Debug Drawers and You: Adding A Debug Drawer To Your App]]></title><description><![CDATA[Said another way, how to supercharge your development workflow with a debug drawer.
As Android developers, we're constantly looking for ways to streamline our workflow.
One often-overlooked tool that can significantly boost your development process i...]]></description><link>https://blog.joetr.com/debug-drawers-and-you-adding-a-debug-drawer-to-your-app</link><guid isPermaLink="true">https://blog.joetr.com/debug-drawers-and-you-adding-a-debug-drawer-to-your-app</guid><category><![CDATA[debug drawer]]></category><category><![CDATA[Android]]></category><category><![CDATA[debugging]]></category><dc:creator><![CDATA[Joe Roskopf]]></dc:creator><pubDate>Thu, 26 Sep 2024 16:43:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1727368951949/5fa23bbf-6651-42d5-9ef2-f8fe4862762b.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Said another way, how to supercharge your development workflow with a debug drawer.</p>
<p>As Android developers, we're constantly looking for ways to streamline our workflow.</p>
<p>One often-overlooked tool that can significantly boost your development process is the debug drawer.</p>
<p>In this post, we'll explore how to implement and leverage debug drawers to take your Android app development to the next level.</p>
<h1 id="heading-what-is-a-debug-drawer">What is a Debug Drawer?</h1>
<p>A debug drawer is a hidden panel in your app that contains various tools and information useful during development and testing.</p>
<p>Think of it as a Swiss Army knife for developers – always there when you need it, but invisible to regular users.</p>
<p><img src="https://github.com/JakeWharton/u2020/raw/master/u2020.gif" alt="dd" /></p>
<h1 id="heading-adding-a-debug-drawer-to-your-app">Adding A Debug Drawer To Your App</h1>
<p>The concept we are going to employ to add a debug drawer to your app is to use debug and release configurations in Gradle to include different modules depending on the build.</p>
<p>To add a debug drawer to your app, we will first start by creating a new module.</p>
<p>In this example, I'll call it <code>:debug-drawer</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727296471592/4b9d10b4-bf07-4974-89e8-ff956ae2ef2c.png" alt class="image--center mx-auto" /></p>
<p>We are also going to want to create a no-op release implementation of this module so we can have different behaviors in debug vs. release builds.</p>
<p>In this example, I'll call it <code>:debug-drawer-release</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727296488777/800635f7-77ff-46ea-8ddf-b35c8bd2e7af.png" alt class="image--center mx-auto" /></p>
<p>Lastly, we are going to create a <code>:debug-drawer-api</code> module for common configurations of our debug drawer feature.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727296500907/75aa79ff-b0a6-4784-9302-3cb338eb0297.png" alt class="image--center mx-auto" /></p>
<p>In our <code>:app</code> module, I'm going to add <code>:debug-drawer</code> to the <code>:app</code> module as a dependency via the <code>debugImplementation</code> Gradle configuration.</p>
<p>I'm also going to add the <code>:debug-drawer-release</code> to the <code>:app</code> module as a dependency via the <code>releaseImplementation</code> Gradle configuration.</p>
<p>Lastly, I'm going to add <code>:debug-drawer-api</code> to the <code>:app</code> module as a dependency via the regular <code>implementation</code> Gradle configuration</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727296509617/3051e0ae-da88-4666-a46d-e334cd0731c5.png" alt class="image--center mx-auto" /></p>
<p>Now comes the actual implementation of opening the debug drawer!</p>
<p>In this example, we will be opening the debug drawer by pressing the volume down key two times.</p>
<p>The main reason for this is most developers on our team use emulators, where sliding to open can be awkward.</p>
<p>Add the following code to the <code>:debug-drawer-api</code> module:</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">DebugOnlyKeyListener</span> </span>{
  <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onKeyDown</span><span class="hljs-params">(event: <span class="hljs-type">KeyEvent</span>)</span></span>
}
</code></pre>
<p>Then, we will modify the <code>:debug-drawer</code> and <code>:debug-drawer-release</code> modules to both depend on the <code>:debug-drawer-api</code> module via the <code>implementation</code> Gradle configuration.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727296704600/3ee2585d-4336-4944-b1b2-95720171ce4a.png" alt class="image--center mx-auto" /></p>
<p>Now, we can move on to actually creating debug and release implementations of <code>DebugOnlyKeyListener</code></p>
<p>We will create classes with the same name that implement the <code>DebugOnlyKeyListener</code>.</p>
<p>The one that lives under <code>:debug-drawer-release</code> will have a no-op implementation, while the one that lives under <code>:debug-drawer</code> will have an actual implementation.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727296732762/ae2eaef3-1d86-42aa-ac10-9a543fd5ccf4.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727296736556/2a438f54-f1e5-4b0e-8f3f-20ed16c97581.png" alt class="image--center mx-auto" /></p>
<p>Now, let's create the actual implementation for <code>DebugOnlyKeyListener</code> in the <code>:debug-drawer</code> module!</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">import</span> android.app.Application
<span class="hljs-keyword">import</span> android.view.KeyEvent
<span class="hljs-keyword">import</span> com.example.debugdrawer.debug.drawer.api.DebugOnlyKeyListener

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DebugOnlyKeyListenerImpl</span></span>(<span class="hljs-keyword">val</span> application: Application) : DebugOnlyKeyListener {
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> count = <span class="hljs-number">0</span>

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onKeyDown</span><span class="hljs-params">(event: <span class="hljs-type">KeyEvent</span>)</span></span> {
        <span class="hljs-keyword">if</span> (event.isVolumeDownPressed()) {
            count++
        } <span class="hljs-keyword">else</span> {
            count = <span class="hljs-number">0</span>
        }
        <span class="hljs-comment">// Look for volume down twice in a row.</span>
        <span class="hljs-keyword">if</span> (count &gt;= <span class="hljs-number">2</span>) {
            count = <span class="hljs-number">0</span>

            <span class="hljs-comment">// start the debug activity</span>
            <span class="hljs-keyword">val</span> intent = Intent(application, DebugDrawerActivity::<span class="hljs-keyword">class</span>.java)
            intent.flags += Intent.FLAG_ACTIVITY_NEW_TASK
            application.startActivity(intent)
        }
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> KeyEvent.<span class="hljs-title">isVolumeDownPressed</span><span class="hljs-params">()</span></span>: <span class="hljs-built_in">Boolean</span> {
        <span class="hljs-keyword">return</span> keyCode == KeyEvent.KEYCODE_VOLUME_DOWN &amp;&amp;
                (action == KeyEvent.ACTION_DOWN || action == KeyEvent.ACTION_UP)
    }
}
</code></pre>
<p>While a fairly trivial implementation, we are just listening for 2 consecutive volume down presses to trigger some functionality.</p>
<p>Now, we can add an Activity to the <code>:debug-drawer</code> module.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">import</span> android.os.Bundle
<span class="hljs-keyword">import</span> androidx.activity.ComponentActivity
<span class="hljs-keyword">import</span> androidx.activity.compose.setContent
<span class="hljs-keyword">import</span> androidx.compose.material3.Text

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DebugDrawerActivity</span> : <span class="hljs-type">ComponentActivity</span></span>() {

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onCreate</span><span class="hljs-params">(savedInstanceState: <span class="hljs-type">Bundle</span>?)</span></span> {
        <span class="hljs-keyword">super</span>.onCreate(savedInstanceState)
        setContent {
            Text(<span class="hljs-string">"Hello from debug drawer!"</span>)
        }
    }
}
</code></pre>
<pre><code class="lang-xml"><span class="hljs-meta">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">manifest</span> <span class="hljs-attr">xmlns:android</span>=<span class="hljs-string">"http://schemas.android.com/apk/res/android"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">application</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">activity</span>
            <span class="hljs-attr">android:name</span>=<span class="hljs-string">".DebugDrawerActivity"</span>
            <span class="hljs-attr">android:launchMode</span>=<span class="hljs-string">"singleInstance"</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">application</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">manifest</span>&gt;</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727296827184/ae9a441e-8d19-4979-81c6-2f7b13d75d2f.png" alt class="image--center mx-auto" /></p>
<p>Lastly, this can all be tied together in the main Activity that you want to trigger the debug drawer from.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727296842830/a581de28-755d-48b1-9df4-bd0c045ff9f7.png" alt class="image--center mx-auto" /></p>
<p>Now, to test a debug build!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727296876905/be744eb0-e486-4e76-8af9-c6238151e57b.gif" alt class="image--center mx-auto" /></p>
<p>The debug Activity correctly opens in a debug build!</p>
<p>Now to test a release build.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727296893085/15f82e70-0d38-45ad-927b-20e20ab4977d.gif" alt class="image--center mx-auto" /></p>
<p>In a release build, nothing happens after pressing the volume down two times.</p>
<p>At this point, you can add any functionality you want to your <code>DebugDrawerActivity</code> like listing feature flags, modifying and viewing network requests, etc.</p>
]]></content:encoded></item><item><title><![CDATA[Compose Guard: Detecting Regressions In Jetpack Compose]]></title><description><![CDATA[As Jetpack Compose becomes more widely used across Android (and Multiplatform projects!), detecting regressions via tooling becomes more important to shift left and detect regressions earlier in the development cycle instead of relying on manually ca...]]></description><link>https://blog.joetr.com/compose-guard-detecting-regressions-in-jetpack-compose</link><guid isPermaLink="true">https://blog.joetr.com/compose-guard-detecting-regressions-in-jetpack-compose</guid><category><![CDATA[Jetpack Compose]]></category><category><![CDATA[compose]]></category><category><![CDATA[Android]]></category><dc:creator><![CDATA[Joe Roskopf]]></dc:creator><pubDate>Wed, 22 May 2024 00:03:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1716331556262/a2ac0945-2e7c-42c0-830b-28abb24b48de.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As Jetpack Compose becomes more widely used across Android (and Multiplatform <a target="_blank" href="https://android-developers.googleblog.com/2024/05/android-support-for-kotlin-multiplatform-to-share-business-logic-across-mobile-web-server-desktop.html?m=1">projects</a>!), detecting regressions via tooling becomes more important to <a target="_blank" href="https://en.wikipedia.org/wiki/Shift-left_testing">shift left</a> and detect regressions earlier in the development cycle instead of relying on manually catching errors.</p>
<p>These types of regressions are helpful to catch for a multitude of reasons. More context can be found <a target="_blank" href="https://github.com/JetBrains/kotlin/blob/master/plugins/compose/design/compiler-metrics.md">here</a>, but below are a few high level reasons:</p>
<ol>
<li><p>If you see a function that is restartable but not skippable, it’s not always a bad sign, but it sometimes is an opportunity to do one of two things:</p>
<ol>
<li><p>Make the function skippable by ensuring all of its parameters are stable</p>
</li>
<li><p>Make the function not restartable by marking it as a <code>@NonRestartableComposable</code></p>
</li>
</ol>
</li>
<li><p>Default expressions should be <code>@static</code> in every case except for the following two cases:</p>
<ol>
<li><p>You are explicitly reading an observable dynamic variable. Composition Locals and state variables are an important example of this. In these cases, you need to rely on the fact that the default expression will be re-executed when the value changes</p>
</li>
<li><p>You are explicitly calling a composable function, such as <code>remember</code>. The most common use case for this is state hoisting</p>
</li>
</ol>
</li>
<li><p>Not all classes need to be stable, but a class being stable unlocks a lot of flexibility for the compose compiler to make optimizations when a stable type is being used in places, which is why it is such an important concept for compose.</p>
</li>
</ol>
<p>To help detect regressions via tooling, you can utilize a new Gradle Plugin called <a target="_blank" href="https://github.com/j-roskopf/ComposeGuard">Compose Guard</a>. Compose Guard allows you to generate compose compiler metrics and detect regressions to those metrics on PRs or release builds on CI.</p>
<p>Compose Guard currently checks for numerous regressions that would be useful to a team:</p>
<ul>
<li><p>New restartable but not skippable <code>@Composables</code> are added</p>
</li>
<li><p>New unstable classes are added (only triggers if they are used as a <code>@Composable</code> parameter)</p>
</li>
<li><p>New <code>@dynamic</code> properties are added to a <code>@Composable</code></p>
</li>
<li><p>New unstable parameters are added to a <code>@Composable</code></p>
</li>
</ul>
<p>By being able catch these regressions to the UI layer in PRs, you can ensure better performance without developers having to manually catch these types of issues in a PR review.</p>
<h2 id="heading-adding-compose-guard-to-your-project">Adding Compose Guard To Your Project</h2>
<p>Compose Guard is available via Maven Central. To start, add the plugin to your root <code>build.gradle.kts</code> file.</p>
<p><img src="https://img.shields.io/maven-central/v/com.joetr.compose.guard/com.joetr.compose.guard.gradle.plugin" alt="Maven Central Version" /></p>
<pre><code class="lang-kotlin">plugins {
    id(<span class="hljs-string">"com.joetr.compose.guard"</span>) version <span class="hljs-string">"&lt;latest version&gt;"</span> apply <span class="hljs-literal">false</span>
}
</code></pre>
<p>Then, you can apply the plugin to any subsequent modules that contain your Compose code. This can be simplified by utilizing convention plugins as well. I will use Now In Android as an example:</p>
<p><a target="_blank" href="https://github.com/android/nowinandroid/blob/main/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt">AndroidApplicationComposeConventionPlugin.kt</a></p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">import</span> com.android.build.api.dsl.ApplicationExtension
<span class="hljs-keyword">import</span> com.google.samples.apps.nowinandroid.configureAndroidCompose
<span class="hljs-keyword">import</span> org.gradle.api.Plugin
<span class="hljs-keyword">import</span> org.gradle.api.Project
<span class="hljs-keyword">import</span> org.gradle.kotlin.dsl.getByType

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AndroidApplicationComposeConventionPlugin</span> : <span class="hljs-type">Plugin</span>&lt;<span class="hljs-type">Project</span>&gt; </span>{
    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">apply</span><span class="hljs-params">(target: <span class="hljs-type">Project</span>)</span></span> {
        with(target) {
            pluginManager.apply(<span class="hljs-string">"com.android.application"</span>)

            <span class="hljs-comment">// apply Compose Guard Plugin</span>
            pluginManager.apply(<span class="hljs-string">"com.joetr.compose.guard"</span>)

            <span class="hljs-keyword">val</span> extension = extensions.getByType&lt;ApplicationExtension&gt;()
            configureAndroidCompose(extension)
        }
    }
}
</code></pre>
<p><a target="_blank" href="https://github.com/android/nowinandroid/blob/main/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt">AndroidLibraryComposeConventionPlugin.kt</a></p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">import</span> com.android.build.gradle.LibraryExtension
<span class="hljs-keyword">import</span> com.google.samples.apps.nowinandroid.configureAndroidCompose
<span class="hljs-keyword">import</span> org.gradle.api.Plugin
<span class="hljs-keyword">import</span> org.gradle.api.Project
<span class="hljs-keyword">import</span> org.gradle.kotlin.dsl.dependencies
<span class="hljs-keyword">import</span> org.gradle.kotlin.dsl.getByType
<span class="hljs-keyword">import</span> org.gradle.kotlin.dsl.kotlin

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AndroidLibraryComposeConventionPlugin</span> : <span class="hljs-type">Plugin</span>&lt;<span class="hljs-type">Project</span>&gt; </span>{
    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">apply</span><span class="hljs-params">(target: <span class="hljs-type">Project</span>)</span></span> {
        with(target) {
            pluginManager.apply(<span class="hljs-string">"com.android.library"</span>)

            <span class="hljs-comment">// apply Compose Guard Plugin</span>
            pluginManager.apply(<span class="hljs-string">"com.joetr.compose.guard"</span>)

            <span class="hljs-keyword">val</span> extension = extensions.getByType&lt;LibraryExtension&gt;()
            configureAndroidCompose(extension)
        }
    }
}
</code></pre>
<p>Now that you've applied the plugin, you will notice new Gradle tasks appear for the project. Compose Guard adds two new tasks for each variant as well as one 'clean' task.</p>
<ul>
<li><p><code>&lt;variant&gt;ComposeCompilerGenerate</code> (example <code>releaseComposeCompilerGenerate</code>)</p>
<ul>
<li>Generate golden compose metrics to compare against</li>
</ul>
</li>
<li><p><code>&lt;variant&gt;ComposeCompilerCheck</code> (example <code>releaseComposeCompilerCheck</code>)</p>
<ul>
<li>Generates new metrics and compares against golden values</li>
</ul>
</li>
<li><p><code>composeCompilerClean</code></p>
<ul>
<li>Deletes all golden and check compiler metrics</li>
</ul>
</li>
</ul>
<p>You can execute a root level <code>prodReleaseComposeCompilerGenerate</code> task to generate the metrics for every module in Now In Android (but you can also just run <code>:app:prodReleaseComposeCompilerGenerate</code> to generate the the metrics for just the <code>:app</code> module.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716330251227/a32f4ce1-7294-48b0-839f-08eab760c99a.png" alt class="image--center mx-auto" /></p>
<p>After running the above task, you should now see metrics generated for every module under the <code>compose_reports</code> folder of that module.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716330339535/ebd1326a-84ab-4456-ba29-702c6188eaab.png" alt class="image--center mx-auto" /></p>
<p>The folder name can be customized via Kotlin DSL in the module's build file.</p>
<pre><code class="lang-kotlin">composeGuard {
    outputDirectory = layout.projectDirectory.dir(<span class="hljs-string">"custom_dir"</span>).asFile
}
</code></pre>
<p>Now that the golden metrics are generated, let's add an unstable class to a <code>@Composable</code>! I picked a random <code>@Composable</code> in the NIA app to demo this feature.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">NewUnstableClass</span></span>(<span class="hljs-keyword">var</span> unstableParameter: <span class="hljs-built_in">Int</span>)

<span class="hljs-meta">@Composable</span>
<span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">LoadingState</span><span class="hljs-params">(modifier: <span class="hljs-type">Modifier</span> = Modifier, unstableParameter: <span class="hljs-type">NewUnstableClass</span>)</span></span> {
    NiaLoadingWheel(
        modifier = modifier
            .fillMaxWidth()
            .wrapContentSize()
            .testTag(<span class="hljs-string">"forYou:loading"</span>),
        contentDesc = stringResource(id = R.string.feature_bookmarks_loading),
    )
}
</code></pre>
<p>Now, if I run <code>./gradlew prodReleaseComposeCompilerCheck</code>, you should see the build fail!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716330648615/fcdb1ef7-6d73-4667-97e3-5b09e9b0c6cd.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-kotlin">* What went wrong:
Execution failed <span class="hljs-keyword">for</span> task <span class="hljs-string">':feature:bookmarks:prodReleaseComposeCompilerCheck'</span>.
&gt; New unstable classes were added! 
  ClassDetail(className=NewUnstableClass, stability=UNSTABLE, runtimeStability=UNSTABLE, fields=[Field(status=stable, details=<span class="hljs-keyword">var</span> unstableParameter: <span class="hljs-built_in">Int</span>)], rawContent=RawContent(content=unstable <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">NewUnstableClass</span> </span>{
    stable <span class="hljs-keyword">var</span> unstableParameter: <span class="hljs-built_in">Int</span>
    &lt;runtime stability&gt; = Unstable
  }))
  More info: https:<span class="hljs-comment">//github.com/androidx/androidx/blob/androidx-main/compose/compiler/design/compiler-metrics.md#classes-that-are-unstable</span>
</code></pre>
<p>By adding this check to PR and release build jobs on CI, you can start to detect these types of UI regressions!</p>
<h2 id="heading-summary">Summary</h2>
<p>By adding Compose Guard to your project, you can start to detect UI regressions via tooling. These UI regressions have a multitude of performance implications for a project, so it's important to try and catch issues as early as possible.</p>
<h2 id="heading-notice">Notice</h2>
<p>I, <a target="_blank" href="https://github.com/j-roskopf">Joseph Roskopf</a>, am one of the authors of this Gradle Plugin along with <a target="_blank" href="https://github.com/JJSwigut">Joshua Swigut</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Adding Custom Messaging In Android Studio On Top of Confusing Gradle Errors Via IntelliJ Plugin]]></title><description><![CDATA[In our Android project at work, we have the separation of Android modules and JVM modules. There are a lot of benefits to this setup, but one side effect of this setup is that JVM modules cannot depend on Android modules. However, Android modules can...]]></description><link>https://blog.joetr.com/adding-custom-messaging-in-android-studio-on-top-of-confusing-gradle-errors-via-intellij-plugin</link><guid isPermaLink="true">https://blog.joetr.com/adding-custom-messaging-in-android-studio-on-top-of-confusing-gradle-errors-via-intellij-plugin</guid><category><![CDATA[Android]]></category><category><![CDATA[gradle]]></category><dc:creator><![CDATA[Joe Roskopf]]></dc:creator><pubDate>Tue, 02 Apr 2024 14:40:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1712068368529/9e4fca1b-08eb-4bb1-8641-e4d28029625f.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In our Android project at work, we have the separation of Android modules and JVM modules. There are a lot of benefits to this setup, but one side effect of this setup is that JVM modules cannot depend on Android modules. However, Android modules can depend on JVM modules, and JVM modules can depend on other JVM modules.</p>
<p>When you add an Android module as a dependency in a JVM module, you are left with a fairly confusing error message.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712060200435/7b9170af-177a-42c7-a198-8054d40d02be.png" alt class="image--center mx-auto" /></p>
<p>I wanted to intercept this and provide a more friendly error message to developers on the team who might run into this issue. We have an internally managed IntelliJ / Android Studio plugin that we use that I thought I could tie into to help with this situation.</p>
<p>There are many solutions to listen to build / sync / compilation status from within an IntelliJ plugin: <a target="_blank" href="https://github.com/JetBrains/android/blob/master/android/src/com/android/tools/idea/gradle/project/sync/GradleSyncListener.java"><code>GradleSyncListener</code></a>, <a target="_blank" href="https://github.com/JetBrains/intellij-community/blob/master/platform/external-system-impl/src/com/intellij/openapi/externalSystem/service/execution/ExternalSystemExecutionAware.kt"><code>ExternalSystemExecutionAware</code></a>, <a target="_blank" href="https://github.com/JetBrains/android/blob/idea/232.10227.8/project-system-gradle/src/com/android/tools/idea/gradle/project/build/GradleBuildListener.java"><code>GradleBuildListener</code></a>, <a target="_blank" href="https://github.com/joewalnes/idea-community/blob/master/java/compiler/openapi/src/com/intellij/openapi/compiler/CompilationStatusListener.java"><code>CompilationStatusListener</code></a>, <a target="_blank" href="https://github.com/JetBrains/intellij-community/blob/master/platform/lang-api/src/com/intellij/task/ProjectTaskListener.java"><code>ProjectTaskListener</code></a>, and <a target="_blank" href="https://github.com/JetBrains/intellij-community/blob/idea/233.14475.28/platform/execution/src/com/intellij/execution/ExecutionListener.java"><code>ExecutionListener</code></a> . However, the issue I was running into was that when I added an Android library / module to a JVM module in my project, it was being reported as a successful build / sync.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712066686093/a9872df4-fe10-45cf-9446-903934c36d62.png" alt class="image--center mx-auto" /></p>
<p>(<code>:coroutines</code> is a sample Android module being added to a JVM module in my project)</p>
<p>My plan was to manually parse build files looking for our internal JVM + Android plugin identifiers and parsing through each module's dependencies if I couldn't find a way to accomplish this via IntelliJ / Gradle APIs.</p>
<p>Luckily, after some more digging, I came across the <a target="_blank" href="https://github.com/JetBrains/intellij-community/blob/idea/233.15026.9/plugins/gradle/src/org/jetbrains/plugins/gradle/issue/GradleIssueChecker.kt">GradleIssueChecker</a> extension point. Using the GradleIssueChecker, we can be alerted when a build fails with a useful error message and report a BuildIssue on top of the original Gradle issue.</p>
<p>To start using the GradleIssueChecker API, there were some dependencies I needed to add to the existing project.</p>
<p>In the <code>build.gradle.kts</code> file for the project I added:</p>
<pre><code class="lang-kotlin">plugins {
    ...
    id(<span class="hljs-string">"idea"</span>) <span class="hljs-comment">// added "idea" plugin</span>
}

intellij {
    pluginName = properties(<span class="hljs-string">"pluginName"</span>)
    version = properties(<span class="hljs-string">"platformVersion"</span>)
    type = properties(<span class="hljs-string">"platformType"</span>)

    plugins = <span class="hljs-string">"android"</span> <span class="hljs-comment">// added "android" plugin</span>
}
</code></pre>
<p>In the <code>resources/META_INF/plugin.xml</code> I added:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">idea-plugin</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- Added --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">depends</span>&gt;</span>com.intellij.gradle<span class="hljs-tag">&lt;/<span class="hljs-name">depends</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">depends</span>&gt;</span>org.jetbrains.android<span class="hljs-tag">&lt;/<span class="hljs-name">depends</span>&gt;</span>

    <span class="hljs-comment">&lt;!-- Added --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">extensions</span> <span class="hljs-attr">defaultExtensionNs</span>=<span class="hljs-string">"org.jetbrains.plugins.gradle"</span>&gt;</span>
        <span class="hljs-comment">&lt;!--suppress PluginXmlValidity --&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">issueChecker</span> <span class="hljs-attr">implementation</span>=<span class="hljs-string">"path to class that implements GradleIssueChecker"</span>/&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">extensions</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">idea-plugin</span>&gt;</span>
</code></pre>
<p>Finally, to implement our own <code>GradleIssueChecker</code> class:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">import</span> com.intellij.build.issue.BuildIssue
<span class="hljs-keyword">import</span> com.intellij.build.issue.BuildIssueQuickFix
<span class="hljs-keyword">import</span> com.intellij.openapi.project.Project
<span class="hljs-keyword">import</span> com.intellij.pom.Navigatable
<span class="hljs-keyword">import</span> org.gradle.<span class="hljs-keyword">internal</span>.component.NoMatchingConfigurationSelectionException
<span class="hljs-keyword">import</span> org.jetbrains.plugins.gradle.issue.GradleIssueChecker
<span class="hljs-keyword">import</span> org.jetbrains.plugins.gradle.issue.GradleIssueData
<span class="hljs-keyword">import</span> org.jetbrains.plugins.gradle.service.execution.GradleExecutionErrorHandler

<span class="hljs-comment">/**
 * Checks for [NoMatchingConfigurationSelectionException] which is often a confusing error
 * and tries to offer a more straight forward error message to developers.
 */</span>
<span class="hljs-meta">@Suppress(<span class="hljs-meta-string">"UnstableApiUsage"</span>)</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CustomGradleIssueChecker</span> : <span class="hljs-type">GradleIssueChecker {</span></span>

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">check</span><span class="hljs-params">(issueData: <span class="hljs-type">GradleIssueData</span>)</span></span>: BuildIssue? {
        <span class="hljs-keyword">val</span> throwable = GradleExecutionErrorHandler.getRootCauseAndLocation(issueData.error).first
        <span class="hljs-keyword">val</span> throwableMessage = throwable.message
        <span class="hljs-keyword">val</span> throwableName = throwable::<span class="hljs-keyword">class</span>.qualifiedName
        <span class="hljs-keyword">val</span> exceptionName = NoMatchingConfigurationSelectionException::<span class="hljs-keyword">class</span>.qualifiedName
        // unsure why a type check of throwable <span class="hljs-keyword">is</span> NoMatchingConfigurationSelectionException does not work
        <span class="hljs-keyword">if</span> (throwableName == exceptionName &amp;&amp; throwableMessage != <span class="hljs-literal">null</span>) {
            // matching some error messaging referring to (I think) the JVM module importing an Android module
            <span class="hljs-keyword">if</span> (throwableMessage.contains(<span class="hljs-string">"attribute 'org.jetbrains.kotlin.platform.type' with value 'jvm'"</span>)) {
                <span class="hljs-keyword">val</span> moduleRegex = Regex(<span class="hljs-string">"""project\s*:\s*(\S+)"""</span>)
                <span class="hljs-keyword">val</span> moduleMatches = moduleRegex.find(throwableMessage)
                <span class="hljs-keyword">val</span> dependencyName = <span class="hljs-keyword">if</span> (moduleMatches != <span class="hljs-literal">null</span> &amp;&amp; moduleMatches.groupValues.isNotEmpty()) {
                    moduleMatches.groupValues[<span class="hljs-number">0</span>]
                } <span class="hljs-keyword">else</span> {
                    <span class="hljs-comment">// If we cannot match a module, try matching the more generic artifact</span>
                    <span class="hljs-keyword">val</span> libraryRegex = Regex(<span class="hljs-string">"""No matching variant of (\S+)"""</span>)
                    <span class="hljs-keyword">val</span> libraryMatches = libraryRegex.find(throwableMessage)
                    <span class="hljs-keyword">if</span> (libraryMatches != <span class="hljs-literal">null</span> &amp;&amp; libraryMatches.groupValues.size &gt;= <span class="hljs-number">2</span>) {
                        libraryMatches.groupValues[<span class="hljs-number">1</span>]
                    } <span class="hljs-keyword">else</span> {
                        <span class="hljs-string">"Unknown"</span>
                    }
                }
                <span class="hljs-keyword">return</span> <span class="hljs-keyword">object</span> : BuildIssue {
                    <span class="hljs-keyword">override</span> <span class="hljs-keyword">val</span> description: String
                        <span class="hljs-keyword">get</span>() = <span class="hljs-string">"""
                            The dependency ('<span class="hljs-variable">$dependencyName</span>') looks to be an Android dependency added to a JVM only module.
                        """</span>.trimIndent()
                    <span class="hljs-keyword">override</span> <span class="hljs-keyword">val</span> quickFixes: List&lt;BuildIssueQuickFix&gt;
                        <span class="hljs-keyword">get</span>() = emptyList()
                    <span class="hljs-keyword">override</span> <span class="hljs-keyword">val</span> title: String
                        <span class="hljs-keyword">get</span>() = <span class="hljs-string">"Android Dependency '<span class="hljs-variable">$dependencyName</span>' Detected In JVM Only Module"</span>

                    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getNavigatable</span><span class="hljs-params">(project: <span class="hljs-type">Project</span>)</span></span>: Navigatable? {
                        <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>
                    }
                }
            }
        }

        <span class="hljs-comment">// not an error we care about</span>
        <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>
    }
}
</code></pre>
<p>In our custom checker, we start at the main entry point of the <code>GradleIssueChecker</code> API - <code>fun check(issueData: GradleIssueData): BuildIssue?</code>. The main API gives us <code>GradleIssueData</code> and it is our job to return an optional <code>BuildIssue</code> to report.</p>
<p>The first thing I want to do is to try and match the error (<code>NoMatchingConfigurationSelectionException)</code> that occurs when an Android module is added to a JVM module. After we match that error, we can do some (probably brittle 😁) string matching on the error message to make sure we are specifically in the scenario we want to match. The last part tries to match either the internal module, <code>project :coroutines</code> , or the Android artifact, <code>No matching variant of androidx.activity:activity:1.8.2</code>, that triggered the error so we can include it in our error message.</p>
<p>This pattern has more examples throughout the Android Studio plugin that can be found on Github <a target="_blank" href="https://github.com/search?q=%22%3A+GradleIssueChecker%22&amp;type=code">here</a>. For example, if you have ever seen the error message <code>Disable Gradle 'offline mode' and sync project</code> - <a target="_blank" href="https://github.com/JetBrains/android/blob/6617b8ffb80c14436ebfbb43271b2047a0e78285/project-system-gradle/src/com/android/tools/idea/gradle/project/sync/errors/InternetConnectionIssueChecker.kt#L32">here</a> is that implementation.</p>
<p>I hope you found this interesting Intellij plugin API as useful as I did!</p>
]]></content:encoded></item><item><title><![CDATA[Dagger SPI - Adding Custom Graph Validation (with KSP and tests!)]]></title><description><![CDATA[From the Dagger site:
The Dagger Service Provider Interface (SPI) is a mechanism to hook into Dagger’s annotation processor and access the same binding graph model that Dagger uses. With the SPI, you can write a plugin that

adds project-specific err...]]></description><link>https://blog.joetr.com/dagger-spi-adding-custom-graph-validation-with-ksp-and-tests</link><guid isPermaLink="true">https://blog.joetr.com/dagger-spi-adding-custom-graph-validation-with-ksp-and-tests</guid><category><![CDATA[dagger spi]]></category><category><![CDATA[ksp]]></category><category><![CDATA[dagger-hilt]]></category><category><![CDATA[dagger]]></category><category><![CDATA[Testing]]></category><category><![CDATA[Android]]></category><category><![CDATA[android app development]]></category><category><![CDATA[shiftlefttesting]]></category><dc:creator><![CDATA[Joe Roskopf]]></dc:creator><pubDate>Sat, 02 Mar 2024 14:16:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1709387238135/40a38ad4-60ee-41cb-9eea-03d07571b5d3.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>From the <a target="_blank" href="https://dagger.dev/dev-guide/spi.html">Dagger</a> site:</p>
<p>The Dagger Service Provider Interface (<a target="_blank" href="https://en.wikipedia.org/wiki/Service_provider_interface">SPI</a>) is a mechanism to hook into Dagger’s annotation processor and access the same binding graph model that Dagger uses. With the SPI, you can write a plugin that</p>
<ul>
<li><p>adds project-specific errors and warnings, e.g. <code>Bindings for android.content.Context must have a @Qualifier</code> or <code>Bindings that implement DatabaseRelated must be scoped</code></p>
</li>
<li><p>generates extra Java source files</p>
</li>
<li><p>serializes the Dagger model to a resource file</p>
</li>
<li><p>builds visualizations of the Dagger model</p>
</li>
<li><p>and more!</p>
</li>
</ul>
<p>The first bullet point is what we are after today.</p>
<p>Moving as many checks as possible to static analysis tools like <a target="_blank" href="https://developer.android.com/studio/write/lint">Lint</a>, <a target="_blank" href="https://github.com/detekt/detekt">Detekt</a>, and SPI plugins is a great way to <a target="_blank" href="https://en.wikipedia.org/wiki/Shift-left_testing">Shift Left</a> and fix these types of issues before a PR is ever raised.</p>
<h1 id="heading-rules">Rules</h1>
<p>Let's start first by defining the custom behavior we want to validate with our plugins, and then we will get to writing tests and incorporating the plugins into our project.</p>
<p>The three plugins we will be writing in this post are:</p>
<ol>
<li><p>Make sure no un-qualified primitives are provided into the Dagger graph. Something like this would be flagged with the plugin:</p>
<ol>
<li><pre><code class="lang-kotlin">  <span class="hljs-meta">@Module</span>
  <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PrimitiveModule</span> </span>{
    <span class="hljs-comment">// should have a qualifier to provide a primitive</span>
    <span class="hljs-meta">@Provides</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">provideString</span><span class="hljs-params">()</span></span>: String {
      <span class="hljs-keyword">return</span> <span class="hljs-string">"test"</span>
    }
  }
</code></pre>
</li>
</ol>
</li>
<li><p>Make sure classes that extend from a single interface are added to the Dagger graph via <code>@Binds</code> instead of <code>@Provides</code> Something like this would not be allowed:</p>
<ol>
<li><pre><code class="lang-kotlin">  <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">TestInterface</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">hello</span><span class="hljs-params">()</span></span>
  }

  <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TestClass</span> : <span class="hljs-type">TestInterface {</span></span>
    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">hello</span><span class="hljs-params">()</span></span> {
      TODO(<span class="hljs-string">"Not yet implemented"</span>)
    }
  }

  <span class="hljs-meta">@Singleton</span> <span class="hljs-meta">@Component(modules = [MyModule::class])</span>
  <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">MyComponent</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">testClass</span><span class="hljs-params">()</span></span>: TestClass
  }

  <span class="hljs-meta">@Module</span> <span class="hljs-keyword">object</span> MyModule {
    <span class="hljs-comment">// should be added via @Binds instead</span>
    <span class="hljs-meta">@Provides</span> <span class="hljs-meta">@Singleton</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">providesTestClass</span><span class="hljs-params">()</span></span> = TestClass()
  }
</code></pre>
</li>
</ol>
</li>
<li><p>Lastly, we want to make sure classes are provided uniquely into the graph between <code>@Inject</code> constructors and <code>@Provides</code> :</p>
<ol>
<li><pre><code class="lang-kotlin">  <span class="hljs-meta">@Singleton</span> <span class="hljs-meta">@Component</span>
  <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">MyComponent</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">dependency</span><span class="hljs-params">()</span></span>: Dependency
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">subcomponent</span><span class="hljs-params">()</span></span>: MySubcomponent
  }

  <span class="hljs-meta">@Subcomponent(modules = [MySubmodule::class])</span>
  <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">MySubcomponent</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">dependency</span><span class="hljs-params">()</span></span>: Dependency
  }

  <span class="hljs-meta">@Module</span> <span class="hljs-keyword">object</span> MySubmodule {
    <span class="hljs-meta">@Provides</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">providesDependency</span><span class="hljs-params">()</span></span> = Dependency(<span class="hljs-string">"Provides"</span>)
  }

  <span class="hljs-comment">// Already added via @Provides, no need for @Inject constructor</span>
  <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Dependency</span> <span class="hljs-meta">@Inject</span> <span class="hljs-keyword">constructor</span></span>(dep: String)
</code></pre>
</li>
</ol>
</li>
</ol>
<h1 id="heading-setup">Setup</h1>
<p>To start, we will want to create a JVM module to host our new plugins. We will also need KSP and the following dependencies:</p>
<pre><code class="lang-kotlin">plugins {
    id(<span class="hljs-string">"org.jetbrains.kotlin.jvm"</span>)
    id(<span class="hljs-string">"com.google.devtools.ksp"</span>)
}

dependencies {
    <span class="hljs-keyword">val</span> daggerVersion = <span class="hljs-string">"2.48"</span>
    <span class="hljs-keyword">val</span> autoServiceKspVersion = <span class="hljs-string">"1.1.0"</span>
    <span class="hljs-keyword">val</span> googleAutoServiceVersion = <span class="hljs-string">"1.1.1"</span>
    <span class="hljs-keyword">val</span> kctVersion = <span class="hljs-string">"0.4.0"</span>

    implementation(<span class="hljs-string">"com.google.dagger:dagger-spi:<span class="hljs-variable">$daggerVersion</span>"</span>)
    ksp(<span class="hljs-string">"dev.zacsweers.autoservice:auto-service-ksp:<span class="hljs-variable">$autoServiceKspVersion</span>"</span>)
    compileOnly(<span class="hljs-string">"com.google.auto.service:auto-service:<span class="hljs-variable">$googleAutoServiceVersion</span>"</span>)

    <span class="hljs-comment">// for testing our SPI plugins</span>
    testImplementation(<span class="hljs-string">"dev.zacsweers.kctfork:core:<span class="hljs-variable">$kctVersion</span>"</span>)
    testImplementation(<span class="hljs-string">"dev.zacsweers.kctfork:ksp:<span class="hljs-variable">$kctVersion</span>"</span>)
    testImplementation(<span class="hljs-string">"com.google.dagger:dagger-compiler:<span class="hljs-variable">$daggerVersion</span>"</span>)
}
</code></pre>
<p>Within our newly created module, assuming it was called <code>:dagger-spi-plugins</code> , we will want to add our first plugin.</p>
<p>The following plugin makes sure all primitive types are provided with a qualifier:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">import</span> com.google.auto.service.AutoService
<span class="hljs-keyword">import</span> dagger.spi.model.BindingGraph
<span class="hljs-keyword">import</span> dagger.spi.model.BindingGraphPlugin
<span class="hljs-keyword">import</span> dagger.spi.model.DiagnosticReporter
<span class="hljs-keyword">import</span> javax.tools.Diagnostic

<span class="hljs-comment">// list of class types we are interested in</span>
<span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> PRIMITIVES =
  listOf(
    Integer::<span class="hljs-keyword">class</span>.java.name,
    String::<span class="hljs-keyword">class</span>.java.name,
    <span class="hljs-built_in">Boolean</span>::<span class="hljs-keyword">class</span>.java.name,
    <span class="hljs-built_in">Float</span>::<span class="hljs-keyword">class</span>.java.name,
    <span class="hljs-built_in">Double</span>::<span class="hljs-keyword">class</span>.java.name
  )

// Adds the plugin via a Service Loader with Zac Sweer<span class="hljs-string">'s fork of auto-service
@AutoService(BindingGraphPlugin::class)
class PrimitiveValidator : BindingGraphPlugin {

  override fun visitGraph(bindingGraph: BindingGraph, diagnosticReporter: DiagnosticReporter) {
    bindingGraph
       // grab all bindings
      .bindings()
       // filter the types if they are contained in our primitives list and don'</span>t have a qualifier
      .filter { binding -&gt;
        <span class="hljs-keyword">val</span> key = binding.key()
        PRIMITIVES.contains(key.type().toString()) &amp;&amp; !key.qualifier().isPresent
      }
      .forEach { binding -&gt;
        <span class="hljs-comment">// report an error for any present binding</span>
        diagnosticReporter.reportBinding(
          Diagnostic.Kind.ERROR,
          binding,
          <span class="hljs-string">"Primitives should be annotated with any qualifier"</span>
        )
      }
  }
}
</code></pre>
<p>Now we can add our 2nd plugin. This plugin will make sure classes that extend from a single interface are added to the Dagger graph via <code>@Binds</code> instead of <code>@Provides</code> :</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">import</span> com.google.auto.service.AutoService
<span class="hljs-keyword">import</span> com.google.devtools.ksp.closestClassDeclaration
<span class="hljs-keyword">import</span> com.google.devtools.ksp.symbol.ClassKind
<span class="hljs-keyword">import</span> dagger.spi.model.Binding
<span class="hljs-keyword">import</span> dagger.spi.model.BindingGraph
<span class="hljs-keyword">import</span> dagger.spi.model.BindingGraphPlugin
<span class="hljs-keyword">import</span> dagger.spi.model.BindingKind
<span class="hljs-keyword">import</span> dagger.spi.model.DaggerProcessingEnv
<span class="hljs-keyword">import</span> dagger.spi.model.DiagnosticReporter
<span class="hljs-keyword">import</span> javax.tools.Diagnostic

<span class="hljs-meta">@AutoService(BindingGraphPlugin::class)</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BindsValidator</span> : <span class="hljs-type">BindingGraphPlugin {</span></span>

  <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">visitGraph</span><span class="hljs-params">(bindingGraph: <span class="hljs-type">BindingGraph</span>, diagnosticReporter: <span class="hljs-type">DiagnosticReporter</span>)</span></span> {
    <span class="hljs-comment">// grab all bindings</span>
    bindingGraph.bindings()
      .asSequence()
      <span class="hljs-comment">// filter for things being provided via @Provides</span>
      .filter { it.kind() == BindingKind.PROVISION }
      .forEach { provisionedBinding -&gt;
        <span class="hljs-comment">// grab all classes with a single super type that is an interface</span>
        <span class="hljs-keyword">val</span> classBindingsWithInterfaceSuperTypes = bindingGraph
          .bindings(provisionedBinding.key())
          .asSequence()
          .filter { binding -&gt;
            <span class="hljs-comment">// we are only interested in the KSP pass, not javac</span>
            <span class="hljs-keyword">if</span> (binding.key().type().backend() == DaggerProcessingEnv.Backend.KSP) {
              binding.key().type().ksp().declaration.closestClassDeclaration()?.let { closestClassDeclaration -&gt;
                <span class="hljs-comment">// if the binding we are visiting is a class</span>
                closestClassDeclaration.classKind == ClassKind.CLASS &amp;&amp;
                  <span class="hljs-comment">// extending from exactly 1 thing</span>
                  closestClassDeclaration.superTypes.count() == <span class="hljs-number">1</span> &amp;&amp;
                  <span class="hljs-comment">// and that thing is an interface</span>
                  closestClassDeclaration.superTypes.first().resolve().declaration.closestClassDeclaration()?.classKind == ClassKind.INTERFACE
              } ?: <span class="hljs-literal">false</span>
            } <span class="hljs-keyword">else</span> {
              <span class="hljs-literal">false</span>
            }
          }
          .toList()

        <span class="hljs-keyword">if</span> (classBindingsWithInterfaceSuperTypes.isNotEmpty()) {
          <span class="hljs-comment">// report an error for each instance</span>
          classBindingsWithInterfaceSuperTypes.forEach {
            diagnosticReporter.reportBinding(
              Diagnostic.Kind.ERROR,
              it,
              <span class="hljs-string">"<span class="hljs-subst">${it.key().type().ksp().declaration.simpleName.getShortName()}</span> should be added to the graph via @Binds instead of @Provides since it extends a single interface"</span>
            )
          }
        }
      }
  }
}
</code></pre>
<p>The last plugin we want to add is one that makes sure classes are provided uniquely into the graph between <code>@Inject</code> constructors and <code>@Provides</code> :</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">import</span> com.google.auto.service.AutoService
<span class="hljs-keyword">import</span> dagger.spi.model.Binding
<span class="hljs-keyword">import</span> dagger.spi.model.BindingGraph
<span class="hljs-keyword">import</span> dagger.spi.model.BindingGraphPlugin
<span class="hljs-keyword">import</span> dagger.spi.model.BindingKind
<span class="hljs-keyword">import</span> dagger.spi.model.DiagnosticReporter
<span class="hljs-keyword">import</span> javax.tools.Diagnostic

<span class="hljs-meta">@AutoService(BindingGraphPlugin::class)</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UniqueInjectBindingValidator</span> : <span class="hljs-type">BindingGraphPlugin {</span></span>

  <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">visitGraph</span><span class="hljs-params">(bindingGraph: <span class="hljs-type">BindingGraph</span>, diagnosticReporter: <span class="hljs-type">DiagnosticReporter</span>)</span></span> {
    bindingGraph
      <span class="hljs-comment">// grab all bindings</span>
      .bindings()
      .asSequence()
      <span class="hljs-comment">// filter for bindings being added via @Inject constructors</span>
      .filter { it.kind() == BindingKind.INJECTION }
      .forEach { injectBinding: Binding -&gt;
        <span class="hljs-comment">// grab other bindings that are not @Inject consturcotrs or member injection</span>
        <span class="hljs-keyword">val</span> otherBindings =
          bindingGraph
            .bindings(injectBinding.key())
            .asSequence()
            <span class="hljs-comment">// Filter out other @Inject bindings</span>
            .filter { it.kind() != BindingKind.INJECTION }
            <span class="hljs-comment">// Filter out other member injection sites</span>
            .filter { it.kind() != BindingKind.MEMBERS_INJECTION }
            .toList()

        <span class="hljs-comment">// we are left with a list of bindings provided via @Provides</span>
        <span class="hljs-comment">// report an error for each one</span>
        <span class="hljs-keyword">if</span> (otherBindings.isNotEmpty()) {
          diagnosticReporter.reportBinding(
            Diagnostic.Kind.ERROR,
            injectBinding,
            <span class="hljs-string">"@Inject constructor has duplicates other bindings: <span class="hljs-subst">${otherBindings.joinToString()}</span>"</span>
          )
        }
      }
  }
}
</code></pre>
<p>Phew, I'm a little tired after writing those plugins. Let's take a a small break and look at a cute puppy.</p>
<p><img src="https://www.usatoday.com/gcdn/presto/2020/03/17/USAT/c0eff9ec-e0e4-42db-b308-f748933229ee-XXX_ThinkstockPhotos-200460053-001.jpg?crop=1170,658,x292,y120&amp;width=1170&amp;height=658&amp;format=pjpg&amp;auto=webp" alt="National Puppy Day 2022: 30 cute puppy photos to make you smile" /></p>
<p>Now that we are feeling refreshed, let's write some unit tests for our plugins! In the test source set for our <code>:dagger-spi-plugins</code> module, we will first add some utility methods to better facilitate our testing.</p>
<p>We will be using Zac Sweer's fork of the <a target="_blank" href="https://github.com/ZacSweers/kotlin-compile-testing">Kotlin Compile Testing</a> library to test our code output, and these utility functions just make our test setup easier.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">import</span> com.tschuchort.compiletesting.JvmCompilationResult
<span class="hljs-keyword">import</span> com.tschuchort.compiletesting.KotlinCompilation
<span class="hljs-keyword">import</span> com.tschuchort.compiletesting.SourceFile
<span class="hljs-keyword">import</span> com.tschuchort.compiletesting.kspArgs
<span class="hljs-keyword">import</span> com.tschuchort.compiletesting.kspSourcesDir
<span class="hljs-keyword">import</span> com.tschuchort.compiletesting.kspWithCompilation
<span class="hljs-keyword">import</span> com.tschuchort.compiletesting.symbolProcessorProviders
<span class="hljs-keyword">import</span> dagger.<span class="hljs-keyword">internal</span>.codegen.KspComponentProcessor
<span class="hljs-keyword">import</span> org.intellij.lang.annotations.Language
<span class="hljs-keyword">import</span> java.io.File
<span class="hljs-keyword">import</span> org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi

<span class="hljs-keyword">internal</span> <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">KotlinCompiler</span> </span>{
  <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">compile</span><span class="hljs-params">(<span class="hljs-keyword">vararg</span> sourceFiles: <span class="hljs-type">SourceFile</span>)</span></span>: CompilationResult
}

<span class="hljs-meta">@OptIn(ExperimentalCompilerApi::class)</span>
<span class="hljs-keyword">internal</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">KspKotlinCompiler</span></span>(tempDir: File) : KotlinCompiler {

  <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> compiler =
    KotlinCompilation().apply {
      inheritClassPath = <span class="hljs-literal">true</span>
      symbolProcessorProviders =
        listOf(
          KspComponentProcessor.Provider.withTestPlugins(
            <span class="hljs-comment">// our 3 custom plugins written earlier</span>
            BindsValidator(),
            PrimitiveValidator(),
            UniqueInjectBindingValidator()
          ),
        )
      kspArgs = getKspArguments()
      kspWithCompilation = <span class="hljs-literal">true</span>
      verbose = <span class="hljs-literal">false</span>
      workingDir = tempDir
    }

  <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">compile</span><span class="hljs-params">(<span class="hljs-keyword">vararg</span> sourceFiles: <span class="hljs-type">SourceFile</span>)</span></span>: CompilationResult {
    compiler.sources = sourceFiles.asList()
    <span class="hljs-keyword">return</span> CompilationResult(
      compiler.compile(),
      findGeneratedFiles(compiler.kspSourcesDir),
      compiler.workingDir.resolve(<span class="hljs-string">"sources"</span>),
    )
  }
}

<span class="hljs-keyword">internal</span> <span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CompilationResult</span></span>
<span class="hljs-meta">@OptIn(ExperimentalCompilerApi::class)</span>
<span class="hljs-keyword">constructor</span>(
  <span class="hljs-keyword">val</span> result: JvmCompilationResult,
  <span class="hljs-keyword">val</span> generatedFiles: List&lt;File&gt;,
  <span class="hljs-keyword">val</span> sourcesDir: File,
)

<span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">findGeneratedFiles</span><span class="hljs-params">(file: <span class="hljs-type">File</span>)</span></span>: List&lt;File&gt; {
  <span class="hljs-keyword">return</span> file.walkTopDown().filter { it.isFile }.toList()
}

<span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getKspArguments</span><span class="hljs-params">()</span></span> =
  mutableMapOf(
    <span class="hljs-string">"dagger.fullBindingGraphValidation"</span> to <span class="hljs-string">"ERROR"</span>,
  )

<span class="hljs-keyword">internal</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">createSource</span><span class="hljs-params">(<span class="hljs-meta">@Language(<span class="hljs-meta-string">"kotlin"</span>)</span> code: <span class="hljs-type">String</span>)</span></span>: SourceFile {
  <span class="hljs-keyword">return</span> SourceFile.kotlin(<span class="hljs-string">"<span class="hljs-subst">${code.findFullQualifiedName()}</span>.kt"</span>, code)
}

<span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> String.<span class="hljs-title">findFullQualifiedName</span><span class="hljs-params">()</span></span>: String {
  <span class="hljs-keyword">val</span> packageRegex = <span class="hljs-string">"package (.*)"</span>.toRegex()
  <span class="hljs-keyword">val</span> packageName = packageRegex.find(<span class="hljs-keyword">this</span>)?.groupValues?.<span class="hljs-keyword">get</span>(<span class="hljs-number">1</span>)?.trim()
  <span class="hljs-keyword">val</span> objectRegex = <span class="hljs-string">"(abstract )?(class|interface|object) ([^ ]*)"</span>.toRegex()
  <span class="hljs-keyword">val</span> objectMatcher = checkNotNull(objectRegex.find(<span class="hljs-keyword">this</span>)) { <span class="hljs-string">"No class/interface/object found"</span> }
  <span class="hljs-keyword">val</span> objectName = objectMatcher.groupValues[<span class="hljs-number">3</span>]
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">if</span> (packageName != <span class="hljs-literal">null</span>) {
    <span class="hljs-string">"<span class="hljs-subst">${packageName.replace(<span class="hljs-string">"."</span>, <span class="hljs-string">"/"</span>)}</span>/<span class="hljs-variable">$objectName</span>"</span>
  } <span class="hljs-keyword">else</span> {
    objectName
  }
}
</code></pre>
<p>Now that we have some helper functions, let's write the first test for the PrimitiveValidator plugin:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">import</span> com.tschuchort.compiletesting.KotlinCompilation
<span class="hljs-keyword">import</span> org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
<span class="hljs-keyword">import</span> org.junit.Rule
<span class="hljs-keyword">import</span> org.junit.Test
<span class="hljs-keyword">import</span> org.junit.rules.TemporaryFolder

<span class="hljs-meta">@OptIn(ExperimentalCompilerApi::class)</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PrimitiveValidatorTest</span> </span>{

  <span class="hljs-meta">@JvmField</span> <span class="hljs-meta">@Rule</span> <span class="hljs-keyword">var</span> folder = TemporaryFolder()

  <span class="hljs-meta">@Test</span>
  <span class="hljs-function"><span class="hljs-keyword">fun</span> `primitives should not be provided without a qualifier`<span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">val</span> compiler = KspKotlinCompiler(tempDir = folder.root)

    <span class="hljs-keyword">val</span> component =
      createSource(
        <span class="hljs-string">"""
            package test

            import dagger.Module
            import dagger.Provides

            @Module
            class PrimitiveModule {

              @Provides
              fun provideString(): String {
                return "test"
              }
            }
            """</span>
          .trimIndent(),
      )

    <span class="hljs-keyword">val</span> compilation = compiler.compile(component)

    assert(compilation.result.exitCode == KotlinCompilation.ExitCode.COMPILATION_ERROR)
    assert(
      compilation.result.messages.contains(<span class="hljs-string">"Primitives should be annotated with any qualifier"</span>)
    )
  }

  <span class="hljs-meta">@Test</span>
  <span class="hljs-function"><span class="hljs-keyword">fun</span> `primitives can be provided with a qualifier`<span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">val</span> compiler = KspKotlinCompiler(tempDir = folder.root)

    <span class="hljs-keyword">val</span> component =
      createSource(
        <span class="hljs-string">"""
            package test

            import dagger.Module
            import dagger.Provides
            import javax.inject.Qualifier

            @Qualifier annotation class PrimitiveQualifier

            @Module
            class PrimitiveModule {

              @Provides
              @PrimitiveQualifier
              fun provideString(): String {
                return "test"
              }
            }
            """</span>
          .trimIndent(),
      )

    <span class="hljs-keyword">val</span> compilation = compiler.compile(component)

    assert(compilation.result.exitCode == KotlinCompilation.ExitCode.OK)
  }
}
</code></pre>
<p>The second test is our happy path - showing a primitive being provided with a qualifier does not trigger any errors. The first test is demonstrating that an error is thrown with our custom message when a primitive is provided.</p>
<p>The second test we will write is for our BindsValidator plugin:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">import</span> com.tschuchort.compiletesting.KotlinCompilation
<span class="hljs-keyword">import</span> org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
<span class="hljs-keyword">import</span> org.junit.Rule
<span class="hljs-keyword">import</span> org.junit.Test
<span class="hljs-keyword">import</span> org.junit.rules.TemporaryFolder

<span class="hljs-meta">@OptIn(ExperimentalCompilerApi::class)</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BindsValidatorTest</span> </span>{

  <span class="hljs-meta">@JvmField</span> <span class="hljs-meta">@Rule</span> <span class="hljs-keyword">var</span> folder = TemporaryFolder()

  <span class="hljs-meta">@Test</span>
  <span class="hljs-function"><span class="hljs-keyword">fun</span> `items should not be provided <span class="hljs-keyword">if</span> they can be bound instead`<span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">val</span> compiler = KspKotlinCompiler(tempDir = folder.root)

    <span class="hljs-keyword">val</span> component =
      createSource(
        <span class="hljs-string">"""
        package com.example

        import dagger.Component
        import dagger.Module
        import dagger.Provides
        import javax.inject.Inject
        import javax.inject.Singleton

          interface TestInterface {
            fun hello()
          }

          class TestClass : TestInterface {
            override fun hello() {
              TODO("Not yet implemented")
            }
          }

          @Singleton @Component(modules = [MyModule::class])
          interface MyComponent {
            fun testClass(): TestClass
          }

          @Module object MyModule {
            @Provides
            @Singleton fun providesTestClass() = TestClass()
          }
            """</span>
          .trimIndent(),
      )

    <span class="hljs-keyword">val</span> compilation = compiler.compile(component)

    assert(compilation.result.exitCode == KotlinCompilation.ExitCode.COMPILATION_ERROR)
    assert(
      compilation.result.messages.contains(
        <span class="hljs-string">"TestClass should be added to the graph via @Binds instead of @Provides since it extends a single interface"</span>
      )
    )
  }

  <span class="hljs-meta">@Test</span>
  <span class="hljs-function"><span class="hljs-keyword">fun</span> `binds work <span class="hljs-keyword">as</span> intended`<span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">val</span> compiler = KspKotlinCompiler(tempDir = folder.root)

    <span class="hljs-keyword">val</span> component =
      createSource(
        <span class="hljs-string">"""
        package com.example

        import dagger.Component
        import dagger.Module
        import dagger.Binds
        import javax.inject.Inject
        import javax.inject.Singleton

          interface TestInterface {
            fun hello()
          }

          class TestClass @Inject constructor() : TestInterface {
            override fun hello() {
              TODO("Not yet implemented")
            }
          }

          @Singleton @Component(modules = [MyModule::class])
          interface MyComponent {
            fun testClass(): TestClass
          }

          @Module interface MyModule {
            @Binds
            @Singleton fun bindsTestClass(testClass: TestClass) : TestInterface
          }
            """</span>
          .trimIndent(),
      )

    <span class="hljs-keyword">val</span> compilation = compiler.compile(component)

    assert(compilation.result.exitCode == KotlinCompilation.ExitCode.OK)
  }
}
</code></pre>
<p>The second test is our happy path showing that <code>TestClass</code> is successfully bound to <code>TestInterface</code> and no errors are thrown while the first test, which <code>@Provides</code> TestClass instead, shows that our plugin is successfully throwing an error.</p>
<p>Our last test will be for the unique binding plugin.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">import</span> com.tschuchort.compiletesting.KotlinCompilation
<span class="hljs-keyword">import</span> org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
<span class="hljs-keyword">import</span> org.junit.Rule
<span class="hljs-keyword">import</span> org.junit.Test
<span class="hljs-keyword">import</span> org.junit.rules.TemporaryFolder

<span class="hljs-meta">@OptIn(ExperimentalCompilerApi::class)</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UniqueInjectBindingValidatorTest</span> </span>{

  <span class="hljs-meta">@JvmField</span> <span class="hljs-meta">@Rule</span> <span class="hljs-keyword">var</span> folder = TemporaryFolder()

  <span class="hljs-meta">@Test</span>
  <span class="hljs-function"><span class="hljs-keyword">fun</span> `items should not be bound <span class="hljs-keyword">in</span> multiple ways`<span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">val</span> compiler = KspKotlinCompiler(tempDir = folder.root)

    <span class="hljs-keyword">val</span> component =
      createSource(
        <span class="hljs-string">"""
        package com.example

        import dagger.Component
        import dagger.Module
        import dagger.Provides
        import dagger.Subcomponent
        import javax.inject.Inject
        import javax.inject.Singleton

        @Singleton @Component
        interface MyComponent {
          fun dependency(): Dependency
          fun subcomponent(): MySubcomponent
        }

        @Subcomponent(modules = [MySubmodule::class])
        interface MySubcomponent {
          fun dependency(): Dependency
        }

        @Module object MySubmodule {
          @Provides fun providesDependency() = Dependency("Provides")
        }

        class Dependency @Inject constructor(dep: String)            
          """</span>
          .trimIndent(),
      )

    <span class="hljs-keyword">val</span> compilation = compiler.compile(component)

    assert(compilation.result.exitCode == KotlinCompilation.ExitCode.COMPILATION_ERROR)
    assert(compilation.result.messages.contains(<span class="hljs-string">"com.example.Dependency is bound multiple times"</span>))
  }

  <span class="hljs-meta">@Test</span>
  <span class="hljs-function"><span class="hljs-keyword">fun</span> `no errors <span class="hljs-keyword">when</span> bound once`<span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">val</span> compiler = KspKotlinCompiler(tempDir = folder.root)

    <span class="hljs-keyword">val</span> component =
      createSource(
        <span class="hljs-string">"""
        package com.example

        import dagger.Component
        import dagger.Module
        import dagger.Provides
        import javax.inject.Inject
        import javax.inject.Singleton

        @Singleton @Component(modules = [MyModule::class])
        interface MyComponent {
          fun dependency(): Dependency
        }

        @Module object MyModule {
          @Provides fun providesDependency() = Dependency("Provides")
        }

        class Dependency(dep: String)
            """</span>
          .trimIndent(),
      )

    <span class="hljs-keyword">val</span> compilation = compiler.compile(component)

    assert(compilation.result.exitCode == KotlinCompilation.ExitCode.OK)
  }
}
</code></pre>
<p>Similarly, the second test is our happy path showing no errors, while the first test shows an error when <code>Dependency</code> is added via both <code>@Inject</code> constructor as well as <code>@Provides</code> .</p>
<h1 id="heading-consuming-plugins-in-your-app">Consuming Plugins In Your App</h1>
<p>Now that we've written our plugins and the accompanying tests, we want to actually consume them in our project.</p>
<p>To do that, we need to add the following to our app's build file:</p>
<pre><code class="lang-kotlin">android {
    defaultConfig {
        ...
        ksp { arg(<span class="hljs-string">"dagger.fullBindingGraphValidation"</span>, <span class="hljs-string">"WARNING"</span>) }
    }
}

dependencies {
    ksp(project(<span class="hljs-string">":dagger-spi-plugins"</span>))
}
</code></pre>
<p>After doing so, if we add some test module that violates one of our rules and run <code>./gradlew :app:kspDebugKotlin</code>, we should see the build fail successfully!</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Module</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PrimitiveModule</span> </span>{
  <span class="hljs-meta">@Provides</span>
  <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">provideString</span><span class="hljs-params">()</span></span>: String {
    <span class="hljs-keyword">return</span> <span class="hljs-string">"test"</span>
  }
}
</code></pre>
<p>Please note, the ksp argument <code>dagger.fullBindingGraphValidation</code> and <code>:dagger-spi-plugins</code> dependency need to be added to every module you want the validation to occur in! So if you have a custom plugin setup, you may want to add the dependencies to your library module setup.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>Developing custom Dagger SPI plugins has been fun! It not only allowed us to tailor dependency injection to our specific needs but also significantly enhanced our development workflow. By writing tests for our plugins, we ensured their reliability and robustness, providing a safety net that encourages innovation without the fear of regression. Furthermore, our support for Kotlin Symbol Processing (KSP) marked a significant step forward, leveraging the power of Kotlin's tooling to streamline and optimize our build processes.</p>
<p>This is not just about enhancing our tooling; it's about embracing a "shift left" mindset. By moving more checks closer to the compiler and integrating static code analysis tools, we've managed to catch potential issues much earlier in the development cycle. This approach not only saves time and resources but also fosters a culture of quality and responsibility across a team.</p>
]]></content:encoded></item><item><title><![CDATA[Debouncing Clicks in Compose]]></title><description><![CDATA[I recently needed to add a debouncing operator on my button clicks in a Compose multiplatform project. I thought I'd share my solution in case it was useful to someone else.
First, we define an EventProcessor interface. The concrete implementation wi...]]></description><link>https://blog.joetr.com/debouncing-clicks-in-compose</link><guid isPermaLink="true">https://blog.joetr.com/debouncing-clicks-in-compose</guid><category><![CDATA[compose]]></category><category><![CDATA[debouncing]]></category><dc:creator><![CDATA[Joe Roskopf]]></dc:creator><pubDate>Wed, 29 Nov 2023 15:19:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1701271091208/51fc039b-d4ab-4e28-8211-cc6e8f8cf489.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I recently needed to add a debouncing operator on my button clicks in a Compose multiplatform project. I thought I'd share my solution in case it was useful to someone else.</p>
<p>First, we define an <code>EventProcessor</code> interface. The concrete implementation will be responsible for debouncing our clicks.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">internal</span> <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">EventProcessor</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">processEvent</span><span class="hljs-params">(event: () -&gt; <span class="hljs-type">Unit</span>)</span></span>

    <span class="hljs-keyword">companion</span> <span class="hljs-keyword">object</span> {
        <span class="hljs-keyword">val</span> buttonClickMap = mutableMapOf&lt;String, EventProcessor&gt;()
    }
}
</code></pre>
<p>The implementation:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">private</span> <span class="hljs-keyword">const</span> <span class="hljs-keyword">val</span> DEBOUNCE_TIME_MILLIS = <span class="hljs-number">1000L</span>

<span class="hljs-keyword">private</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">EventProcessorImpl</span> : <span class="hljs-type">EventProcessor {</span></span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> now: <span class="hljs-built_in">Long</span>
        <span class="hljs-comment">// this is being used in Compose multiplatform </span>
        <span class="hljs-comment">// switch out with whatever millisecond provider you want</span>
        <span class="hljs-keyword">get</span>() = Clock.System.now().toEpochMilliseconds()

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> lastEventTimeMs: <span class="hljs-built_in">Long</span> = <span class="hljs-number">0</span>

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">processEvent</span><span class="hljs-params">(event: () -&gt; <span class="hljs-type">Unit</span>)</span></span> {
        <span class="hljs-keyword">if</span> (now - lastEventTimeMs &gt;= DEBOUNCE_TIME_MILLIS) {
            event.invoke()
        }
        lastEventTimeMs = now
    }
}
</code></pre>
<p>The <code>Composable</code> function where our <code>EventProcessor</code> will be used and a helper function for getting this button's <code>EventProcessor</code></p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">internal</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> EventProcessor.Companion.<span class="hljs-title">get</span><span class="hljs-params">(id: <span class="hljs-type">String</span>)</span></span>: EventProcessor {
    <span class="hljs-keyword">return</span> buttonClickMap.getOrPut(
        id
    ) {
        EventProcessorImpl()
    }
}

<span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">debouncedClick</span><span class="hljs-params">(
    id: <span class="hljs-type">String</span> = randomUUID()</span></span>,
    onClick: () -&gt; <span class="hljs-built_in">Unit</span>,
): () -&gt; <span class="hljs-built_in">Unit</span> {
    <span class="hljs-keyword">val</span> multipleEventsCutter = remember { EventProcessor.<span class="hljs-keyword">get</span>(id) }
    <span class="hljs-keyword">val</span> newOnClick: () -&gt; <span class="hljs-built_in">Unit</span> = {
        multipleEventsCutter.processEvent { onClick() }
    }
    <span class="hljs-keyword">return</span> newOnClick
}
</code></pre>
<p>Here is an example of it being used:</p>
<pre><code class="lang-kotlin">PrimaryButton(
    onClick = debouncedClick {
        <span class="hljs-comment">// handle click</span>
    },
) {
    Text(<span class="hljs-string">"Button"</span>)
}
</code></pre>
<p>Full code:</p>
<p><a target="_blank" href="https://gist.github.com/j-roskopf/990baa5beef767fbb2fae8cce33e2529">https://gist.github.com/j-roskopf/990baa5beef767fbb2fae8cce33e2529</a></p>
]]></content:encoded></item><item><title><![CDATA[Adding "Debug" Checks To Your Compose Multiplatform Project: Android, iOS, and Desktop]]></title><description><![CDATA[While working on my recent Compose Multiplatform project, Sync Sphere, which targets iOS, Android, and Desktop, I needed to add some debug checks in my application to perform some different functionality when in debug mode.
When debugging (which I me...]]></description><link>https://blog.joetr.com/adding-debug-checks-to-your-compose-multiplatform-project-android-ios-and-desktop</link><guid isPermaLink="true">https://blog.joetr.com/adding-debug-checks-to-your-compose-multiplatform-project-android-ios-and-desktop</guid><category><![CDATA[compose multiplatform]]></category><category><![CDATA[Android]]></category><category><![CDATA[iOS]]></category><category><![CDATA[desktop]]></category><category><![CDATA[debugging]]></category><dc:creator><![CDATA[Joe Roskopf]]></dc:creator><pubDate>Mon, 20 Nov 2023 01:05:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1700442284657/74c209ee-7db5-4d85-9f84-1931170ece91.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>While working on my recent Compose Multiplatform project, <a target="_blank" href="https://github.com/j-roskopf/SyncSphere">Sync Sphere</a>, which targets iOS, Android, and Desktop, I needed to add some debug checks in my application to perform some different functionality when in debug mode.</p>
<p>When debugging (which I mean to be developing locally), I wanted to target a different table for my remote database, so I wasn't cluttering up production data with all of my local development data.</p>
<p>The solution wasn't super obvious to me when I originally set out to solve this problem, so I wanted to share what ended up working for me.</p>
<p>In my <code>commonMain</code>, I defined a <code>BuildConfig</code> interface and an expect declaration for my <code>BuildConfigImpl</code></p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">BuildConfig</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">isDebug</span><span class="hljs-params">()</span></span>: <span class="hljs-built_in">Boolean</span>
}

<span class="hljs-keyword">expect</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BuildConfigImpl</span> : <span class="hljs-type">BuildConfig</span></span>
</code></pre>
<p>In my <code>androidMain</code> src set, it looks pretty familiar to an Android developer:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">actual</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BuildConfigImpl</span></span>(<span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> applicationContext: Context) : BuildConfig {
    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">isDebug</span><span class="hljs-params">()</span></span>: <span class="hljs-built_in">Boolean</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-number">0</span> != applicationContext.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
    }
}
</code></pre>
<p>In my <code>iosMain</code> src set, it looks pretty familiar to an iOS developer. (I think at least, I'm not a good iOS developer 😅)</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">import</span> kotlin.experimental.ExperimentalNativeApi

<span class="hljs-keyword">actual</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BuildConfigImpl</span></span>() : BuildConfig {

    <span class="hljs-meta">@OptIn(ExperimentalNativeApi::class)</span>
    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">isDebug</span><span class="hljs-params">()</span></span>: <span class="hljs-built_in">Boolean</span> {
        <span class="hljs-keyword">return</span> Platform.isDebugBinary
    }
}
</code></pre>
<p>In my Desktop target and src set, <code>desktopMain</code> , I use System Properties to define a local debug flag to pass along to my application so it can be flagged true when I'm working locally.</p>
<p>First, I added a new property to my local <code>gradle.properties</code> located in my Gradle home directory (<code>~/.gradle/gradle.properties</code>)</p>
<pre><code class="lang-bash">systemProp.syncSphereDebug=<span class="hljs-literal">true</span>
</code></pre>
<p>Then, in my <code>desktopApp</code> build file, I added the following code:</p>
<pre><code class="lang-kotlin">tasks.withType&lt;JavaExec&gt;().configureEach {
    systemProperty(<span class="hljs-string">"syncSphereDebug"</span>, System.getProperty(<span class="hljs-string">"syncSphereDebug"</span>))
}
</code></pre>
<p>This allows me to now have an implementation for <code>BuildConfigImpl</code> for Desktop that looks like:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">actual</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BuildConfigImpl</span></span>() : BuildConfig {
    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">isDebug</span><span class="hljs-params">()</span></span>: <span class="hljs-built_in">Boolean</span> {
        <span class="hljs-keyword">val</span> debugSystemProperty = System.getProperty(<span class="hljs-string">"syncSphereDebug"</span>)
        <span class="hljs-keyword">return</span> debugSystemProperty?.toBoolean() ?: <span class="hljs-literal">false</span>
    }
}
</code></pre>
<p>I hope this post was helpful where I covered enabling some sort of debug / local environment for providing different behavior when developing your Compose Multiplatform project!</p>
]]></content:encoded></item><item><title><![CDATA[Sharing Code Across Multiple Targets In Compose Multiplatform]]></title><description><![CDATA[While working on my recent Compose Multiplatform app, Sync Sphere, I added Desktop as a new target. Now supporting Android, iOS, and Desktop, there was some specific code that I wanted to share and have the same between Android and iOS, but differ on...]]></description><link>https://blog.joetr.com/sharing-code-across-multiple-targets-in-compose-multiplatform</link><guid isPermaLink="true">https://blog.joetr.com/sharing-code-across-multiple-targets-in-compose-multiplatform</guid><category><![CDATA[compose multiplatform]]></category><category><![CDATA[code sharing]]></category><dc:creator><![CDATA[Joe Roskopf]]></dc:creator><pubDate>Fri, 17 Nov 2023 14:34:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1700231668527/ea3fb680-19ab-4497-bda8-014e7d651d67.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>While working on my recent Compose Multiplatform app, <a target="_blank" href="https://github.com/j-roskopf/SyncSphere">Sync Sphere</a>, I added Desktop as a new target. Now supporting Android, iOS, and Desktop, there was some specific code that I wanted to share and have the same between Android and iOS, but differ on Desktop.</p>
<p>One solution is to have the code duplicated and live under <code>androidMain</code> and <code>iosMain</code> under the shared module. However, duplicating code like that isn't a good path forward for a multitude of reasons.</p>
<p>Adding a shared source set between Android and iOS was a perfect solution for me here, allowing me to have one definition for both mobile platforms.</p>
<p>Starting with the interface and class definitions in <code>commonMain</code></p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">RoomRepository</span> </span>{
    ...
}
</code></pre>
<pre><code class="lang-kotlin"><span class="hljs-keyword">expect</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RoomRepositoryImpl</span></span>(
    dictionary: Dictionary,
    <span class="hljs-comment">// CrashReporting is also an interface that has expect / actual</span>
    <span class="hljs-comment">// implementations that are the same on mobile, but differ on Desktop</span>
    crashReporting: CrashReporting,
) : RoomRepository
</code></pre>
<p>In <code>desktopMain</code>, I can define my <code>RoomRepositoryImpl</code> like normal</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">actual</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RoomRepositoryImpl</span> <span class="hljs-title">actual</span> <span class="hljs-keyword">constructor</span></span>(
    dictionary: Dictionary,
    crashReporting: CrashReporting,
) : RoomRepository {
    ... Desktop specific implementation
}
</code></pre>
<p>Now on the mobile side, I created a new folder under the <code>shared</code> module called <code>mobileMain</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700231354562/5187e281-7d1f-4070-b8b3-6914466234d9.png" alt class="image--center mx-auto" /></p>
<p>I register <code>mobileMain</code> as a source set in the <code>shared</code> module's build file like so:</p>
<pre><code class="lang-kotlin">        <span class="hljs-keyword">val</span> mobileMain <span class="hljs-keyword">by</span> creating {
            androidMain.dependsOn(<span class="hljs-keyword">this</span>)
            iosMain.dependsOn(<span class="hljs-keyword">this</span>)
            dependencies {
                dependsOn(commonMain)

                .. other dependencies
            }
        }
</code></pre>
<p>And now, I am able to put my common mobile implementation for <code>RoomRepository</code> that can be shared between Android and iOS!</p>
]]></content:encoded></item><item><title><![CDATA[Seamless Typography in Action: Implementing Custom Fonts in Compose Multiplatform for Android, iOS, and Desktop]]></title><description><![CDATA[I recently needed to use a custom font for my Compose multiplatform project Sync Sphere and I thought I'd share my solution in case it was helpful to anyone else.
First, we need to add a font to commonMain/resources/font

For this example, I have gon...]]></description><link>https://blog.joetr.com/seamless-typography-in-action-implementing-custom-fonts-in-compose-multiplatform-for-android-ios-and-desktop</link><guid isPermaLink="true">https://blog.joetr.com/seamless-typography-in-action-implementing-custom-fonts-in-compose-multiplatform-for-android-ios-and-desktop</guid><category><![CDATA[compose multiplatform]]></category><category><![CDATA[fonts]]></category><category><![CDATA[Android]]></category><category><![CDATA[iOS]]></category><dc:creator><![CDATA[Joe Roskopf]]></dc:creator><pubDate>Wed, 15 Nov 2023 13:43:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1700055777369/bc6b8bf4-4ec1-472a-9d5a-fc3da95c4e19.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I recently needed to use a custom font for my Compose multiplatform project <a target="_blank" href="https://github.com/j-roskopf/SyncSphere">Sync Sphere</a> and I thought I'd share my solution in case it was helpful to anyone else.</p>
<p>First, we need to add a font to <code>commonMain/resources/font</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700054612021/e631f444-b386-4f8a-8ea7-8849d0d3f27d.png" alt class="image--center mx-auto" /></p>
<p>For this example, I have gone with <code>dancingscript_regular.ttf</code> The resource name should follow android resource conventions and be all lowercase with underscores if necessary.</p>
<p>I am adding this font to part of an app theme, but this part is optional.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">AppTheme</span><span class="hljs-params">(
    content:
    @<span class="hljs-type">Composable</span>()
        () -&gt; <span class="hljs-type">Unit</span>,
)</span></span> {

    MaterialTheme(
        content = content,
        typography = appTypography,
    )
}
</code></pre>
<p><code>appTypography</code> is defined as a variable that creates a custom typography.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> appTypography : Typography
    <span class="hljs-meta">@Composable</span>
    <span class="hljs-keyword">get</span>() {
        <span class="hljs-keyword">return</span> Typography(
            defaultFontFamily = dancingScriptRegular
        )
    }
</code></pre>
<p><code>dancingScriptRegular</code> is a custom font that we use expect/actual to get a different platform implementation.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> dancingScriptRegular: FontFamily
    <span class="hljs-meta">@Composable</span>
    <span class="hljs-keyword">get</span>() {
        <span class="hljs-keyword">return</span> FontFamily(
            font(
                <span class="hljs-string">"DancingScript"</span>,
                <span class="hljs-string">"dancingscript_regular"</span>,
                FontWeight.Normal,
                FontStyle.Normal,
            ),
        )
    }
</code></pre>
<p>The <code>font</code> function is the main bread and butter of our solution here. This is where we use expect/actual to give Android / iOS / Desktop implementations.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700054883905/80b98fae-0078-45e2-be4f-dce76a77540c.png" alt class="image--center mx-auto" /></p>
<p>Under <code>shared/src/commonMain/kotlin</code>, I created a <code>FontResource.kt</code> class that looks like</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">import</span> androidx.compose.runtime.Composable
<span class="hljs-keyword">import</span> androidx.compose.ui.text.font.Font
<span class="hljs-keyword">import</span> androidx.compose.ui.text.font.FontStyle
<span class="hljs-keyword">import</span> androidx.compose.ui.text.font.FontWeight

<span class="hljs-meta">@Composable</span>
<span class="hljs-keyword">expect</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">font</span><span class="hljs-params">(name: <span class="hljs-type">String</span>, res: <span class="hljs-type">String</span>, weight: <span class="hljs-type">FontWeight</span>, style: <span class="hljs-type">FontStyle</span>)</span></span>: Font
</code></pre>
<p>Under <code>androidMain</code>, <code>iosMain</code>, and <code>desktopMain</code> we will define our actual implemetations.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700054960853/1e33b086-fe5b-4915-947e-1d584cb56aba.png" alt class="image--center mx-auto" /></p>
<p><code>androidMain</code>:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">import</span> androidx.compose.runtime.Composable
<span class="hljs-keyword">import</span> androidx.compose.ui.platform.LocalContext
<span class="hljs-keyword">import</span> androidx.compose.ui.text.font.Font
<span class="hljs-keyword">import</span> androidx.compose.ui.text.font.FontStyle
<span class="hljs-keyword">import</span> androidx.compose.ui.text.font.FontWeight

<span class="hljs-meta">@Composable</span>
<span class="hljs-keyword">actual</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">font</span><span class="hljs-params">(name: <span class="hljs-type">String</span>, res: <span class="hljs-type">String</span>, weight: <span class="hljs-type">FontWeight</span>, style: <span class="hljs-type">FontStyle</span>)</span></span>: Font {
    <span class="hljs-keyword">val</span> context = LocalContext.current
    <span class="hljs-keyword">val</span> id = context.resources.getIdentifier(res, <span class="hljs-string">"font"</span>, context.packageName)
    <span class="hljs-keyword">return</span> Font(id, weight, style)
}
</code></pre>
<p><code>iosMain</code>:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">import</span> androidx.compose.runtime.Composable
<span class="hljs-keyword">import</span> androidx.compose.ui.text.font.Font
<span class="hljs-keyword">import</span> androidx.compose.ui.text.font.FontStyle
<span class="hljs-keyword">import</span> androidx.compose.ui.text.font.FontWeight
<span class="hljs-keyword">import</span> kotlinx.coroutines.runBlocking
<span class="hljs-keyword">import</span> org.jetbrains.compose.resources.ExperimentalResourceApi
<span class="hljs-keyword">import</span> org.jetbrains.compose.resources.resource

<span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> cache: MutableMap&lt;String, Font&gt; = mutableMapOf()

<span class="hljs-meta">@OptIn(ExperimentalResourceApi::class)</span>
<span class="hljs-meta">@Composable</span>
<span class="hljs-keyword">actual</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">font</span><span class="hljs-params">(name: <span class="hljs-type">String</span>, res: <span class="hljs-type">String</span>, weight: <span class="hljs-type">FontWeight</span>, style: <span class="hljs-type">FontStyle</span>)</span></span>: Font {
    <span class="hljs-comment">// use a cache to store fonts and re-use </span>
    <span class="hljs-keyword">return</span> cache.getOrPut(res) {
        <span class="hljs-keyword">val</span> byteArray = runBlocking {
            resource(<span class="hljs-string">"font/<span class="hljs-variable">$res</span>.ttf"</span>).readBytes()
        }
        androidx.compose.ui.text.platform.Font(res, byteArray, weight, style)
    }
}
</code></pre>
<p><code>desktopMain</code>:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">import</span> androidx.compose.runtime.Composable
<span class="hljs-keyword">import</span> androidx.compose.ui.text.font.Font
<span class="hljs-keyword">import</span> androidx.compose.ui.text.font.FontStyle
<span class="hljs-keyword">import</span> androidx.compose.ui.text.font.FontWeight

<span class="hljs-meta">@Composable</span>
<span class="hljs-keyword">actual</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">font</span><span class="hljs-params">(name: <span class="hljs-type">String</span>, res: <span class="hljs-type">String</span>, weight: <span class="hljs-type">FontWeight</span>, style: <span class="hljs-type">FontStyle</span>)</span></span>: Font =
    androidx.compose.ui.text.platform.Font(<span class="hljs-string">"font/<span class="hljs-variable">$res</span>.ttf"</span>, weight, style)
</code></pre>
<p>Lastly, in your shared module <code>build.gradle.kts</code> under the <code>android</code> block, you'll have to add this line so that the Android app will know to use <code>commonMain/resources</code> and a <code>res</code> src directory.</p>
<pre><code class="lang-kotlin">    android {
    compileSdk = (findProperty(<span class="hljs-string">"android.compileSdk"</span>) <span class="hljs-keyword">as</span> String).toInt()
    namespace = <span class="hljs-string">"com.myapplication.common"</span>

    sourceSets[<span class="hljs-string">"main"</span>].manifest.srcFile(<span class="hljs-string">"src/androidMain/AndroidManifest.xml"</span>)
    sourceSets[<span class="hljs-string">"main"</span>].res.srcDirs(<span class="hljs-string">"src/androidMain/res"</span>)

    <span class="hljs-comment">// This is the line to add</span>
    sourceSets[<span class="hljs-string">"main"</span>].res.srcDirs(<span class="hljs-string">"src/commonMain/resources"</span>) <span class="hljs-comment">// &lt;=== This is the line to add</span>
    <span class="hljs-comment">// This is the line to add</span>

    sourceSets[<span class="hljs-string">"main"</span>].resources.srcDirs(<span class="hljs-string">"src/commonMain/resources"</span>)

    defaultConfig {
        minSdk = (findProperty(<span class="hljs-string">"android.minSdk"</span>) <span class="hljs-keyword">as</span> String).toInt()
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
    kotlin {
        jvmToolchain(<span class="hljs-number">17</span>)
    }
}
</code></pre>
<p>And then you should be able to run your project on iOS, Android, and Desktop using a common font.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700055274966/4b32c973-de11-4c47-8264-68b3a8bf4624.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700055287555/061007e4-6e27-4fd0-ad21-1d924187d5db.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700055295920/4ffc2dc2-3e9a-48a3-ac22-bd976d67d29e.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-cocoapods">CocoaPods</h1>
<p>A finishing note about CocoaPods. When I was writing this article, I was using the most up-to-date version of the <a target="_blank" href="https://github.com/JetBrains/compose-multiplatform-template">Compose Multiplatform template</a> on Github from JetBrains as a demo. When I was adding this to my project Sync Sphere, I had to do some additional work to get fonts working on iOS. After doing all of the steps listed above, I also had to run <code>./gradlew :shared:podInstall</code> to get the fonts copied over to iOS, otherwise I was running into a resource not found exception.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>I hope these series of code snippets can prove helpful to someone implementing custom typography in their Compose Multiplatform app.</p>
]]></content:encoded></item><item><title><![CDATA[Integrating iOS into the Fold: Generating an IPA for Compose Multiplatform Projects]]></title><description><![CDATA[Intro
With Kotlin Multiplatform 1.5.10 going stable, now is as good of a time as any to dive into Compose Multiplatform.
With my background in Android development and some familiarity with Jetpack Compose, Compose Multiplatform is a great solution fo...]]></description><link>https://blog.joetr.com/integrating-ios-into-the-fold-generating-an-ipa-for-compose-multiplatform-projects</link><guid isPermaLink="true">https://blog.joetr.com/integrating-ios-into-the-fold-generating-an-ipa-for-compose-multiplatform-projects</guid><category><![CDATA[compose multiplatform]]></category><category><![CDATA[iOS]]></category><dc:creator><![CDATA[Joe Roskopf]]></dc:creator><pubDate>Sat, 04 Nov 2023 17:59:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1699121098487/7c1fa7b3-5a2f-47d8-8b51-b62ce56a343b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-intro">Intro</h2>
<p>With Kotlin Multiplatform 1.5.10 going <a target="_blank" href="https://blog.jetbrains.com/kotlin/2023/08/compose-multiplatform-1-5-0-release/">stable</a>, now is as good of a time as any to dive into <a target="_blank" href="https://blog.jetbrains.com/kotlin/2023/11/compose-multiplatform-1-5-10-release/">Compose Multiplatform</a>.</p>
<p>With my background in Android development and some familiarity with Jetpack Compose, Compose Multiplatform is a great solution for when I need a cross-platform solution for UI + business logic to be shared across the targets that I'm interested in; Android, iOS, and Desktop.</p>
<p>However, my unfamiliarity with the intricacies of iOS and XCode has left me scratching my head from time to time. Don't get me wrong, general development using Compose Multiplatform has been great! I am usually working within Android Studio writing Kotlin, two tools I am extremely familiar with, but recently I wanted to distribute a test version of my iOS app on Firebase for friends and family to test, and that process took some time for me to understand. Today, I wanted to document some of my learnings so it can be helpful for others as well.</p>
<h2 id="heading-problem">Problem</h2>
<p>Using my Android knowledge and some Googling, I have found the generally accepted version of how to generate an IPA file is to open XCode, select your relevant destination (in this example, I am choosing any iOS device, arm64) and select Product -&gt; Archive.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699119879165/6b6b2a8f-01a8-4524-a13d-8e84399ce9a5.png" alt class="image--center mx-auto" /></p>
<p>However, when I do that, I receive an error message about needing Java 17 when I am using Java 16. As an Android engineer, I am aware that projects using AGP 8.0+ are required to use Java 17, however, I am never install Java 16 on my system. I am truly baffled by where that error was coming from. My guess is that XCode must ship with some internal version of Java? What is even more confusing is that I have my Java home set correctly in my <code>.zshrc</code> file and when I input <code>java -version</code> in the terminal, I am met with what I expect to see, Java 17.</p>
<pre><code class="lang-yaml"><span class="hljs-string">joe@Joes-MacBook-Pro</span> <span class="hljs-string">~</span> <span class="hljs-string">%</span> <span class="hljs-string">java</span> <span class="hljs-string">-version</span>
<span class="hljs-string">openjdk</span> <span class="hljs-string">version</span> <span class="hljs-string">"17.0.7"</span> <span class="hljs-number">2023-04-18 </span><span class="hljs-string">LTS</span>
<span class="hljs-string">OpenJDK</span> <span class="hljs-string">Runtime</span> <span class="hljs-string">Environment</span> <span class="hljs-string">Corretto-17.0.7.7.1</span> <span class="hljs-string">(build</span> <span class="hljs-number">17.0</span><span class="hljs-number">.7</span><span class="hljs-string">+7-LTS)</span>
<span class="hljs-string">OpenJDK</span> <span class="hljs-number">64</span><span class="hljs-string">-Bit</span> <span class="hljs-string">Server</span> <span class="hljs-string">VM</span> <span class="hljs-string">Corretto-17.0.7.7.1</span> <span class="hljs-string">(build</span> <span class="hljs-number">17.0</span><span class="hljs-number">.7</span><span class="hljs-string">+7-LTS,</span> <span class="hljs-string">mixed</span> <span class="hljs-string">mode,</span> <span class="hljs-string">sharing)</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699119952099/e50b2a96-5437-4452-9bdd-24144e23b049.png" alt class="image--center mx-auto" /></p>
<p>I had a hunch that for some reason, XCode wasn't aware of my path variables.</p>
<h2 id="heading-solution">Solution</h2>
<p>I've run into situations before with GIT GUI clients where opening them via the finder/dock doesn't allow for the application to be aware of your environment/path variables. To work around that, from previous issues, I've learned that opening your program via the command line with make the application aware of your environment/path variables.</p>
<p>So in the root of my project I executed <code>open iosApp/iosApp.xcodeproj</code> which should open XCode with your iOS project. After selecting Product -&gt; Archive again, I was finally able to generate the archive for my project.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699120655721/82324ee1-94c9-481b-9d94-7cf51af725fd.png" alt class="image--center mx-auto" /></p>
<p>I hope this helps!</p>
]]></content:encoded></item><item><title><![CDATA[Managing Build File Names in Large Multi-Module Android Projects]]></title><description><![CDATA[If you're working on a large Android project with many modules, you've likely noticed that the build files can become difficult to manage and find, especially as the project continues to grow. This is namely due to the fact that as you add more modul...]]></description><link>https://blog.joetr.com/managing-build-file-names-in-large-multi-module-android-projects</link><guid isPermaLink="true">https://blog.joetr.com/managing-build-file-names-in-large-multi-module-android-projects</guid><category><![CDATA[Android]]></category><category><![CDATA[gradle]]></category><category><![CDATA[modularization]]></category><dc:creator><![CDATA[Joe Roskopf]]></dc:creator><pubDate>Sat, 04 Nov 2023 17:28:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1699121039018/5fc55b1d-bde2-415b-9f5f-6966c74e8580.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you're working on a large Android project with many modules, you've likely noticed that the build files can become difficult to manage and find, especially as the project continues to grow. This is namely due to the fact that as you add more modules, you will be adding more and more <code>build.gradle.kts</code> files (or <code>build.gradle</code> if you haven't migrated yet). However, there's a simple solution to keep these build files organized: name them according to their respective module names. This article will walk you through how you can achieve this by writing some custom code in your <code>settings.gradle.kts</code></p>
<h2 id="heading-the-traditional-challenge"><strong>The Traditional Challenge</strong></h2>
<p>Android's default build system, Gradle, creates a build file for each module in the project, typically named <code>build.gradle</code> or <code>build.gradle.kts</code> for Kotlin. This convention is straightforward for smaller projects, but in larger, more complex projects with nested modules, locating the correct build file can become cumbersome.</p>
<h2 id="heading-the-solution"><strong>The Solution</strong></h2>
<p>To simplify this process, we introduce the <code>includeProject</code> function. This function allows us to include a project into the build by its name and specifies an optional file path.</p>
<p>Here's how it works:</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">/**
 * Include module within the current project. Allows for specifying an optional file path
 */</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">includeProject</span><span class="hljs-params">(name: <span class="hljs-type">String</span>, filePath: <span class="hljs-type">String</span>? = <span class="hljs-literal">null</span>)</span></span> {
    settings.include(name)
    <span class="hljs-keyword">val</span> project = project(name)
    project.configureProjectDir(filePath)
    project.configureBuildFileName(name)
}
</code></pre>
<p>In the <code>includeProject</code> function, we have two parameters: <code>name</code> and an optional <code>filepath</code>. The <code>name</code> argument is used to include the module and then to obtain the project. The project's directory and build file are then configured. The optional parameter for specifying a custom file path can be included when you have a module name that does not follow the file system.</p>
<p>The <code>configureProjectDir</code> function sets the project directory and checks if the directory exists and is indeed a directory (and not a file). If not, a GradleException will be thrown and the build will fail to sync.</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">/**
 * Configures the project directory
 */</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> ProjectDescriptor.<span class="hljs-title">configureProjectDir</span><span class="hljs-params">(filePath: <span class="hljs-type">String</span>? = <span class="hljs-literal">null</span>)</span></span> {
    <span class="hljs-keyword">if</span> (filePath != <span class="hljs-literal">null</span>) {
        projectDir = File(rootDir, filePath)
    }
    <span class="hljs-keyword">if</span> (!projectDir.exists()) {
        <span class="hljs-keyword">throw</span> GradleException(<span class="hljs-string">"Path: <span class="hljs-variable">$projectDir</span> does not exist. Cannot include project: <span class="hljs-variable">$name</span>"</span>)
    }
    <span class="hljs-keyword">if</span> (!projectDir.isDirectory) {
        <span class="hljs-keyword">throw</span> GradleException(<span class="hljs-string">"Path: <span class="hljs-variable">$projectDir</span> is a file instead of a directory. Cannot include project: <span class="hljs-variable">$name</span>"</span>)
    }
}
</code></pre>
<p>Meanwhile, the <code>configureBuildFileName</code> function is the heart of what we are wanting to accomplsh. It sets the build file name based on the module name and checks if the file exists. If not, again, a GradleException is thrown.</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">/**
 * Searches for all of the relevant module names we want allowed. 
 * For example, if we have a file structure:
 * 
 * ├── grandparent
 *    ├── parent
 *       ├── module
 *
 * where `grandparent` and `parent` are just folders and not logical modules, and `module` is a Gradle module,
 * we'd want to allow the following entries for `module`
 * 
 * 1. includeProject(":module")
 * 2. includeProject(":parent-module")
 * 3. includeProject(":grandparent-module")
 * 
 * In slightly more realistic example, let's say you have the following file structure:
 * ├── repository
 *    ├── fake
 *    ├── api
 *    ├── impl
 * 
 * We would want to allow the following names for including the 3 logical modules here, fake, api, and impl:
 * 
 * 1. includeProject(":repository-fake")
 * 2. includeProject(":repository-api")
 * 3. includeProject(":repository-impl")
 */</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> ProjectDescriptor.<span class="hljs-title">configureBuildFileName</span><span class="hljs-params">(projectName: <span class="hljs-type">String</span>)</span></span> {
    <span class="hljs-keyword">val</span> name = projectName.substringAfterLast(<span class="hljs-string">":"</span>)
    <span class="hljs-keyword">val</span> thirdToLastIndex = lastOrdinalIndexOf(projectName, <span class="hljs-string">':'</span>, <span class="hljs-number">3</span>)
    <span class="hljs-keyword">val</span> secondToLastIndex = lastOrdinalIndexOf(projectName, <span class="hljs-string">':'</span>, <span class="hljs-number">2</span>)
    <span class="hljs-keyword">val</span> lastIndex = lastOrdinalIndexOf(projectName, <span class="hljs-string">':'</span>, <span class="hljs-number">1</span>)
    <span class="hljs-keyword">val</span> directParentModule = projectName.substring(secondToLastIndex, lastIndex).trim(<span class="hljs-string">':'</span>)
    <span class="hljs-keyword">val</span> thirdLevelParentModule = projectName.substring(thirdToLastIndex, lastIndex).trim(<span class="hljs-string">':'</span>).replace(<span class="hljs-string">":"</span>,<span class="hljs-string">"-"</span>)

    <span class="hljs-keyword">val</span> filesToCheck = listOf(
        <span class="hljs-string">"<span class="hljs-variable">$name</span>.gradle.kts"</span>,
        <span class="hljs-string">"<span class="hljs-variable">$directParentModule</span>-<span class="hljs-variable">$name</span>.gradle.kts"</span>,
        <span class="hljs-string">"<span class="hljs-variable">$thirdLevelParentModule</span>-<span class="hljs-variable">$name</span>.gradle.kts"</span>,
        <span class="hljs-comment">// any other files you wish to be named</span>
        <span class="hljs-comment">// potentially still allow for build.gradle.kts</span>
    )

    filesToCheck.firstOrNull {
        buildFileName = it
        buildFile.exists()
    } ?: <span class="hljs-keyword">throw</span> GradleException(<span class="hljs-string">"None of the following build files exist: <span class="hljs-subst">${filesToCheck.joinToString(<span class="hljs-string">", "</span>)}</span>. Cannot include project: <span class="hljs-variable">$name</span>"</span>)
}

<span class="hljs-comment">/**
 * Finds the n-th last index within a String
 */</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">lastOrdinalIndexOf</span><span class="hljs-params">(string: <span class="hljs-type">String</span>, searchChar: <span class="hljs-type">Char</span>, ordinal: <span class="hljs-type">Int</span>)</span></span>: <span class="hljs-built_in">Int</span> {
    <span class="hljs-keyword">val</span> numberOfOccurrences = string.count { it == searchChar }

    <span class="hljs-keyword">if</span>(numberOfOccurrences &lt;= ordinal) {
        <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>
    }
    <span class="hljs-keyword">var</span> found = <span class="hljs-number">0</span>
    <span class="hljs-keyword">var</span> index = string.length
    <span class="hljs-keyword">do</span> {
        index = string.lastIndexOf(searchChar, index - <span class="hljs-number">1</span>)
        <span class="hljs-keyword">if</span> (index &lt; <span class="hljs-number">0</span>) {
            <span class="hljs-keyword">return</span> index
        }
        found++
    } <span class="hljs-keyword">while</span> (found &lt; ordinal)
    <span class="hljs-keyword">return</span> index
}
</code></pre>
<p>This function also handles different naming formats based on the module's hierarchy. So, for instance, if we have a file structure like this:</p>
<pre><code class="lang-plaintext">├── repository
   ├── fake
   ├── api
   ├── impl
</code></pre>
<p>Where <code>fake</code>, <code>api</code>, and <code>impl</code> are all logical modules, we can now include them in our Android project in the following way:</p>
<pre><code class="lang-kotlin">includeProject(<span class="hljs-string">":repository-fake"</span>)
includeProject(<span class="hljs-string">":repository-api"</span>)
includeProject(<span class="hljs-string">":repository-impl"</span>)
</code></pre>
<p>This flexibility allows you to maintain a naming pattern that matches your project's module hierarchy.</p>
<h2 id="heading-wrapping-up"><strong>Wrapping Up</strong></h2>
<p>The <code>includeProject</code> function can be a huge timesaver in managing large, multi-module projects. Not only does it ensure that your project adheres to a logical, hierarchical naming convention, but it also reduces the risk of errors from manually entering project names and paths.</p>
<p>By enforcing a more systematic naming convention for your build files, you can navigate and manage your codebase more efficiently, especially when your project continues to scale. As a result, you'll spend less time searching for the correct build files and more time on what matters most - writing quality code for your Android application.</p>
]]></content:encoded></item><item><title><![CDATA[Simplifying Android Module Creation: Enter the Module Maker Plugin]]></title><description><![CDATA[We've all been there. It's another day in the Android Studio jungle, and you're tasked with creating a new module for your project. As you fire up the built-in New Module wizard in Android Studio, you're hit with a sense of dread. Isn't there a bette...]]></description><link>https://blog.joetr.com/simplifying-android-module-creation-enter-the-module-maker-plugin</link><guid isPermaLink="true">https://blog.joetr.com/simplifying-android-module-creation-enter-the-module-maker-plugin</guid><category><![CDATA[Android]]></category><category><![CDATA[gradle]]></category><category><![CDATA[modules]]></category><dc:creator><![CDATA[Joe Roskopf]]></dc:creator><pubDate>Wed, 12 Jul 2023 01:58:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1689127781606/894bba51-bc45-43fc-8f3c-576adbc50756.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>We've all been there. It's another day in the Android Studio jungle, and you're tasked with creating a new module for your project. As you fire up the built-in New Module wizard in Android Studio, you're hit with a sense of dread. Isn't there a better way?</p>
<p>While the default New Module wizard does its job, it often falls short when aligning with specific project standards. It doesn't allow for much customization, leading to tedious and time-consuming manual adjustments. But, fret not! There's a new player in town, ready to streamline your module creation - introducing the <a target="_blank" href="https://plugins.jetbrains.com/plugin/21724-module-maker">Module Maker</a> plugin.</p>
<h2 id="heading-meet-the-module-maker-plugin"><strong>Meet the Module Maker Plugin</strong></h2>
<p>The Module Maker plugin for Android Studio is an IntelliJ / Android Studio plugin for those looking to tailor their new modules to their project-specific standards. The plugin extends beyond the limitations of Android Studio's built-in tools, allowing for a more streamlined and customizable module creation experience.</p>
<p>Let's take a look at some of the unique features that make this plugin a must-have in your development toolkit.</p>
<ol>
<li><p><strong>3 Module Creation Structure:</strong> Following the structure highlighted in this <a target="_blank" href="https://speakerdeck.com/vrallev/android-at-scale-at-square"><strong>talk</strong></a> <strong>from Ralf Wondratschek at Square</strong>, the plugin allows you to create three modules at a time with customizable names. By default, the three module structure is composed of an API, Glue, and Impl module, helping you maintain cleaner and more manageable codebases.</p>
</li>
<li><p><strong>Kotlin and Groovy Build Files:</strong> You can choose to have build files for each module generated as either Kotlin or Groovy. This flexibility ensures you can stick with your preferred language and keep your project's standards consistent.</p>
</li>
<li><p><strong>Customizable Build File Names:</strong> Either stick with the default name ( <code>build.gradle</code> or <code>build.gradle.kts</code> ) or have your build file name follow the module. Tailoring the build file names to the module can help streamline the identification process and improve project organization.</p>
</li>
<li><p><strong>Optional README and .gitignore Files:</strong> Upon creating your modules, the plugin can generate an optional README and .gitignore file, helping you jumpstart your module documentation and version control settings.</p>
</li>
<li><p><strong>Android or JVM Only Module:</strong> Whether your module is meant for Android or just JVM, the plugin has got you covered.</p>
</li>
<li><p><strong>Package Structure Specification:</strong> Specify your package structure and have the folders created directly on the filesystem, eliminating the need for manual creation and organization.</p>
</li>
<li><p><strong>Automatic Settings Update and Sync:</strong> Once your new modules are ready, the plugin automatically adds them to your settings.gradle(.kts) file and syncs your project, sparing you the hassle of manual updates and syncs.</p>
</li>
</ol>
<h2 id="heading-customize-modules-to-your-hearts-content"><strong>Customize Modules to Your Heart's Content</strong></h2>
<p>The Module Maker plugin understands that every project is unique, and therefore, it offers a robust settings page where you can specify a range of options to suit your project's specific needs. You can:</p>
<ol>
<li><p>Specify different Android and JVM build file templates.</p>
</li>
<li><p>Define different API, Glue, and Impl build file templates.</p>
</li>
<li><p>Customize API, Glue, and Impl module names.</p>
</li>
<li><p>Specify a custom .gitignore template.</p>
</li>
<li><p>Specify a default package name to persist across usages of the plugin</p>
</li>
</ol>
<h2 id="heading-the-wrap-up"><strong>The Wrap Up</strong></h2>
<p>In the dynamic landscape of Android development, maintaining a consistent and efficient project structure can often be challenging. The built-in New Module wizard, while helpful, may not always align with your project's specific standards. This is where the Module Maker plugin comes in - designed as a practical solution to enhance your module creation process.</p>
<p>With a wide range of features, from allowing a 3-module creation structure to offering customization options for your build files, the Module Maker plugin equips you with the tools to tailor your project to your preferences. Its ability to generate optional README and .gitignore files, automatic project sync, and customizable settings, all work towards a more streamlined and personalized Android project setup.</p>
<p>As the author of the Module Maker plugin, I aimed to provide a tool that could simplify and adapt to the unique needs of each Android project. I hope you find this plugin helpful and that it can assist in enhancing your workflow, saving time, and maintaining project consistency. After all, development should be more about solving complex problems and less about wrestling with project setup and organization.</p>
<h2 id="heading-links">Links</h2>
<p>Github - <a target="_blank" href="https://github.com/j-roskopf/ModuleMakerPlugin">https://github.com/j-roskopf/ModuleMakerPlugin</a></p>
<p>Plugin - <a target="_blank" href="https://plugins.jetbrains.com/plugin/21724-module-maker">https://plugins.jetbrains.com/plugin/21724-module-maker</a></p>
]]></content:encoded></item><item><title><![CDATA[Migrate Your Android App To Gradle Version Catalogs]]></title><description><![CDATA[As an Android developer, you know that keeping track of dependencies and their versions can be a challenging task. With each new version, libraries can introduce breaking changes, and it can be difficult to ensure that your project stays up-to-date w...]]></description><link>https://blog.joetr.com/migrate-your-android-app-to-gradle-version-catalogs</link><guid isPermaLink="true">https://blog.joetr.com/migrate-your-android-app-to-gradle-version-catalogs</guid><category><![CDATA[Android]]></category><category><![CDATA[gradle]]></category><category><![CDATA[Version Catalog]]></category><dc:creator><![CDATA[Joe Roskopf]]></dc:creator><pubDate>Thu, 13 Apr 2023 13:31:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1681392691244/14f76590-1c79-48dd-a041-f399cdb5714b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As an Android developer, you know that keeping track of dependencies and their versions can be a challenging task. With each new version, libraries can introduce breaking changes, and it can be difficult to ensure that your project stays up-to-date with the latest updates across all of your application's modules.</p>
<p>One solution to this problem is to use a <a target="_blank" href="https://docs.gradle.org/current/userguide/platforms.html">version catalog</a>. A version catalog is a file that defines the versions of your project's dependencies, making it easier to manage and update them. In this blog post, we'll take a look at how to migrate an Android project to using a version catalog.</p>
<h2 id="heading-step-1-create-a-version-catalog">Step 1: Create a Version Catalog</h2>
<p>The first step in migrating your Android project to use a version catalog is to create the catalog file. The version catalog is a simple TOML (more info about TOML files can be found <a target="_blank" href="https://toml.io/en/">here</a>) file that lists the dependencies and their respective versions. You can create the file manually by right-clicking on your <code>gradle</code> folder and selecting New -&gt; File and adding <code>libs.versions.toml</code>. It is possible to change the catalog file name, however, this requires changing your build files so it is not recommended at the current time.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681387764566/d0524106-67e2-44d5-bde3-a4e4ea6d82cf.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681387798096/aaf1de09-ac57-408e-b70c-1a3b90a41db9.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-step-2-adding-dependencies-to-the-toml-file">Step 2: Adding dependencies to the .toml file</h2>
<p>In your <code>libs.versions.toml</code> file, add these sections:</p>
<pre><code class="lang-yaml">[<span class="hljs-string">versions</span>]

[<span class="hljs-string">libraries</span>]

[<span class="hljs-string">plugins</span>]
</code></pre>
<p>We will be migrating the following dependency to being defined in the version catalog.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681387492406/719ddfd2-fec3-446c-a140-ea069eeec4f1.png" alt class="image--center mx-auto" /></p>
<p>This can be defined in the <code>.toml</code> file by adding a version and a library like so:</p>
<pre><code class="lang-yaml">[<span class="hljs-string">versions</span>]
<span class="hljs-string">core-ktx</span> <span class="hljs-string">=</span> <span class="hljs-string">"1.10.0"</span>

[<span class="hljs-string">libraries</span>]
<span class="hljs-string">androidx-core-ktx</span> <span class="hljs-string">=</span> { <span class="hljs-string">group</span> <span class="hljs-string">=</span> <span class="hljs-string">"androidx.core"</span>, <span class="hljs-string">name</span> <span class="hljs-string">=</span> <span class="hljs-string">"core-ktx"</span>, <span class="hljs-string">version.ref</span> <span class="hljs-string">=</span> <span class="hljs-string">"core-ktx"</span> }

[<span class="hljs-string">plugins</span>]
</code></pre>
<h2 id="heading-step-3-use-the-version-catalog-in-your-dependencies">Step 3: Use the Version Catalog in Your Dependencies</h2>
<p>Finally, you need to update your project's dependencies to use the version catalog. You can do this by replacing the first line with a reference to the version catalog. For example:</p>
<pre><code class="lang-kotlin">implementation <span class="hljs-string">'androidx.core:core-ktx:1.10.0'</span>
</code></pre>
<p>can be replaced with:</p>
<pre><code class="lang-kotlin">implementation(libs.androidx.core.ktx)
</code></pre>
<h2 id="heading-step-5-plugins">Step 5: Plugins</h2>
<p>In the root <code>build.gradle</code> file of your project, numerous plugins are defined. Using a version catalog also allows you to define these plugin versions as well.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681388062911/a5e460b2-aa18-493d-a96a-e39d1228f350.png" alt class="image--center mx-auto" /></p>
<p>First, you will add the plugin versions and definitions to the <code>.toml</code> file.</p>
<pre><code class="lang-yaml">[<span class="hljs-string">versions</span>]
<span class="hljs-string">core-ktx</span> <span class="hljs-string">=</span> <span class="hljs-string">"1.10.0"</span>

<span class="hljs-comment"># Plugin versions</span>
<span class="hljs-string">androidGradlePlugin</span> <span class="hljs-string">=</span> <span class="hljs-string">"7.4.2"</span>
<span class="hljs-string">kotlin</span> <span class="hljs-string">=</span> <span class="hljs-string">"1.8.0"</span>

[<span class="hljs-string">libraries</span>]
<span class="hljs-string">androidx-core-ktx</span> <span class="hljs-string">=</span> { <span class="hljs-string">group</span> <span class="hljs-string">=</span> <span class="hljs-string">"androidx.core"</span>, <span class="hljs-string">name</span> <span class="hljs-string">=</span> <span class="hljs-string">"core-ktx"</span>, <span class="hljs-string">version.ref</span> <span class="hljs-string">=</span> <span class="hljs-string">"core-ktx"</span> }

[<span class="hljs-string">plugins</span>]
<span class="hljs-string">android-application</span> <span class="hljs-string">=</span> { <span class="hljs-string">id</span> <span class="hljs-string">=</span> <span class="hljs-string">"com.android.application"</span>, <span class="hljs-string">version.ref</span> <span class="hljs-string">=</span> <span class="hljs-string">"androidGradlePlugin"</span> }
<span class="hljs-string">android-library</span> <span class="hljs-string">=</span> { <span class="hljs-string">id</span> <span class="hljs-string">=</span> <span class="hljs-string">"com.android.library"</span>, <span class="hljs-string">version.ref</span> <span class="hljs-string">=</span> <span class="hljs-string">"androidGradlePlugin"</span> }
<span class="hljs-string">kotlin-android</span> <span class="hljs-string">=</span> { <span class="hljs-string">id</span> <span class="hljs-string">=</span> <span class="hljs-string">"org.jetbrains.kotlin.android"</span>, <span class="hljs-string">version.ref</span> <span class="hljs-string">=</span> <span class="hljs-string">"kotlin"</span> }
</code></pre>
<p>Then, you can change your root <code>build.gradle</code> file to use the newly defined plugins like so:</p>
<pre><code class="lang-yaml"><span class="hljs-string">plugins</span> {
    <span class="hljs-string">alias</span> <span class="hljs-string">libs.plugins.android.application</span> <span class="hljs-string">apply</span> <span class="hljs-literal">false</span>
    <span class="hljs-string">alias</span> <span class="hljs-string">libs.plugins.android.library</span> <span class="hljs-string">apply</span> <span class="hljs-literal">false</span>
    <span class="hljs-string">alias</span> <span class="hljs-string">libs.plugins.kotlin.android</span> <span class="hljs-string">apply</span> <span class="hljs-literal">false</span>
}
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>By using a version catalog, you can easily manage your project's dependencies across modules and ensure that you are using the latest versions. You can update the versions in the catalog file, and have a single source of truth for versions of dependencies in your application.</p>
<p>In conclusion, migrating an Android project to use a version catalog is a simple process that can save you a lot of time and effort in managing your dependencies. By following the steps outlined in this blog post, you can easily migrate your project to use a version catalog and enjoy the benefits of easier dependency management.</p>
]]></content:encoded></item><item><title><![CDATA[How To Format Only Staged Files With Spotless]]></title><description><![CDATA[As a developer, you know that keeping your code clean and consistent is crucial for the success of any project. One tool that can help you achieve this is Spotless, a powerful code formatting plugin for Gradle and Maven. However, you may find yoursel...]]></description><link>https://blog.joetr.com/how-to-format-only-staged-files-with-spotless</link><guid isPermaLink="true">https://blog.joetr.com/how-to-format-only-staged-files-with-spotless</guid><category><![CDATA[Git]]></category><category><![CDATA[formatting]]></category><dc:creator><![CDATA[Joe Roskopf]]></dc:creator><pubDate>Wed, 12 Apr 2023 15:23:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1681269181846/e7f2bc04-594c-4c16-b27f-e1c7a72705fe.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As a developer, you know that keeping your code clean and consistent is crucial for the success of any project. One tool that can help you achieve this is Spotless, a powerful code formatting plugin for Gradle and Maven. However, you may find yourself in situations where you only want to apply Spotless to the files that have been staged in your Git repository. In this post, we'll explore how to do just that.</p>
<p>First, let's define what we mean by "staged files." In Git, a staged file has been modified and added to the Git index, which means it is ready to be committed. This is different from an unstaged file, which has been modified but not yet added to the index.</p>
<p>With definitions out of the way, we then want to create a bash script that will invoke Spotless (this assumes you already have Spotless set up in your project). In this setup, the script will be invoked as a pre-commit Git hook. The script also presumes the presence of a <code>.zshrc</code> file. Some slight modifications would be needed to make this compatible with <code>bash</code>.</p>
<p>This script will do the following:</p>
<ol>
<li><p>Stash any unstaged files</p>
</li>
<li><p>Apply Spotless to the remaining files</p>
</li>
<li><p>Add any modified files from Spotless to the git index</p>
</li>
<li><p>Cleanup the stash from step 1</p>
</li>
</ol>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/zsh</span>

<span class="hljs-comment"># Workaround for Git GUI users to have environment variables available (https://community.atlassian.com/t5/Bitbucket-questions/SourceTree-Hook-failing-because-paths-don-t-seem-to-be-set/qaq-p/274792)</span>
<span class="hljs-built_in">source</span> ~/.zshrc

<span class="hljs-comment"># Immediately exit if any command has a non-zero exit status</span>
<span class="hljs-built_in">set</span> -e

<span class="hljs-built_in">echo</span> <span class="hljs-string">"Running formatter..."</span>

<span class="hljs-comment"># Creaate a patch file</span>
GIT_STASH_FILE=<span class="hljs-string">"stash.patch"</span>

<span class="hljs-comment"># Stash unstaged changes</span>
git diff &gt; <span class="hljs-string">"<span class="hljs-variable">$GIT_STASH_FILE</span>"</span>

<span class="hljs-comment"># add the patch so it is not stashed</span>
git add <span class="hljs-string">"<span class="hljs-variable">$GIT_STASH_FILE</span>"</span>

<span class="hljs-comment"># stash untracked files</span>
git stash -k

<span class="hljs-comment"># apply spotless</span>
./gradlew spotlessApply --daemon

<span class="hljs-comment"># re-add any changes that spotless created</span>
git add -u

<span class="hljs-comment"># store the last exit code</span>
RESULT=$?

<span class="hljs-keyword">if</span> <span class="hljs-built_in">test</span> -f <span class="hljs-string">"<span class="hljs-variable">$GIT_STASH_FILE</span>"</span>;
<span class="hljs-keyword">then</span>
  <span class="hljs-built_in">echo</span> <span class="hljs-string">"<span class="hljs-variable">$GIT_STASH_FILE</span> has been found"</span>

    <span class="hljs-comment"># apply the patch</span>
    git apply stash.patch --allow-empty

    <span class="hljs-comment"># delete the patch and re-add that to the index</span>
    rm -f stash.patch
    git add stash.patch
<span class="hljs-keyword">else</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"<span class="hljs-variable">$GIT_STASH_FILE</span> has not been found"</span>
<span class="hljs-keyword">fi</span>

<span class="hljs-comment"># delete the WIP stash</span>
git stash drop

<span class="hljs-comment"># return the exit code</span>
<span class="hljs-built_in">exit</span> <span class="hljs-variable">$RESULT</span>
</code></pre>
<p>That's it! Now, whenever you commit changes to your Git repository, Spotless will be applied to only the staged files. This ensures that your code is always clean and consistent, which can save you time and headaches down the line.</p>
<p>In conclusion, applying Spotless to only the staged files in your Git repository via a bash script in a pre-commit hook is a straightforward process that can improve the quality and consistency of the code that you want to commit, while leaving unstaged code alone. By following the steps outlined in this blog post, you can easily integrate Spotless into your development workflow and enjoy the benefits of clean code.</p>
]]></content:encoded></item><item><title><![CDATA[Profiling Android Builds With Gradle Profiler]]></title><description><![CDATA[If you're working on an Android project, you know that building your app can take a long time, especially as your codebase grows. Gradle is the build system that Android Studio uses, and it's responsible for compiling your code, generating your APK, ...]]></description><link>https://blog.joetr.com/profiling-android-builds-with-gradle-profiler</link><guid isPermaLink="true">https://blog.joetr.com/profiling-android-builds-with-gradle-profiler</guid><category><![CDATA[Android]]></category><category><![CDATA[android app development]]></category><category><![CDATA[gradle]]></category><category><![CDATA[gradle profiler]]></category><dc:creator><![CDATA[Joe Roskopf]]></dc:creator><pubDate>Wed, 12 Apr 2023 01:35:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1681263323782/ed153ae3-d87a-4a22-8a45-65462de4850d.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you're working on an Android project, you know that building your app can take a long time, especially as your codebase grows. Gradle is the build system that Android Studio uses, and it's responsible for compiling your code, generating your APK, and running tests. But when you're dealing with large projects, Gradle builds can become slow and frustrating.</p>
<p>That's where Gradle Profiler comes in. Gradle Profiler is a tool that can help you measure and analyze your Gradle builds, so you can identify bottlenecks and speed up your build times. In this post, we'll walk through how to install and use Gradle Profiler, and we'll show you how to use it to test different scenarios to optimize your build times.</p>
<h2 id="heading-step-1-installing-gradle-profiler">Step 1: Installing Gradle Profiler</h2>
<p>The first step in using Gradle Profiler is to install it via <a target="_blank" href="https://sdkman.io/">SDKMAN</a>. SDKMAN is a tool for managing software development kits, making it easy to install and manage Gradle Profiler.</p>
<p>To install SDKMAN on a Unix-based system, open up your terminal and run the following command:</p>
<pre><code class="lang-bash">curl -s <span class="hljs-string">"https://get.sdkman.io"</span> | bash
</code></pre>
<p>Once you've installed SDKMAN, you can use it to install Gradle Profiler by running the following command (you may need to restart your terminal first):</p>
<pre><code class="lang-bash">sdk install gradleprofiler
</code></pre>
<h2 id="heading-step-2-running-gradle-profiler">Step 2: Running Gradle Profiler</h2>
<p>Once you've installed Gradle Profiler, you can start using it to analyze your builds. To do this, you'll first create a <code>.scenarios</code> file. This is where you will define the different scenarios you want to profile. A sample <code>profiling.scenarios</code> file might look like this:</p>
<pre><code class="lang-bash">AssembleDebug8G {
    title = <span class="hljs-string">"assembleDebug with 8 gigs"</span>
    tasks = [<span class="hljs-string">"assembleDebug"</span>]
    warm-ups: 2
    iterations: 5
    cleanup-tasks = [<span class="hljs-string">"clean"</span>]
    jvm-args = [<span class="hljs-string">"-Xmx8g"</span>, <span class="hljs-string">"-XX:MaxPermSize=1024m"</span>, <span class="hljs-string">"-XX:+HeapDumpOnOutOfMemoryError"</span>, <span class="hljs-string">"-XX:+UseParallelGC"</span>]
}

AssembleDebug6G {
    title = <span class="hljs-string">"assembleDebug with 6 gigs"</span>
    tasks = [<span class="hljs-string">"assembleDebug"</span>]
    warm-ups: 2
    iterations: 5
    cleanup-tasks = [<span class="hljs-string">"clean"</span>]
    jvm-args = [<span class="hljs-string">"-Xmx6g"</span>, <span class="hljs-string">"-XX:MaxPermSize=1024m"</span>, <span class="hljs-string">"-XX:+HeapDumpOnOutOfMemoryError"</span>, <span class="hljs-string">"-XX:+UseParallelGC"</span>]
}

AssembleDebugParallel {
    title = <span class="hljs-string">"assembleDebug with 8 gigs and gradle parallel execution"</span>
    tasks = [<span class="hljs-string">"assembleDebug"</span>]
    warm-ups: 2
    iterations: 5
    gradle-args = [<span class="hljs-string">"--parallel"</span>]
    cleanup-tasks = [<span class="hljs-string">"clean"</span>]
    jvm-args = [<span class="hljs-string">"-Xmx8g"</span>, <span class="hljs-string">"-XX:MaxPermSize=1024m"</span>, <span class="hljs-string">"-XX:+HeapDumpOnOutOfMemoryError"</span>, <span class="hljs-string">"-XX:+UseParallelGC"</span>]
}
</code></pre>
<p>3 different scenarios are defined in this file, all centered around the <code>assembleDebug</code> Gradle task. One with 8 gigs to the JVM args, one with 6 gigs, and one enabling Gradle <a target="_blank" href="https://docs.gradle.org/current/userguide/performance.html#parallel_execution">parallel execution</a>. We have also defined some additional information like the number of warm-ups and iterations to go through, as well as some clean-up tasks. Full documentation can be found on the Gradle Profiler <a target="_blank" href="https://github.com/gradle/gradle-profiler">Github</a>.</p>
<p>To now execute one of these scenarios, we can use the following command in the terminal (assuming the file created above is in the root directory of your project and named <code>profiling.scenarios</code>). With the following command, we are running the <code>AssembleDebug6G</code> scenario that we defined earlier.</p>
<pre><code class="lang-bash">gradle-profiler --benchmark --project-dir . --scenario-file profiling.scenarios AssembleDebug6G
</code></pre>
<p>Note - You may want to add the following to your <code>.gitignore</code></p>
<pre><code class="lang-plaintext">/profile-out/
/profile-out-*/
/gradle-user-home/
</code></pre>
<h2 id="heading-step-3-analyzing-the-gradle-profiler-report">Step 3: Analyzing the Gradle Profiler Report</h2>
<p>Once your build has been completed, Gradle will generate a report file that contains all of the information that was collected during the build. With the command above, this report file is saved in the root directory of your project.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681251913127/7f9ff0ed-81dd-4e1a-9a62-c109953aa36a.png" alt class="image--center mx-auto" /></p>
<p>To analyze the report, we can open up the <code>.html</code> file found in <code>profile-out</code> in this example. In this report, we can see fields like the mean, median, 25th, and 75th percentiles as well as the runtimes of each iteration.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681251987206/146164ed-3349-46c7-bb9b-b7a5356f9cc8.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-step-4-testing-different-scenarios">Step 4: Testing Different Scenarios</h2>
<p>Now that you've got Gradle Profiler set up, you can start testing different scenarios to see how they affect your build times. One common scenario that you might want to test is increasing the JVM args to see if that helps speed up your build.</p>
<p>To test this scenario, we can instead run the <code>AssembleDebug8G</code> scenario that we defined earlier. This will increase the maximum heap size that Gradle is allowed to use during the build, which can help speed up the build by reducing the amount of time spent on garbage collecting.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681252259597/5831e551-623f-4eae-8cd3-e7041b4ac905.png" alt class="image--center mx-auto" /></p>
<p>We can see that when we compare the results between using 6 gigs and 8 gigs, there isn't a ton of difference in mean execution time across the five iterations. That's okay though! Now we know! Increasing past 6 gigs for the Now In Android project doesn't give us much benefit.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this post, we've shown you how to use Gradle Profiler to analyze your Gradle builds and identify bottlenecks that are slowing down your build times. By testing different scenarios, such as increasing the JVM args or switching to a different version of Gradle, you can optimize your builds and reduce your overall development time.</p>
<p>By taking the time to profile your builds with Gradle Profiler, you can save yourself a lot of frustration and make your development process smoother and more efficient. So give it a try, and see how much time you can save!</p>
]]></content:encoded></item><item><title><![CDATA[Cacheable Custom Gradle Tasks]]></title><description><![CDATA[As software developers, we are always looking for ways to optimize our build processes and make them as efficient as possible. As an Android engineer specifically, this often means interacting with Gradle and the tasks within its task graph to optimi...]]></description><link>https://blog.joetr.com/cacheable-custom-gradle-tasks</link><guid isPermaLink="true">https://blog.joetr.com/cacheable-custom-gradle-tasks</guid><category><![CDATA[Android]]></category><category><![CDATA[gradle]]></category><dc:creator><![CDATA[Joe Roskopf]]></dc:creator><pubDate>Mon, 10 Apr 2023 21:28:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1681161978633/7aa8d71a-ddfb-404d-9ec4-02e3eb295ae8.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As software developers, we are always looking for ways to optimize our build processes and make them as efficient as possible. As an Android engineer specifically, this often means interacting with Gradle and the tasks within its task graph to optimize your build. This problem becomes especially challenging when you define custom Gradle tasks for your project and want to ensure they play nicely inside Gradle's caching mechanism so that extra work isn't done every build.</p>
<p>Have you ever noticed the output of a build, how some tasks have <code>UP-TO-DATE</code> next to the task and some don't? The Gradle build system provides a powerful mechanism for defining and executing custom tasks. However, when we define a custom task, it is often necessary to ensure that it only runs when something has changed. If a task is executed every time we build our project, it can significantly slow down our build process and waste valuable time.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681160327099/870ac17f-fd70-415a-ab51-af2ed5759a43.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-usecase-for-a-custom-gradle-task">Usecase for a Custom Gradle Task</h1>
<p>On a recent project I was working on, there was a bash script that was executed before every commit. Git's pre-commit hook was utilized for this task and it worked well. However, for this to work on a developer's machine, they had to manually symlink the bash script to the pre-commit hook.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">import</span> org.gradle.api.Plugin
<span class="hljs-keyword">import</span> org.gradle.api.Project

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PreCommitPlugin</span> : <span class="hljs-type">Plugin</span>&lt;<span class="hljs-type">Project</span>&gt; </span>{
    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">apply</span><span class="hljs-params">(target: <span class="hljs-type">Project</span>)</span></span> {
        with(target) {
            tasks.register(<span class="hljs-string">"installPreCommitHook"</span>) {

                doLast {
                    exec {
                        <span class="hljs-comment">// make the bash script executable</span>
                        commandLine(<span class="hljs-string">"chmod"</span>, <span class="hljs-string">"+x"</span>, <span class="hljs-string">"../pre-commit.sh"</span>)
                    }
                    exec {
                        <span class="hljs-comment">// sym link it to the the pre-commit hook</span>
                        commandLine(<span class="hljs-string">"ln"</span>, <span class="hljs-string">"-s"</span>, <span class="hljs-string">"-f"</span>, <span class="hljs-string">"../../pre-commit.sh"</span>, <span class="hljs-string">"../.git/hooks/pre-commit"</span>)
                    }
                    exec {
                        <span class="hljs-comment">// make the symlink executable</span>
                        commandLine(<span class="hljs-string">"chmod"</span>, <span class="hljs-string">"+x"</span>, <span class="hljs-string">"../.git/hooks/pre-commit"</span>)
                    }
                }
            }

            <span class="hljs-comment">// execute every time `preBuild` runs</span>
         tasks.getByPath(<span class="hljs-string">":app:preBuild"</span>).dependsOn(<span class="hljs-string">"installFormattingPreCommitHook"</span>)
        }
    }
}
</code></pre>
<p>On a first pass, the custom Gradle plugin I wrote worked very well. However, after a few weeks in the wild, one of the developers pointed out to me that the task was running every build. Using Android Studio's Build Analyzer, I could confirm it was indeed running every build when it didn't necessarily need to.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681160892532/c2ae091e-2881-4508-800c-279059500429.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681160973136/d8c3a7f8-37b6-4b11-a912-8a1b8867900d.png" alt class="image--center mx-auto" /></p>
<p>(Of course, in the grand scheme of things, &lt;0.1s isn't a huge concern, but an app's poor build time is often the result of a death by a thousand cuts)</p>
<p>To avoid this, we need to define the outputs for our custom Gradle tasks so that they are cacheable. This means that if the inputs for a task have not changed since the last time it was run, Gradle can skip the task and use the cached output instead. This can save us a lot of time and speed up our builds.</p>
<p>For this particular use case, the output of the task is the existence of the pre-commit hook. To define the outputs for a custom Gradle task, we need to use the <code>outputs</code> property of the <code>Task</code> object. We can define the output files, directories, or even properties that our task generates. Here is an example of how to define the output files for our custom task:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> gitHookPreCommitFile = file(<span class="hljs-string">"../.git/hooks/pre-commit"</span>)
outputs.file(gitHookPreCommitFile.absolutePath)
</code></pre>
<p>By defining the outputs for our task in this way, Gradle can automatically check whether the input file has changed since the last time the task was run. If the input file has not changed, Gradle can skip the task (and mark it <code>UP-TO-DATE)</code> and use the cached output file instead. This can significantly speed up our builds and make our build process more efficient.</p>
<p>In addition to defining output files, we can also define output directories and properties for our custom Gradle tasks. This allows us to cache more complex data structures from our custom Gradle tasks and avoid executing our tasks unnecessarily.</p>
<p>In conclusion, defining outputs for custom Gradle tasks is an important step in optimizing our build processes. By doing so, we can make our tasks cacheable and avoid executing them unnecessarily on every incremental build. This can save us time and make our build process more efficient by not wasting CPU cycles on unnecessary tasks.</p>
<p>Full source code:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">import</span> org.gradle.api.Plugin
<span class="hljs-keyword">import</span> org.gradle.api.Project

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PreCommitPlugin</span> : <span class="hljs-type">Plugin</span>&lt;<span class="hljs-type">Project</span>&gt; </span>{
    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">apply</span><span class="hljs-params">(target: <span class="hljs-type">Project</span>)</span></span> {
        with(target) {
            tasks.register(<span class="hljs-string">"installPreCommitHook"</span>) {

                <span class="hljs-comment">// define outputs of this task so that it is not executed every time</span>
                <span class="hljs-keyword">val</span> gitHookPreCommitFile = file(<span class="hljs-string">"../.git/hooks/pre-commit"</span>)
                outputs.file(gitHookPreCommitFile.absolutePath)

                doLast {
                    exec {
                        <span class="hljs-comment">// make the bash script executable</span>
                        commandLine(<span class="hljs-string">"chmod"</span>, <span class="hljs-string">"+x"</span>, <span class="hljs-string">"../pre-commit.sh"</span>)
                    }
                    exec {
                        <span class="hljs-comment">// sym link it to the the pre-commit hook</span>
                        commandLine(<span class="hljs-string">"ln"</span>, <span class="hljs-string">"-s"</span>, <span class="hljs-string">"-f"</span>, <span class="hljs-string">"../../pre-commit.sh"</span>, <span class="hljs-string">"../.git/hooks/pre-commit"</span>)
                    }
                    exec {
                        <span class="hljs-comment">// make the symlink executable</span>
                        commandLine(<span class="hljs-string">"chmod"</span>, <span class="hljs-string">"+x"</span>, <span class="hljs-string">"../.git/hooks/pre-commit"</span>)
                    }
                }
            }

            <span class="hljs-comment">// execute every time `preBuild` runs</span>
            tasks.getByPath(<span class="hljs-string">":app:preBuild"</span>).dependsOn(<span class="hljs-string">"installPreCommitHookmai"</span>)
        }
    }
}
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Better Stack Traces with Coroutines]]></title><description><![CDATA[Have you ever encountered a stack trace involving coroutines that made you scratch your head in confusion? If so, you're not alone. Coroutines can make stack traces more complicated, but with the right approach, you can make them much easier to under...]]></description><link>https://blog.joetr.com/better-stack-traces-with-coroutines</link><guid isPermaLink="true">https://blog.joetr.com/better-stack-traces-with-coroutines</guid><category><![CDATA[Android]]></category><category><![CDATA[coroutines]]></category><dc:creator><![CDATA[Joe Roskopf]]></dc:creator><pubDate>Mon, 10 Apr 2023 17:38:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1681143431832/824af969-a155-4256-b380-97e0a5df5a0c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Have you ever encountered a stack trace involving coroutines that made you scratch your head in confusion? If so, you're not alone. Coroutines can make stack traces more complicated, but with the right approach, you can make them much easier to understand. In this post, we'll explore how to use a custom coroutine exception handler to improve stack traces involving coroutines.</p>
<p>First, let's start with a quick overview of what coroutines are. Coroutines are a type of concurrency primitive that allow you to write asynchronous code in a synchronous style. In other words, you can write code that looks like it's running synchronously, but it's actually running asynchronously under the hood.</p>
<p>One of the benefits of coroutines is that they can simplify asynchronous code by eliminating the need for callbacks or promises. However, coroutines can also make stack traces more complicated because they involve multiple points of suspension and resumption. This can make it difficult to trace the path of execution through your code.</p>
<p>To make stack traces involving coroutines easier to understand, you can use a custom coroutine exception handler. A coroutine exception handler is a mechanism that allows you to intercept and handle exceptions that occur within coroutines. By default, when an exception is thrown in a coroutine, it will propagate up the call stack until it reaches the top-level coroutine, where it will be re-thrown as an unhandled exception. This can make it difficult to pinpoint the exact location where the exception occurred.</p>
<p>With a custom coroutine exception handler, you can intercept the exception as soon as it occurs and take action to handle it. This allows you to add additional context to the exception, such as the coroutine stack trace, which can make it much easier to understand what went wrong.</p>
<p>Here's an example of how you can use a custom coroutine exception handler with a custom exception postpended to the original exception to improve stack traces involving coroutines:</p>
<pre><code class="lang-kotlin">    <span class="hljs-keyword">internal</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">createBreadcrumbsExceptionHandler</span><span class="hljs-params">()</span></span>: CoroutineExceptionHandler {
        <span class="hljs-keyword">val</span> breadcrumbsException = createBreadcrumbsException()

        <span class="hljs-keyword">return</span> CoroutineExceptionHandler { _, throwable -&gt;
            <span class="hljs-keyword">val</span> currentThread = Thread.currentThread()
            run {
                currentThread.uncaughtExceptionHandler ?: Thread.getDefaultUncaughtExceptionHandler()
            }.uncaughtException(
                currentThread,
                throwable.addBreadcrumbs(breadcrumbsException),
            )
        }
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> Throwable.<span class="hljs-title">addBreadcrumbs</span><span class="hljs-params">(
        breadcrumbsException: <span class="hljs-type">BreadcrumbException</span>,
    )</span></span>: Throwable {
    <span class="hljs-keyword">val</span> capturedThrowable = <span class="hljs-keyword">this</span>

    <span class="hljs-comment">// create a new instance of the breadcrumb exception with the original breadcrumb message</span>
    <span class="hljs-comment">// but with the cause of the throwable we are adding the breadcrumb to</span>
    <span class="hljs-keyword">val</span> newBreadcrumbWithOriginalThrowable =
      breadcrumbsException::<span class="hljs-class"><span class="hljs-keyword">class</span></span>
        .java
        .getDeclaredConstructor(String::<span class="hljs-keyword">class</span>.java, Throwable::<span class="hljs-keyword">class</span>.java)
        .newInstance(breadcrumbsException.message, capturedThrowable.cause)
        .apply { stackTrace = breadcrumbsException.stackTrace }

    <span class="hljs-comment">// create a new instance of the original throwable but with the breadcrumb exception as the cause</span>
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">this</span>::<span class="hljs-class"><span class="hljs-keyword">class</span></span>
      .java
      .getDeclaredConstructor(String::<span class="hljs-keyword">class</span>.java, Throwable::<span class="hljs-keyword">class</span>.java)
      .newInstance(message, newBreadcrumbWithOriginalThrowable)
      .apply { stackTrace = capturedThrowable.stackTrace }
    }
</code></pre>
<p>In the code above, we've declared a method for creating a custom coroutine exception handler where we add a custom <code>BreadcrumbException</code> to the original uncaught exception. This custom <code>BreadcrumbException</code> will be responsible for adding additional information to the stack trace that will better point us to the problem code in our stack trace.</p>
<p>The <code>BreadcrumbException</code> is defined as follows:</p>
<pre><code class="lang-kotlin">    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">createBreadcrumbsException</span><span class="hljs-params">()</span></span> =
        BreadcrumbException()
            .apply {
                <span class="hljs-keyword">val</span> queue: Queue&lt;StackTraceElement&gt; = LinkedList(stackTrace.asList())

                <span class="hljs-keyword">val</span> iterator = queue.iterator()
                <span class="hljs-keyword">while</span> (iterator.hasNext()) {
                    <span class="hljs-keyword">val</span> next = iterator.next()

                    <span class="hljs-comment">// we need to remove all of the 'helper' classes from the stack trace</span>
                    <span class="hljs-comment">// as that really just adds noise and isn't overly helpful</span>
                    <span class="hljs-comment">// `CoroutinesBreadcrumbExceptionHandler` is the object containing all of the functions listed so far. `FlowExtensions` and `CoroutineScopeExtensions` will make their appearance later on. </span>
                    <span class="hljs-keyword">if</span> (next.className == CoroutinesBreadcrumbExceptionHandler::<span class="hljs-keyword">class</span>.qualifiedName ||
                        next.className == FlowExtensions::<span class="hljs-keyword">class</span>.qualifiedName ||
                        next.className == CoroutineScopeExtensions::<span class="hljs-keyword">class</span>.qualifiedName
                    ) {
                        iterator.remove()
                    } <span class="hljs-keyword">else</span> {
                        break
                    }
                }
                stackTrace = queue.toTypedArray()
            }

<span class="hljs-keyword">class</span> BreadcrumbException : RuntimeException(<span class="hljs-string">"More details about the original stack trace:"</span>)
</code></pre>
<p>After defining all of the code to add our custom <code>BreadcrumbException</code> via a custom <code>CoroutineExceptionHandler</code>, how do we actually use them? The first way we can use them is by creating an extension function for launching a coroutine.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">object</span> CoroutineScopeExtensions {
    <span class="hljs-function"><span class="hljs-keyword">fun</span> CoroutineScope.<span class="hljs-title">launchWithBreadcrumbs</span><span class="hljs-params">(
        context: <span class="hljs-type">CoroutineContext</span> = EmptyCoroutineContext,
        start: <span class="hljs-type">CoroutineStart</span> = CoroutineStart.DEFAULT,
        block: <span class="hljs-type">suspend</span> <span class="hljs-type">CoroutineScope</span>.() -&gt; <span class="hljs-type">Unit</span>,
    )</span></span>: Job {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">this</span>.launch(
            context = context + createBreadcrumbsExceptionHandler(),
            start = start,
            block = block,
        )
    }
}
</code></pre>
<p>This has been a lot to setup, but what does it get us? We are going to look at some code that throws an exception and some before/after comparisons of the stack traces.</p>
<p>First, the code:</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainActivity</span> : <span class="hljs-type">AppCompatActivity</span></span>() {
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> viewModel : MainViewModel <span class="hljs-keyword">by</span> viewModels()

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onCreate</span><span class="hljs-params">(savedInstanceState: <span class="hljs-type">Bundle</span>?)</span></span> {
        <span class="hljs-keyword">super</span>.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        viewModel.testException()
    }
}
</code></pre>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainViewModel</span> : <span class="hljs-type">ViewModel</span></span>() {

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">testException</span><span class="hljs-params">()</span></span> {
        viewModelScope.launch {
            throwException1()
        }
    }

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">throwException1</span><span class="hljs-params">()</span></span> {
        delay(<span class="hljs-number">300</span>)
        throwException2()
    }

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">throwException2</span><span class="hljs-params">()</span></span> {
        delay(<span class="hljs-number">300</span>)
        throwException3()
    }

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">throwException3</span><span class="hljs-params">()</span></span> {
        delay(<span class="hljs-number">300</span>)
        <span class="hljs-keyword">throw</span> IllegalArgumentException(<span class="hljs-string">"oh no"</span>)
    }
}
</code></pre>
<p>The code above provides the following stack trace:</p>
<pre><code class="lang-kotlin">E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.joetr.coroutines, PID: <span class="hljs-number">11825</span>
    java.lang.IllegalArgumentException: oh no
        at com.joetr.coroutines.MainViewModel.throwException3(MainViewModel.kt:<span class="hljs-number">36</span>)
        at com.joetr.coroutines.MainViewModel.access$throwException3(MainViewModel.kt:<span class="hljs-number">16</span>)
        at com.joetr.coroutines.MainViewModel$throwException3$<span class="hljs-number">1</span>.invokeSuspend(Unknown Source:<span class="hljs-number">14</span>)
        at kotlin.coroutines.jvm.<span class="hljs-keyword">internal</span>.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:<span class="hljs-number">33</span>)
        at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:<span class="hljs-number">234</span>)
        at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:<span class="hljs-number">166</span>)
        at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:<span class="hljs-number">397</span>)
        at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:<span class="hljs-number">431</span>)
        at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:<span class="hljs-number">420</span>)
        at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:<span class="hljs-number">518</span>)
        at kotlinx.coroutines.android.HandlerContext$scheduleResumeAfterDelay$$inlined$Runnable$<span class="hljs-number">1</span>.run(Runnable.kt:<span class="hljs-number">19</span>)
        at android.os.Handler.handleCallback(Handler.java:<span class="hljs-number">942</span>)
        at android.os.Handler.dispatchMessage(Handler.java:<span class="hljs-number">99</span>)
        at android.os.Looper.loopOnce(Looper.java:<span class="hljs-number">201</span>)
        at android.os.Looper.loop(Looper.java:<span class="hljs-number">288</span>)
        at android.app.ActivityThread.main(ActivityThread.java:<span class="hljs-number">7898</span>)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.<span class="hljs-keyword">internal</span>.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:<span class="hljs-number">548</span>)
        at com.android.<span class="hljs-keyword">internal</span>.os.ZygoteInit.main(ZygoteInit.java:<span class="hljs-number">936</span>)
        Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@<span class="hljs-number">37</span>c4392, Dispatchers.Main.immediate]
</code></pre>
<p>But when we modify the code that launches the coroutine to:</p>
<pre><code class="lang-kotlin">    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">testException</span><span class="hljs-params">()</span></span> {
        <span class="hljs-comment">// using our custom `launchWithBreadcrumbs` method</span>
        viewModelScope.launchWithBreadcrumbs {
            throwException1()
        }
    }
</code></pre>
<p>We get a stack trace with a good bit more information in it. When we examine the following stack trace, we can see references to <code>MainActivity</code>'s <code>onCreate</code> and <code>MainViewModel</code>'s <code>testException</code> method, which were both originally missing in the first stack trace.</p>
<pre><code class="lang-kotlin">E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.joetr.coroutines, PID: <span class="hljs-number">12126</span>
    java.lang.IllegalArgumentException: oh no
        at com.joetr.coroutines.MainViewModel.throwException3(MainViewModel.kt:<span class="hljs-number">36</span>)
        at com.joetr.coroutines.MainViewModel.access$throwException3(MainViewModel.kt:<span class="hljs-number">16</span>)
        at com.joetr.coroutines.MainViewModel$throwException3$<span class="hljs-number">1</span>.invokeSuspend(Unknown Source:<span class="hljs-number">14</span>)
        at kotlin.coroutines.jvm.<span class="hljs-keyword">internal</span>.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:<span class="hljs-number">33</span>)
        at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:<span class="hljs-number">234</span>)
        at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:<span class="hljs-number">166</span>)
        at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:<span class="hljs-number">397</span>)
        at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:<span class="hljs-number">431</span>)
        at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:<span class="hljs-number">420</span>)
        at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:<span class="hljs-number">518</span>)
        at kotlinx.coroutines.android.HandlerContext$scheduleResumeAfterDelay$$inlined$Runnable$<span class="hljs-number">1</span>.run(Runnable.kt:<span class="hljs-number">19</span>)
        at android.os.Handler.handleCallback(Handler.java:<span class="hljs-number">942</span>)
        at android.os.Handler.dispatchMessage(Handler.java:<span class="hljs-number">99</span>)
        at android.os.Looper.loopOnce(Looper.java:<span class="hljs-number">201</span>)
        at android.os.Looper.loop(Looper.java:<span class="hljs-number">288</span>)
        at android.app.ActivityThread.main(ActivityThread.java:<span class="hljs-number">7898</span>)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.<span class="hljs-keyword">internal</span>.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:<span class="hljs-number">548</span>)
        at com.android.<span class="hljs-keyword">internal</span>.os.ZygoteInit.main(ZygoteInit.java:<span class="hljs-number">936</span>)
     Caused <span class="hljs-keyword">by</span>: com.joetr.coroutines.BreadcrumbException: More details about the original stack trace:
        at com.joetr.coroutines.MainViewModel.testException(MainViewModel.kt:<span class="hljs-number">19</span>)
        at com.joetr.coroutines.MainActivity.onCreate(MainActivity.kt:<span class="hljs-number">21</span>)
        at android.app.Activity.performCreate(Activity.java:<span class="hljs-number">8290</span>)
        at android.app.Activity.performCreate(Activity.java:<span class="hljs-number">8269</span>)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:<span class="hljs-number">1384</span>)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:<span class="hljs-number">3657</span>)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:<span class="hljs-number">3813</span>)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:<span class="hljs-number">101</span>)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:<span class="hljs-number">135</span>)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:<span class="hljs-number">95</span>)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:<span class="hljs-number">2308</span>)
        at android.os.Handler.dispatchMessage(Handler.java:<span class="hljs-number">106</span>)
        at android.os.Looper.loopOnce(Looper.java:<span class="hljs-number">201</span>) 
        at android.os.Looper.loop(Looper.java:<span class="hljs-number">288</span>) 
        at android.app.ActivityThread.main(ActivityThread.java:<span class="hljs-number">7898</span>) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.<span class="hljs-keyword">internal</span>.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:<span class="hljs-number">548</span>) 
        at com.android.<span class="hljs-keyword">internal</span>.os.ZygoteInit.main(ZygoteInit.java:<span class="hljs-number">936</span>)
</code></pre>
<h2 id="heading-stateflow">StateFlow</h2>
<p>A common pattern in the Android world is to expose the state of your feature from your ViewModel to be collected in your Fragment / Activity. The second way we can use our custom exception handler is via an extension on the <code>stateIn</code> function provided via the <a target="_blank" href="https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/state-in.html">Coroutines</a> package.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">object</span> FlowExtensions {
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-type">&lt;T&gt;</span> Flow<span class="hljs-type">&lt;T&gt;</span>.<span class="hljs-title">stateInWithBreadcrumbs</span><span class="hljs-params">(
        scope: <span class="hljs-type">CoroutineScope</span>,
        started: <span class="hljs-type">SharingStarted</span>,
        initialValue: <span class="hljs-type">T</span>,
    )</span></span>: StateFlow&lt;T&gt; {
        <span class="hljs-keyword">return</span> stateIn(
            scope = scope + createBreadcrumbsExceptionHandler(),
            started = started,
            initialValue = initialValue,
        )
    }
}
</code></pre>
<p>When we use the standard <code>stateIn</code> method, there is a lot to be desired about the stack trace.</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainViewModel</span> : <span class="hljs-type">ViewModel</span></span>() {
    <span class="hljs-keyword">val</span> state = flow&lt;String&gt; {
        <span class="hljs-keyword">throw</span> IllegalArgumentException(<span class="hljs-string">"oh no"</span>)
    }.stateIn(
        scope = viewModelScope,
        started = SharingStarted.Eagerly,
        initialValue = <span class="hljs-string">""</span>,
    )
}
</code></pre>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainActivity</span> : <span class="hljs-type">AppCompatActivity</span></span>() {
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> viewModel : MainViewModel <span class="hljs-keyword">by</span> viewModels()

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onCreate</span><span class="hljs-params">(savedInstanceState: <span class="hljs-type">Bundle</span>?)</span></span> {
        <span class="hljs-keyword">super</span>.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        lifecycleScope.launch {
            viewModel.state.collect {
                Log.d(<span class="hljs-string">"MainActivity"</span>, it)
            }
        }
    }
}
</code></pre>
<p>The code above provides the following stack trace:</p>
<pre><code class="lang-kotlin">E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.joetr.coroutines, PID: <span class="hljs-number">12872</span>
    java.lang.IllegalArgumentException: oh no
        at com.joetr.coroutines.MainViewModel$state$<span class="hljs-number">1</span>.invokeSuspend(MainViewModel.kt:<span class="hljs-number">19</span>)
        at com.joetr.coroutines.MainViewModel$state$<span class="hljs-number">1</span>.invoke(Unknown Source:<span class="hljs-number">8</span>)
        at com.joetr.coroutines.MainViewModel$state$<span class="hljs-number">1</span>.invoke(Unknown Source:<span class="hljs-number">4</span>)
        at kotlinx.coroutines.flow.SafeFlow.collectSafely(Builders.kt:<span class="hljs-number">61</span>)
        at kotlinx.coroutines.flow.AbstractFlow.collect(Flow.kt:<span class="hljs-number">230</span>)
        at kotlinx.coroutines.flow.FlowKt__ShareKt$launchSharing$<span class="hljs-number">1</span>.invokeSuspend(Share.kt:<span class="hljs-number">214</span>)
        at kotlin.coroutines.jvm.<span class="hljs-keyword">internal</span>.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:<span class="hljs-number">33</span>)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:<span class="hljs-number">106</span>)
        at kotlinx.coroutines.EventLoop.processUnconfinedEvent(EventLoop.common.kt:<span class="hljs-number">69</span>)
        at kotlinx.coroutines.<span class="hljs-keyword">internal</span>.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:<span class="hljs-number">376</span>)
        at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:<span class="hljs-number">30</span>)
        at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable$default(Cancellable.kt:<span class="hljs-number">25</span>)
        at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:<span class="hljs-number">110</span>)
        at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:<span class="hljs-number">126</span>)
        at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders.common.kt:<span class="hljs-number">56</span>)
        at kotlinx.coroutines.BuildersKt.launch(Unknown Source:<span class="hljs-number">1</span>)
        at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch$default(Builders.common.kt:<span class="hljs-number">47</span>)
        at kotlinx.coroutines.BuildersKt.launch$default(Unknown Source:<span class="hljs-number">1</span>)
        at com.joetr.coroutines.MainActivity.onCreate(MainActivity.kt:<span class="hljs-number">22</span>)
        at android.app.Activity.performCreate(Activity.java:<span class="hljs-number">8290</span>)
        at android.app.Activity.performCreate(Activity.java:<span class="hljs-number">8269</span>)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:<span class="hljs-number">1384</span>)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:<span class="hljs-number">3657</span>)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:<span class="hljs-number">3813</span>)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:<span class="hljs-number">101</span>)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:<span class="hljs-number">135</span>)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:<span class="hljs-number">95</span>)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:<span class="hljs-number">2308</span>)
        at android.os.Handler.dispatchMessage(Handler.java:<span class="hljs-number">106</span>)
        at android.os.Looper.loopOnce(Looper.java:<span class="hljs-number">201</span>)
        at android.os.Looper.loop(Looper.java:<span class="hljs-number">288</span>)
        at android.app.ActivityThread.main(ActivityThread.java:<span class="hljs-number">7898</span>)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.<span class="hljs-keyword">internal</span>.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:<span class="hljs-number">548</span>)
        at com.android.<span class="hljs-keyword">internal</span>.os.ZygoteInit.main(ZygoteInit.java:<span class="hljs-number">936</span>)
        Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}<span class="hljs-meta">@a43f471</span>, Dispatchers.Main.immediate]
</code></pre>
<p>When we use our custom <code>stateInWithBreadcrumb</code> function, we can see that we again get some more information in our stack trace.</p>
<pre><code class="lang-kotlin">FATAL EXCEPTION: main                                                                              Process: com.joetr.dynamicproxies, PID: <span class="hljs-number">13069</span>                                                                               java.lang.IllegalArgumentException: oh no
    at com.joetr.coroutines.MainViewModel$state$<span class="hljs-number">1</span>.invokeSuspend(MainViewModel.kt:<span class="hljs-number">19</span>)
    at com.joetr.coroutines.MainViewModel$state$<span class="hljs-number">1</span>.invoke(Unknown Source:<span class="hljs-number">8</span>)
    at com.joetr.coroutines.MainViewModel$state$<span class="hljs-number">1</span>.invoke(Unknown Source:<span class="hljs-number">4</span>)
    at kotlinx.coroutines.flow.SafeFlow.collectSafely(Builders.kt:<span class="hljs-number">61</span>)
    at kotlinx.coroutines.flow.AbstractFlow.collect(Flow.kt:<span class="hljs-number">230</span>)
    at kotlinx.coroutines.flow.FlowKt__ShareKt$launchSharing$<span class="hljs-number">1</span>.invokeSuspend(Share.kt:<span class="hljs-number">214</span>)
    at kotlin.coroutines.jvm.<span class="hljs-keyword">internal</span>.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:<span class="hljs-number">33</span>)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:<span class="hljs-number">106</span>)
    at kotlinx.coroutines.EventLoop.processUnconfinedEvent(EventLoop.common.kt:<span class="hljs-number">69</span>)
    at kotlinx.coroutines.<span class="hljs-keyword">internal</span>.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:<span class="hljs-number">376</span>)
    at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:<span class="hljs-number">30</span>)
    at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable$default(Cancellable.kt:<span class="hljs-number">25</span>)
    at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:<span class="hljs-number">110</span>)
    at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:<span class="hljs-number">126</span>)
    at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders.common.kt:<span class="hljs-number">56</span>)
    at kotlinx.coroutines.BuildersKt.launch(Unknown Source:<span class="hljs-number">1</span>)
    at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch$default(Builders.common.kt:<span class="hljs-number">47</span>)
    at kotlinx.coroutines.BuildersKt.launch$default(Unknown Source:<span class="hljs-number">1</span>)
    at com.joetr.coroutines.MainActivity.onCreate(MainActivity.kt:<span class="hljs-number">22</span>)
    at android.app.Activity.performCreate(Activity.java:<span class="hljs-number">8290</span>)
    at android.app.Activity.performCreate(Activity.java:<span class="hljs-number">8269</span>)
    at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:<span class="hljs-number">1384</span>)
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:<span class="hljs-number">3657</span>)
    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:<span class="hljs-number">3813</span>)
    at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:<span class="hljs-number">101</span>)
    at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:<span class="hljs-number">135</span>)
    at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:<span class="hljs-number">95</span>)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:<span class="hljs-number">2308</span>)
    at android.os.Handler.dispatchMessage(Handler.java:<span class="hljs-number">106</span>)
    at android.os.Looper.loopOnce(Looper.java:<span class="hljs-number">201</span>)
    at android.os.Looper.loop(Looper.java:<span class="hljs-number">288</span>)
    at android.app.ActivityThread.main(ActivityThread.java:<span class="hljs-number">7898</span>)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.<span class="hljs-keyword">internal</span>.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:<span class="hljs-number">548</span>)
    at com.android.<span class="hljs-keyword">internal</span>.os.ZygoteInit.main(ZygoteInit.java:<span class="hljs-number">936</span>)
Caused <span class="hljs-keyword">by</span>: com.joetr.coroutines.BreadcrumbException: More details about the original stack trace:
    at com.joetr.coroutines.MainViewModel.&lt;<span class="hljs-keyword">init</span>&gt;(MainViewModel.kt:<span class="hljs-number">20</span>)
    at java.lang.reflect.Constructor.newInstance0(Native Method)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:<span class="hljs-number">343</span>)
    at androidx.lifecycle.ViewModelProvider$NewInstanceFactory.create(ViewModelProvider.kt:<span class="hljs-number">202</span>)
    at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.kt:<span class="hljs-number">324</span>)
    at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.kt:<span class="hljs-number">306</span>)
    at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.kt:<span class="hljs-number">280</span>)
    at androidx.lifecycle.SavedStateViewModelFactory.create(SavedStateViewModelFactory.kt:<span class="hljs-number">128</span>)
    at dagger.hilt.android.<span class="hljs-keyword">internal</span>.lifecycle.HiltViewModelFactory.create(HiltViewModelFactory.java:<span class="hljs-number">118</span>)
    at androidx.lifecycle.ViewModelProvider.<span class="hljs-keyword">get</span>(ViewModelProvider.kt:<span class="hljs-number">187</span>)
    at androidx.lifecycle.ViewModelProvider.<span class="hljs-keyword">get</span>(ViewModelProvider.kt:<span class="hljs-number">153</span>)
    at androidx.lifecycle.ViewModelLazy.getValue(ViewModelLazy.kt:<span class="hljs-number">53</span>)
    at androidx.lifecycle.ViewModelLazy.getValue(ViewModelLazy.kt:<span class="hljs-number">35</span>)
    at com.joetr.coroutines.MainActivity.getViewModel(MainActivity.kt:<span class="hljs-number">16</span>)
<span class="hljs-number">2023</span>-<span class="hljs-number">04</span>-<span class="hljs-number">09</span> <span class="hljs-number">19</span>:<span class="hljs-number">37</span>:<span class="hljs-number">10.303</span> <span class="hljs-number">13069</span>-<span class="hljs-number">13069</span> AndroidRuntime          com.joetr.dynamicproxies             E      at com.joetr.coroutines.MainActivity.access$getViewModel(MainActivity.kt:<span class="hljs-number">14</span>)
    at com.joetr.coroutines.MainActivity$onCreate$<span class="hljs-number">1</span>.invokeSuspend(MainActivity.kt:<span class="hljs-number">23</span>)
    at kotlin.coroutines.jvm.<span class="hljs-keyword">internal</span>.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:<span class="hljs-number">33</span>)
    at kotlinx.coroutines.<span class="hljs-keyword">internal</span>.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:<span class="hljs-number">367</span>)
                                                                                                        ... <span class="hljs-number">25</span> more
</code></pre>
<p>By using a custom coroutine exception handler, we can improve the readability of stack traces involving coroutines. This allows us to quickly and easily identify the location of the exception and the path of execution leading up to it.</p>
<p>In conclusion, coroutines can make stack traces more complicated, but with the right approach, you can make them much easier to understand. By using a custom coroutine exception handler, you can intercept exceptions as soon as they occur and add additional context to the exception, such as the coroutine stack trace. This can make it much easier to identify the location of the exception and the path of execution leading up to it.</p>
<p>Gist with all relevant code can be found <a target="_blank" href="https://gist.github.com/j-roskopf/3e150e7734c906be5cff9bf1b29ebd46">here</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Replacing ViewModelScope In Your ViewModels to Better Test Coroutines]]></title><description><![CDATA[Android Architecture Components (AAC) provides several libraries that help you design robust, testable, and maintainable apps. One of the most important components of AAC is the ViewModel, which provides a lifecycle-aware container for UI-related dat...]]></description><link>https://blog.joetr.com/replacing-viewmodelscope-in-your-viewmodels-to-better-test-coroutines</link><guid isPermaLink="true">https://blog.joetr.com/replacing-viewmodelscope-in-your-viewmodels-to-better-test-coroutines</guid><category><![CDATA[Android]]></category><category><![CDATA[ViewModel]]></category><category><![CDATA[coroutines]]></category><dc:creator><![CDATA[Joe Roskopf]]></dc:creator><pubDate>Wed, 05 Apr 2023 15:21:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1680706369740/d7e6f541-3d62-49f0-8bc4-7adeddf93448.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Android Architecture Components (AAC) provides several libraries that help you design robust, testable, and maintainable apps. One of the most important components of AAC is the <code>ViewModel</code>, which provides a lifecycle-aware container for UI-related data that survives configuration changes. The <code>ViewModel</code> can also be responsible for managing long-running tasks such as network requests, database queries, and other I/O operations.</p>
<p>One of the challenges of working with coroutines in Android is how to properly manage their lifecycle in a testable way. One popular approach is to use the <code>ViewModelScope</code>, which is a lifecycle-aware scope that automatically cancels all coroutines when the associated <code>ViewModel</code> is destroyed. However, using <code>ViewModelScope</code> can make it difficult to test coroutine code in isolation because the coroutines are tightly coupled to the lifecycle of the <code>ViewModel</code> and the <code>ViewModelScope</code> isn't aware about the structured concurrency within your <code>runTest</code> block (from the coroutines-test <a target="_blank" href="https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/run-test.html">package</a>).</p>
<p>In this blog post, we'll discuss why replacing <code>ViewModelScope</code> within AAC ViewModels with a custom solution may be a good idea, and how you can implement a better solution that allows for better testing of coroutine code (which also allows us to move away from having to set <code>Dispatchers.setMain()</code>.</p>
<h2 id="heading-the-problem-with-viewmodelscope"><strong>The Problem with ViewModelScope</strong></h2>
<p>When you use <code>ViewModelScope</code> to launch a coroutine, the coroutine is automatically tied to the lifecycle of the <code>ViewModel</code>. This means that if the <code>ViewModel</code> is destroyed, all running coroutines are canceled. While this behavior is useful in many cases, it can also make it difficult to test coroutine code in isolation. Additionally, the <code>ViewModelScope</code> isn't aware of the structured concurrency that exists within your <code>runTest</code> block, which can make it difficult to define the behavior we want for testing coroutines.</p>
<h2 id="heading-a-custom-solution">A Custom Solution</h2>
<p>The solution we will be striving towards today involves using the <code>ViewModelScope</code> via Hilt in production code, and using the <code>backgroundScope</code> provided to us via <a target="_blank" href="https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-scope/">TestScope</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1680704045333/27ce9fe2-ad65-4ea6-81c5-307ee6aaeb53.png" alt class="image--center mx-auto" /></p>
<p>The first thing we need to do is create the <code>CoroutineScope</code> we will be providing via Hilt to use in our ViewModel. This <code>CloseableCoroutineScope</code>'s <code>Job</code> still needs to be canceled when the <code>ViewModelScope</code> is cleared, which is why we cancel it in <code>onCleared()</code></p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CloseableCoroutineScope</span></span>(context: CoroutineContext) : CoroutineScope, RetainedLifecycle.OnClearedListener {
    <span class="hljs-keyword">override</span> <span class="hljs-keyword">val</span> coroutineContext: CoroutineContext = context

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onCleared</span><span class="hljs-params">()</span></span> {
        coroutineContext.cancel()
    }
}
</code></pre>
<p>Then, via our Hilt module, we can provide this scope in the <code>ViewModelComponent</code>. It is important to note, that we are also calling <code>addOnClearedListener</code> here, so the scope is canceled within the <code>ViewModelLifecycle</code></p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Module</span>
<span class="hljs-meta">@InstallIn(ViewModelComponent::class)</span>
<span class="hljs-keyword">internal</span> <span class="hljs-keyword">object</span> ViewModelScopeModule {
    <span class="hljs-meta">@Provides</span>
    <span class="hljs-meta">@ViewModelScoped</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">provideViewModelCoroutineScope</span><span class="hljs-params">(lifecycle: <span class="hljs-type">ViewModelLifecycle</span>)</span></span>: CoroutineScope {
        <span class="hljs-keyword">return</span> CloseableCoroutineScope(SupervisorJob()).also { closeableCoroutineScope -&gt;
            lifecycle.addOnClearedListener(closeableCoroutineScope)
        }
    }
}
</code></pre>
<h2 id="heading-testing">Testing</h2>
<p>Now that we have the basic setup in place, we can finally use it! First, we will create a <code>ViewModel</code> that takes in our new scope.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@HiltViewModel</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainViewModel</span> <span class="hljs-meta">@Inject</span> <span class="hljs-keyword">constructor</span></span>(
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> scope: CoroutineScope,
) : ViewModel()
</code></pre>
<p>Then, when we want to use that scope, we can do so via:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@HiltViewModel</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainViewModel</span> <span class="hljs-meta">@Inject</span> <span class="hljs-keyword">constructor</span></span>(
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> scope: CoroutineScope,
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> apiService: ApiService,
) : ViewModel() {

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> _result = MutableSharedFlow&lt;<span class="hljs-built_in">Int</span>&gt;()
    <span class="hljs-keyword">val</span> result = _result.asSharedFlow()

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getResult</span><span class="hljs-params">()</span></span> {
        scope.launch {
            _result.emit(apiService.getResult())
        }
    }
}
</code></pre>
<p>The testing of this is really the impetus behind why we want to avoid <code>ViewModelScope</code>. Now that we are no longer using <code>ViewModelScope</code>, our testing setup becomes simpler. In this example, I am using <a target="_blank" href="https://github.com/cashapp/turbine">Turbine</a> for testing.</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainViewModelTest</span> </span>{

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> fakeApiService = <span class="hljs-keyword">object</span> : ApiService {
        <span class="hljs-keyword">override</span> <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getResult</span><span class="hljs-params">()</span></span> = <span class="hljs-number">42</span>
    }

    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> `result <span class="hljs-keyword">is</span> emitted after fetching`<span class="hljs-params">()</span></span> {
        runTest {
            <span class="hljs-keyword">val</span> viewModel = MainViewModel(
                scope = <span class="hljs-keyword">this</span>.backgroundScope,
                apiService = fakeApiService,
            )

            viewModel.result.test {
                viewModel.getResult()
                assertEquals(<span class="hljs-number">42</span>, awaitItem())
            }
        }
    }
}
</code></pre>
<p>From this sample code, we can see that we didn't have to set the main dispatcher anywhere, and we were able to use the <code>backgroundScope</code> provided via <code>TestScope</code> in our ViewModel.</p>
<p>Let's now compare this test code to what we would have to write if we instead used <code>viewModelScope</code> to launch the coroutine in the ViewModel.</p>
<pre><code class="lang-kotlin"><span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getResult</span><span class="hljs-params">()</span></span> {
        viewModelScope.launch {
            _result.emit(apiService.getResult())
        }
    }
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1680718219770/dac3a4f0-3a82-4be0-8eaf-8299d1f48d7d.png" alt class="image--center mx-auto" /></p>
<p>Unfortunately, when we run our test now, we get an error. Referring to the <a target="_blank" href="https://developer.android.com/kotlin/coroutines/test#setting-main-dispatcher">docs</a>, we can see "If your code under test references the main thread, it’ll throw an exception during unit tests.". To get around this, we can create a JUnit Test Rule to automatically set the main dispatcher.</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainDispatcherRule</span></span>(
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">starting</span><span class="hljs-params">(description: <span class="hljs-type">Description</span>)</span></span> {
        Dispatchers.setMain(testDispatcher)
    }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">finished</span><span class="hljs-params">(description: <span class="hljs-type">Description</span>)</span></span> {
        Dispatchers.resetMain()
    }
}
</code></pre>
<p>This test rule can now be used to get our tests to pass as expected</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainViewModelTest</span> </span>{

    <span class="hljs-meta">@get:Rule</span>
    <span class="hljs-keyword">val</span> mainDispatcherRule = MainDispatcherRule()

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> fakeApiService = <span class="hljs-keyword">object</span> : ApiService {
        <span class="hljs-keyword">override</span> <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getResult</span><span class="hljs-params">()</span></span> = <span class="hljs-number">42</span>
    }

    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> `result <span class="hljs-keyword">is</span> emitted after fetching`<span class="hljs-params">()</span></span> {
        runTest {
            <span class="hljs-keyword">val</span> viewModel = MainViewModel(
                scope = <span class="hljs-keyword">this</span>.backgroundScope,
                apiService = fakeApiService,
            )

            viewModel.result.test {
                viewModel.getResult()
                assertEquals(<span class="hljs-number">42</span>, awaitItem())
            }
        }
    }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainDispatcherRule</span></span>(
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">starting</span><span class="hljs-params">(description: <span class="hljs-type">Description</span>)</span></span> {
        Dispatchers.setMain(testDispatcher)
    }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">finished</span><span class="hljs-params">(description: <span class="hljs-type">Description</span>)</span></span> {
        Dispatchers.resetMain()
    }
}
</code></pre>
<p>While we were still able to get a working solution by setting the main dispatcher, that is ultimately an undesirable workaround because for everything to play together nicely, we'd have to pass the TestDispatcher around for anything that wanted to know about it so that the structured concurrency in our <code>runTest</code> block will play nicely.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Using <code>ViewModelScope</code> to launch coroutines can be convenient, but it can also make it difficult to test coroutine code in isolation. By creating a custom scope that's decoupled from the lifecycle of the `ViewModel`, as well as that's managed in isolation in your test, you can launch coroutines in a way that's more testable and easier to manage.</p>
<p>In this blog post, we discussed how to create a custom scope and use it in a <code>ViewModel</code> to launch coroutines. We also showed how to write a unit test for coroutine code that uses a custom scope.</p>
]]></content:encoded></item><item><title><![CDATA[Debugging via Decompilation: Reverse Engineering an APK]]></title><description><![CDATA[Recently, while looking at integrating Braze into an application, I came across a field in BrazeFileUtils called REMOTE_SCHEMES and I was curious to see what the values were that Braze considered for REMOTE_SCHEMES.
Looking at the docs, there was no ...]]></description><link>https://blog.joetr.com/debugging-via-decompilation-reverse-engineering-an-apk</link><guid isPermaLink="true">https://blog.joetr.com/debugging-via-decompilation-reverse-engineering-an-apk</guid><category><![CDATA[Android]]></category><category><![CDATA[apktool]]></category><category><![CDATA[jadx]]></category><dc:creator><![CDATA[Joe Roskopf]]></dc:creator><pubDate>Tue, 04 Apr 2023 20:43:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1680640923397/c10d3d48-e59c-4d23-9911-3aec4e03e123.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently, while looking at integrating Braze into an application, I came across a field in <code>BrazeFileUtils</code> called <code>REMOTE_SCHEMES</code> and I was curious to see what the values were that Braze considered for <code>REMOTE_SCHEMES</code>.</p>
<p>Looking at the <a target="_blank" href="https://appboy.github.io/appboy-android-sdk/javadocs/com/braze/support/BrazeFileUtils.html#REMOTE_SCHEMES">docs</a>, there was no reference to what the schemes were, so I thought I'd do a little digging myself to see if I could infer what the value was for this field.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1680637863466/f5a39052-66e8-4b5b-aeb5-0963ba6fef6b.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-setup">Setup</h2>
<p>The first step of the process was to create a new Android application that include the dependency I wanted to look at, namely <code>BrazeFileUtils</code>. This can be accomplished by bringing the Braze SDK into our application via</p>
<pre><code class="lang-kotlin">    implementation <span class="hljs-string">'com.appboy:android-sdk-ui:24.3.0'</span>
    implementation <span class="hljs-string">'com.appboy:android-sdk-location:24.3.0'</span>
</code></pre>
<p>After that, I generated a debuggable apk via <code>./gradlew :app:assembleDebug</code>. Once I had a debug APK in hand, the next tool I brought in to help was <a target="_blank" href="https://ibotpeaches.github.io/Apktool/">Apktool</a>, a tool for reverse engineering Android apk files.</p>
<h2 id="heading-decompiling">Decompiling</h2>
<ol>
<li><p>By following the instructions on the site, and downloading the latest Apktool JAR file, we are ready to decompile the debug APK we generated earlier. I decompiled the apk via <code>java -jar apktool.jar d app-debug.apk</code> where <code>apktool.jar</code> is the jar downloaded from the Apktool website and <code>app-debug.apk</code> is the debug APK generated from the previous step.</p>
</li>
<li><p>After decompilng via Apktool, we are ultimately left with .smali files, the assembly language used by the Android Dalvik Virtual Machine; usually created by decompiling. We are getting close though! If we open up the new <code>app-debug</code> folder created from Apktool, we can look inside <code>/smali/com/braze/support/BrazeFileUtils.smali</code> and see the field we want! Unfortunately, we don't see the values for it yet.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1680638657929/3d557cc6-1457-4ce4-8503-ac12df9156e5.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1680638806854/d26719b7-aa03-4b80-9fc2-d6c612f09527.png" alt class="image--center mx-auto" /></p>
<ol>
<li><p>The next step is to use a Dex (Smali) to Java decompiler to get the code in a format a little more digestible by a human. For this, we will use <a target="_blank" href="https://github.com/skylot/jadx">jadx</a>. Following the instructions in the readme, and installing jadx via brew (<code>brew install jadx</code>), we are ready for the next step.</p>
</li>
<li><p>After installing jadx, we can finally get the Java code from the .smali files generated earlier via <code>jadx -d out app-debug/smali/com/braze/support/BrazeFileUtils.smali.</code> Usage is from the docs on the site as follows:</p>
<pre><code class="lang-kotlin"> jadx[-gui] [options] &lt;input files&gt; (.apk, .dex, .jar, .<span class="hljs-keyword">class</span>, .smali, .zip, .aar, .arsc, .aab)
 options:
   -d, --output-dir                    - output directory
</code></pre>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1680639302607/77420d4f-fdca-41b3-ad87-a529c2dc9743.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1680639371951/7b707933-b7f3-4e28-a013-d234d2c12a03.png" alt class="image--center mx-auto" /></p>
<p>After opening up the Java file, we can finally see what the remote schemes are!</p>
<p>In conclusion, we utilized <code>Apktool</code> and <code>jadx</code> to reverse engineer source from a debug apk. This can be useful when a 3rd party tool that you have to utilize doesn't expose certain things about their SDK.</p>
]]></content:encoded></item></channel></rss>