<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ColtonIdle</title>
    <description>The latest articles on DEV Community by ColtonIdle (@coltonidle).</description>
    <link>https://dev.to/coltonidle</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F612093%2Fd7372280-90b8-48a6-89c8-4e0ec1e537fd.png</url>
      <title>DEV Community: ColtonIdle</title>
      <link>https://dev.to/coltonidle</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/coltonidle"/>
    <language>en</language>
    <item>
      <title>How to create a "convention" plugin for your multi-module Android app</title>
      <dc:creator>ColtonIdle</dc:creator>
      <pubDate>Sun, 25 Jan 2026 23:49:29 +0000</pubDate>
      <link>https://dev.to/coltonidle/how-to-create-a-convention-plugin-for-your-multi-module-android-app-479k</link>
      <guid>https://dev.to/coltonidle/how-to-create-a-convention-plugin-for-your-multi-module-android-app-479k</guid>
      <description>&lt;p&gt;One thing that trips me up all the time is how to common-ize my build files in a multi-module Android app. Every time I try to learn it, I give up because of overloaded terms, potential footguns, and possible slowdowns in my app. IMO this should be a lot easier, so I end up just duplicating code and sometimes I will just have some sort of &lt;code&gt;subprojects.all{}&lt;/code&gt; block in my root build.gradle to apply something to all of my modules instead.&lt;/p&gt;

&lt;p&gt;I'm trying to learn once more with a very simple case where I have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Android &lt;code&gt;app&lt;/code&gt; module&lt;/li&gt;
&lt;li&gt;Android library &lt;code&gt;lib1&lt;/code&gt; module&lt;/li&gt;
&lt;li&gt;Android library &lt;code&gt;lib2&lt;/code&gt; module&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And I want to extract the common code in the android libraries (lib1 and lib2)&lt;/p&gt;

&lt;p&gt;Some general notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;According to &lt;a href="https://docs.gradle.org/current/userguide/best_practices_structuring_builds.html#favor_composite_builds" rel="noopener noreferrer"&gt;https://docs.gradle.org/current/userguide/best_practices_structuring_builds.html#favor_composite_builds&lt;/a&gt;) that buildSrc isn't recommended and so I should go down a path of a convention plugin for sharing build logic&lt;/li&gt;
&lt;li&gt;Convention plugin is a loaded term and you can have convention plugins in both buildSrc and build-logic . Similarly, you can write your convention plugins as "precompiled script plugins" (.kts ) or regular "binary plugins" (.kt)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/autonomousapps/gradle-glossary" rel="noopener noreferrer"&gt;https://github.com/autonomousapps/gradle-glossary&lt;/a&gt; is a good resource to brush up on gradle terms&lt;/li&gt;
&lt;li&gt;In Android you have gradle "modules", but these modules are really "projects" in the eyes of gradle. Similarly, you might be used to call something a gradle "project" if it uses gradle to build, but in the eyes of gradle this is called a "build". Hence why adding a convention plugin requires you to create another gradle build in your repo, and add it to your initial gradle build via "includedBuild"&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/android/nowinandroid/issues/39" rel="noopener noreferrer"&gt;NowInAndroid&lt;/a&gt; saved ~12s in some cases by removing precompiled script plugins&lt;/li&gt;
&lt;li&gt;If you want the fastest possible performance, you want to publish your convention plugins (annoying for your "typical" android app) (&lt;a href="https://developer.squareup.com/blog/herding-elephants/#a-wild-performance-regression-appeared-from-included-builds-to-publishing-our-build-logic" rel="noopener noreferrer"&gt;see here&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;If you see &lt;code&gt;kotlin-dsl&lt;/code&gt; in your build, you can treat this as an indication that you can try to remove it in order to gain some build speed&lt;/li&gt;
&lt;li&gt;Read &lt;a href="https://mbonnin.net/2025-07-10_the_case_for_kgp/" rel="noopener noreferrer"&gt;https://mbonnin.net/2025-07-10_the_case_for_kgp/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Many definitions of a "convention plugin"&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Convention plugins are just regular plugins&lt;/li&gt;
&lt;li&gt;A "convention plugin" is  a plugin that only your team uses&lt;/li&gt;
&lt;li&gt;A "convention plugin" is a plugin that you share in your build, and so you could say every plugin is a convention plugin, but typically "convention plugins" are understood as being part of your repo&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;id("java-gradle-plugin")&lt;/code&gt; and &lt;code&gt;`java-gradle-plugin`&lt;/code&gt;&lt;br&gt;
are interchangable. Same with &lt;code&gt;maven-publish&lt;/code&gt; &lt;a href="https://github.com/gradle/gradle/blob/f2eb3a67c21a7fd0c68e649e9e9f67c070074f73/build-logic/kotlin-dsl/src/main/kotlin/gradlebuild/kotlindsl/generator/codegen/KotlinExtensionsForGradleApiFacade.kt#L34" rel="noopener noreferrer"&gt;See here&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;subprojects&lt;/code&gt; and &lt;code&gt;allprojects&lt;/code&gt; break project isolation, but if you don't care about project isolation then it should be fine to use. Read more here about why it should not: &lt;a href="https://docs.gradle.org/current/userguide/sharing_build_logic_between_subprojects.html#sec:convention_plugins_vs_cross_configuration" rel="noopener noreferrer"&gt;https://docs.gradle.org/current/userguide/sharing_build_logic_between_subprojects.html#sec:convention_plugins_vs_cross_configuration&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was like 90% done put together with help from Martin Bonnin, but I had to write it down so I don't forget it&lt;/p&gt;

&lt;h2&gt;
  
  
  Conversion
&lt;/h2&gt;

&lt;p&gt;So let's just pretend we did file &amp;gt; new project, then added two new android lib modules (lib1 and lib2). By default we'll have this duplicate code in the two lib modules. (this is default code that AS new module wizard will generate in Jan of 2026):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;plugins&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;libs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;android&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;library&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;android&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;namespace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"com.cidle.lib1"&lt;/span&gt;
  &lt;span class="nf"&gt;compileSdk&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;defaultConfig&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;minSdk&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;27&lt;/span&gt;

    &lt;span class="n"&gt;testInstrumentationRunner&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"androidx.test.runner.AndroidJUnitRunner"&lt;/span&gt;
    &lt;span class="nf"&gt;consumerProguardFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"consumer-rules.pro"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;buildTypes&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;release&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;isMinifyEnabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
      &lt;span class="nf"&gt;proguardFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getDefaultProguardFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"proguard-android-optimize.txt"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"proguard-rules.pro"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;compileOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;sourceCompatibility&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JavaVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VERSION_11&lt;/span&gt;
    &lt;span class="n"&gt;targetCompatibility&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JavaVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VERSION_11&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;libs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;androidx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ktx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;libs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;androidx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;appcompat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;libs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;material&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;testImplementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;libs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;junit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;androidTestImplementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;libs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;androidx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;junit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;androidTestImplementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;libs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;androidx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;espresso&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's de-duplicate with a convention plugin!&lt;/p&gt;

&lt;h3&gt;
  
  
  Steps
&lt;/h3&gt;

&lt;p&gt;1: Create &lt;code&gt;build-logic&lt;/code&gt; directory&lt;br&gt;
2: Add settings.gradle.kts in &lt;code&gt;build-logic&lt;/code&gt; and fill it with&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;dependencyResolutionManagement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;repositories&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;google&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;mavenCentral&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;versionCatalogs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"libs"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"../gradle/libs.versions.toml"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;rootProject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"build-logic"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3: Inside of this new &lt;code&gt;build-logic&lt;/code&gt; dir create a &lt;code&gt;build.gradle.kts&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;plugins&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"java-gradle-plugin"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;libs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kotlin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jvm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;java&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;toolchain&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;languageVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JavaLanguageVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;compileOnly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;libs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;android&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gradlePlugin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;gradleKotlinDsl&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;gradlePlugin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;plugins&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"androidLibrary"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"libtest.android.library"&lt;/span&gt;
      &lt;span class="n"&gt;implementationClass&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"AndroidLibraryConventionPlugin"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: You can’t replace “java-gradle-plugin” with a toml entry since it is a built-in plugin inside Gradle, it’s not a dependency so it doesn’t belong to the version catalog.&lt;/p&gt;

&lt;p&gt;When it comes to &lt;br&gt;
"dependencies {&lt;br&gt;
  implementation(libs.android.gradlePlugin)&lt;br&gt;
  implementation(gradleKotlinDsl())&lt;br&gt;
}"&lt;br&gt;
you needs those as well. gradleKotlinDsl() is not strictly required but it can help with things like tasks.withType() instead of tasks.withType(Jar::class.java) or extensions.configure&lt;/p&gt;

&lt;p&gt;4: Then in &lt;code&gt;convention&lt;/code&gt; dir, create new package and class structure of &lt;code&gt;src/main/kotlin/AndroidLibraryConventionPlugin.kt&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.android.build.api.dsl.LibraryExtension&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.gradle.api.JavaVersion&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.gradle.api.Plugin&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.gradle.api.Project&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.gradle.api.artifacts.VersionCatalogsExtension&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.gradle.kotlin.dsl.configure&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.gradle.kotlin.dsl.dependencies&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.gradle.kotlin.dsl.getByType&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AndroidLibraryConventionPlugin&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Plugin&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Project&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Project&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pluginManager&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.android.library"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="n"&gt;extensions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LibraryExtension&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;compileSdk&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt;

                &lt;span class="nf"&gt;defaultConfig&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;minSdk&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;27&lt;/span&gt;
                    &lt;span class="n"&gt;testInstrumentationRunner&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"androidx.test.runner.AndroidJUnitRunner"&lt;/span&gt;
                    &lt;span class="nf"&gt;consumerProguardFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"consumer-rules.pro"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;

                &lt;span class="nf"&gt;buildTypes&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nf"&gt;release&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;isMinifyEnabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
                        &lt;span class="nf"&gt;proguardFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                            &lt;span class="nf"&gt;getDefaultProguardFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"proguard-android-optimize.txt"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                            &lt;span class="s"&gt;"proguard-rules.pro"&lt;/span&gt;
                        &lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;

                &lt;span class="nf"&gt;compileOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;sourceCompatibility&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JavaVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VERSION_11&lt;/span&gt;
                    &lt;span class="n"&gt;targetCompatibility&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JavaVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VERSION_11&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;libs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;extensions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getByType&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;VersionCatalogsExtension&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="nf"&gt;named&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"libs"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="nf"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"implementation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;libs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findLibrary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"androidx-core-ktx"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"implementation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;libs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findLibrary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"androidx-appcompat"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"implementation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;libs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findLibrary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"material"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"testImplementation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;libs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findLibrary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"junit"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"androidTestImplementation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;libs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findLibrary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"androidx-junit"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"androidTestImplementation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;libs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findLibrary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"androidx-espresso-core"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: I wish there was a better way to use our toml file here. I'm not fond of libs.findLibrary, etc. It might be related to this: &lt;a href="https://github.com/gradle/gradle/issues/15383" rel="noopener noreferrer"&gt;https://github.com/gradle/gradle/issues/15383&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;5: Update &lt;code&gt;:lib1&lt;/code&gt; and &lt;code&gt;:lib2&lt;/code&gt; respective &lt;code&gt;build.gradle.kts&lt;/code&gt; to be&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;plugins&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"libtest.android.library"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;android&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;namespace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"com.cidle.lib1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;plugins&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"libtest.android.library"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;android&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;namespace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"com.cidle.lib2"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We basically went down from 37 lines to 6 lines... in 2 modules! So for every time we add a new module, we save at least those 30 lines and as our "base" android library definition expands (adds more dependencies, lint configuration, etc) then you save yourself from having to re-write those lines too.&lt;/p&gt;

&lt;p&gt;6: In your settings.gradle.kts you need to add one line to add this new &lt;code&gt;build-logic&lt;/code&gt; module as an "included build"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;pluginManagement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;includeBuild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"build-logic"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=====&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="n"&gt;you&lt;/span&gt; &lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;
  &lt;span class="nf"&gt;repositories&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;google&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;content&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;includeGroupByRegex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com\\.android.*"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;includeGroupByRegex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com\\.google.*"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;includeGroupByRegex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"androidx.*"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;mavenCentral&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;gradlePluginPortal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;7: In your toml add (under libraries)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;android-gradlePlugin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;group&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"com.android.tools.build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"gradle-api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;version.ref&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"agp"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and under plugins add&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;kotlin-jvm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"org.jetbrains.kotlin.jvm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;version.ref&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"kotlin"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Done!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thank you again Martin Bonnin for all of the teaching on this subject!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;TL;DR: I think the biggest concept to overcome when trying to setup a convention plugin is that you need &lt;em&gt;another&lt;/em&gt; gradle build in your repository. Example: if you had a file &amp;gt; new android project, and then created two android libs (lib1 and lib2) and wanted to have common dependencies/configuration between those two android library modules, you would need to create (what looks like) a brand new &lt;code&gt;build-logic&lt;/code&gt; module, but this "module" isn't just "another" module you might be used to in your android project — instead it has to be a new little gradle "project" (the gradle term for this is a &lt;strong&gt;new&lt;/strong&gt; gradle &lt;em&gt;build&lt;/em&gt;), which is why you need to write another settings.gradle.kts in &lt;code&gt;build-logic&lt;/code&gt; dir, etc. In order for your actual android project to use this new &lt;code&gt;build-logic&lt;/code&gt; gradle build, you need to reference it via &lt;code&gt;includeBuild()&lt;/code&gt; Get it? Instead of your usual one gradle build in your repo, you will have two, hence the need to call &lt;code&gt;includeBuild()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Please vote for clearer documentation from the Gradle team: &lt;a href="https://github.com/gradle/gradle/issues/36495" rel="noopener noreferrer"&gt;https://github.com/gradle/gradle/issues/36495&lt;/a&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>mobile</category>
      <category>tooling</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to install tailscale on your Unifi router (UDM)</title>
      <dc:creator>ColtonIdle</dc:creator>
      <pubDate>Thu, 09 Oct 2025 19:47:00 +0000</pubDate>
      <link>https://dev.to/coltonidle/how-to-install-tailscale-on-your-unifi-router-udm-5a35</link>
      <guid>https://dev.to/coltonidle/how-to-install-tailscale-on-your-unifi-router-udm-5a35</guid>
      <description>&lt;p&gt;I've become a bit enamored with tailscale. All I wanted to do is replace the "teleport" connection I use from my mobile device to my at home network. This led me down a bit of a rabbit hole. I wanted to install on my UDM Pro Max since it seems like the right "device" on my network that should handle it &lt;strong&gt;and&lt;/strong&gt; I noticed that glinet routers have tailscale support built in. Super cool!&lt;/p&gt;

&lt;p&gt;For now upvote for native tailscale support on unifi hardware and I'll walk you through how to install. &lt;a href="https://community.ui.com/questions/Feature-Request-Support-Tailscale-under-VPN-options/d9ecb8cc-9f25-41bf-b19d-85615c27a857" rel="noopener noreferrer"&gt;https://community.ui.com/questions/Feature-Request-Support-Tailscale-under-VPN-options/d9ecb8cc-9f25-41bf-b19d-85615c27a857&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For me... there are 3 things things I wanted to make sure this tailscale setup would support... mimicking what unifis teleport does for me&lt;/p&gt;

&lt;h2&gt;
  
  
  My 3 goals:
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;access my photo and media server by hitting their IPs directly (192.168.1.116 and 192.168.1.122)&lt;/li&gt;
&lt;li&gt;access my servers via custom dns entries I created on my router (ie. my.media/ and my.photos/ &lt;/li&gt;
&lt;li&gt;I can log into my home tv service app and it thinks I'm at home so I can watch my content&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Install steps
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1.&lt;/strong&gt; Enable ssh on unifi. Go to "Control Plane" &amp;gt; Console &amp;gt; SSH and enable it&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.&lt;/strong&gt; SSH into your UDM &lt;code&gt;ssh root@[YOUR UDM IP]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3.&lt;/strong&gt; Install tailscale via &lt;a href="https://github.com/SierraSoftworks/tailscale-udm" rel="noopener noreferrer"&gt;https://github.com/SierraSoftworks/tailscale-udm&lt;/a&gt; so use &lt;code&gt;curl -sSLq https://raw.github.com/SierraSoftworks/tailscale-udm/main/install.sh | sh&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4.&lt;/strong&gt; Run &lt;code&gt;tailscale up&lt;/code&gt;. You might hit an error. If so it's because you have to use a workaround for newer unifi network updates&lt;/p&gt;

&lt;p&gt;If you hit an error you can do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vim /etc/apt/sources.list

then identify the line for bullseye-backports and update it to

deb https://archive.debian.org/debian/ bullseye-backports main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then save.&lt;/p&gt;

&lt;p&gt;See: &lt;a href="https://github.com/SierraSoftworks/tailscale-udm/issues/116" rel="noopener noreferrer"&gt;https://github.com/SierraSoftworks/tailscale-udm/issues/116&lt;/a&gt; for more info&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5.&lt;/strong&gt; Then to patch DNS not working you must do&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;touch /run/dnsmasq.dhcp.conf.d/tailscale0.conf
vim /run/dnsmasq.dhcp.conf.d/tailscale0.conf

Add this line to the file
interface=tailscale0
and save
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After saving the file, run &lt;code&gt;pkill dnsmasq&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If dns still doesn't work, you might have to kill it via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;killall dnsmasq
then
pgrep dnsmasq
to confirm it was back up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See: &lt;a href="https://github.com/SierraSoftworks/tailscale-udm/issues/122" rel="noopener noreferrer"&gt;https://github.com/SierraSoftworks/tailscale-udm/issues/122&lt;/a&gt; for more info&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6.&lt;/strong&gt; &lt;strong&gt;Done&lt;/strong&gt; (mostly) with setup&lt;/p&gt;

&lt;h2&gt;
  
  
  Going back to my 3 things I outlined
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1.&lt;/strong&gt; access my photo and media server by hitting their IPs directly (192.168.1.116 and 192.168.1.122)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enable subnet routing... and then this works!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.&lt;/strong&gt; access my servers via custom dns entries I created on my router (ie. my.media/ and my.photos/&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Go to tailscale admin console &amp;gt; DNS &amp;gt; Override global dns then &amp;gt; Insert IP of router. And it works!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3.&lt;/strong&gt; I can log into my home tv service app and it thinks I'm at home so I can watch my content&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enable exit node on the UDM, then use UDM as an exit node when you need this&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask Unifi to support tailscale as a first class citizen!
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://community.ui.com/questions/Feature-Request-Support-Tailscale-under-VPN-options/d9ecb8cc-9f25-41bf-b19d-85615c27a857" rel="noopener noreferrer"&gt;https://community.ui.com/questions/Feature-Request-Support-Tailscale-under-VPN-options/d9ecb8cc-9f25-41bf-b19d-85615c27a857&lt;/a&gt;&lt;/p&gt;

</description>
      <category>networking</category>
      <category>security</category>
      <category>tutorial</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Android Dev and zscaler</title>
      <dc:creator>ColtonIdle</dc:creator>
      <pubDate>Fri, 19 Sep 2025 17:50:00 +0000</pubDate>
      <link>https://dev.to/coltonidle/android-dev-and-zscaler-4d2g</link>
      <guid>https://dev.to/coltonidle/android-dev-and-zscaler-4d2g</guid>
      <description>&lt;p&gt;Are you installing cacerts into every jdk?&lt;/p&gt;

&lt;p&gt;How about just adding onto your global gradle.properties (i.e. ~/.gradle/gradle.properties)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;For Gradle:
# local zScaler proxy host and port
systemProp.https.proxyHost=127.0.0.1
systemProp.https.proxyPort=9000
# to support zScaler TLS inspection, use the Windows cert storage with the zScaler cert
systemProp.javax.net.ssl.trustStoreType=Windows-ROOT
# macOS
systemProp.javax.net.ssl.trustStoreType=KeychainStore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On my mac, all I needed was &lt;code&gt;systemProp.javax.net.ssl.trustStoreType=KeychainStore&lt;/code&gt; but I'm including everything else above just in case. &lt;/p&gt;

&lt;p&gt;You can also set this on java instead of gradle. This could be helpful if running tools like gradle-profiler which don't automatically detect your gradle user home.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;export JAVA_TOOL_OPTIONS="-Djavax.net.ssl.trustStoreType=KeychainStore"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Last resort:&lt;br&gt;
&lt;code&gt;~/.gradle/init.d/certs.init.gradle.kts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;add this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import java.security.KeyStore
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate

val trustStoreProp = "javax.net.ssl.trustStore"
val privateRootFile = file("private.pem")

beforeSettings {
    if (System.getProperty(trustStoreProp)?.endsWith("+private") == true) return@beforeSettings
    val defaultTrustStore = System.getProperty(trustStoreProp)?.let { File(it) }
        ?: File(System.getProperty("java.home"), "lib/security/cacerts")
    val alternateTrustStore = File(rootDir, "build/tmp/cacerts+private")
    try {
        val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
        val trustStorePassword = System.getProperty("${trustStoreProp}Password")?.toCharArray()
        if (defaultTrustStore.exists()) defaultTrustStore.inputStream().use { keyStore.load(it, trustStorePassword) }
        val privateRoot = privateRootFile.inputStream()
            .use(CertificateFactory.getInstance("X.509")::generateCertificate) as X509Certificate
        keyStore.setCertificateEntry(privateRoot.subjectX500Principal.name, privateRoot)
        alternateTrustStore.parentFile.mkdirs()
        alternateTrustStore.outputStream().use {
            keyStore.store(it, trustStorePassword ?: "changeit".toCharArray())
        }
    } catch (e: Exception) {
        if (alternateTrustStore.exists()) alternateTrustStore.delete()
        throw e
    }
    System.setProperty(trustStoreProp, alternateTrustStore.absolutePath)
    logger.info("$trustStoreProp set to $alternateTrustStore")
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>android</category>
      <category>java</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Compose for Desktop: App size comparison</title>
      <dc:creator>ColtonIdle</dc:creator>
      <pubDate>Tue, 25 Mar 2025 04:07:31 +0000</pubDate>
      <link>https://dev.to/coltonidle/compose-for-desktop-app-size-comparison-3d8h</link>
      <guid>https://dev.to/coltonidle/compose-for-desktop-app-size-comparison-3d8h</guid>
      <description>&lt;p&gt;While trying to figure out how to use Compose for Desktop with proguard (and conveyor) I got some interesting stats:&lt;/p&gt;

&lt;p&gt;I used an empty template app from &lt;a href="https://kmp.jetbrains.com" rel="noopener noreferrer"&gt;https://kmp.jetbrains.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'll be using an apple silicon mac as my benchmark for app sizes.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;./gradlew packageDmg&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;64.9MB&lt;/strong&gt; for .dmg&lt;br&gt;
&lt;strong&gt;124.1MB&lt;/strong&gt; for .app (what you get when you open .dmg)&lt;/p&gt;

&lt;p&gt;This doesn't use conveyor, but we can use it as a baseline.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;./gradlew packageReleaseDmg&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;51.9MB&lt;/strong&gt; for .dmg&lt;br&gt;
&lt;strong&gt;109.9MB&lt;/strong&gt; for .app (what you get when you open .dmg)&lt;/p&gt;

&lt;p&gt;This is all we need to run proguard apparently as any &lt;code&gt;release&lt;/code&gt; task will have proguard enabled. See: &lt;a href="https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-native-distribution.html#minification-and-obfuscation" rel="noopener noreferrer"&gt;https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-native-distribution.html#minification-and-obfuscation&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;./gradlew packageReleaseDmg&lt;/code&gt; but with &lt;code&gt;obfuscate.set(true)&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;48MB&lt;/strong&gt; for .dmg&lt;br&gt;
&lt;strong&gt;105.8MB&lt;/strong&gt; for .app (what you get when you open .dmg)&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;./gradlew desktopJar &amp;amp;&amp;amp; conveyor make site&lt;/code&gt; A regular unproguarded app with conveyor&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;71.5MB&lt;/strong&gt; example-1.0.0-mac-aarch64.zip&lt;br&gt;
&lt;strong&gt;120.9MB&lt;/strong&gt; Example.app&lt;/p&gt;

&lt;p&gt;At this point I have a minimal conveyor.conf&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;include "#!./gradlew -q printConveyorConfig"

app {
  display-name = "SampleProject"
  site.base-url = "localhost:3000"
}
conveyor.compatibility-level = 17
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;./gradlew desktopJar &amp;amp;&amp;amp; conveyor make site&lt;/code&gt; A regular unproguarded app with conveyor but enables "native library extraction" (I still don't really know what it is, but it seems like it's the recommended way to distribute apps with conveyor, even though it's off by default) See: &lt;a href="https://github.com/hydraulic-software/compose-multiplatform-starter/blob/master/conveyor.conf#L5-L10" rel="noopener noreferrer"&gt;https://github.com/hydraulic-software/compose-multiplatform-starter/blob/master/conveyor.conf#L5-L10&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;62.3MB&lt;/strong&gt; example-1.0.0-mac-aarch64.zip&lt;br&gt;
&lt;strong&gt;123.8MB&lt;/strong&gt; Example.app&lt;/p&gt;

&lt;p&gt;My conveyor.conf&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;include "#!./gradlew -q printConveyorConfig"

include required("https://raw.githubusercontent.com/hydraulic-software/conveyor/master/configs/jvm/extract-native-libraries.conf")

app {
  display-name = "SampleProject"
  site.base-url = "localhost:3000"
}
conveyor.compatibility-level = 17
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Surprisingly small zip!&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;./gradlew proguardReleaseJars &amp;amp;&amp;amp; conveyor make site&lt;/code&gt; Proguarded app with conveyor&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;58.6MB&lt;/strong&gt; example-1.0.0-mac-aarch64.zip&lt;br&gt;
&lt;strong&gt;106.3MB&lt;/strong&gt; Example.app&lt;/p&gt;

&lt;p&gt;My conveyor.conf gets a little tricky. You can figure out the paths at the bottom by running &lt;code&gt;./gradlew printConveyorConfig&lt;/code&gt; See: &lt;a href="https://conveyor.hydraulic.dev/17.0/configs/jvm/#proguard-obfuscation" rel="noopener noreferrer"&gt;https://conveyor.hydraulic.dev/17.0/configs/jvm/#proguard-obfuscation&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;include "#!./gradlew -q printConveyorConfig"
gradle-cache = ${env.HOME}/.gradle    # Note: UNIX specific config!

app {
  display-name = "SampleProject"
  site.base-url = "localhost:3000"

  # Import all the obfuscated JARs, except the JAR that contains the platform native graphics code.
  inputs = [{
        from = composeApp/build/compose/tmp/main-release/proguard
        remap = [
            "**"
            "-skiko-awt-runtime-*.jar"
        ]
      }]

      # Put the dropped JAR back with the right version for each platform.
      linux.amd64.inputs = ${app.inputs} [ ${gradle-cache}/caches/modules-2/files-2.1/org.jetbrains.skiko/skiko-awt-runtime-linux-x64/0.9.3/e6fa1645020a9ed8ca2c1058f541dfeff8a9df6/skiko-awt-runtime-linux-x64-0.9.3.jar ]
      mac.aarch64.inputs = ${app.inputs} [ ${gradle-cache}/caches/modules-2/files-2.1/org.jetbrains.skiko/skiko-awt-runtime-macos-arm64/0.9.3/9fb269f5829942f307e14d0f9bafdd30c886cabc/skiko-awt-runtime-macos-arm64-0.9.3.jar ]
      mac.amd64.inputs = ${app.inputs} [ ${gradle-cache}/caches/modules-2/files-2.1/org.jetbrains.skiko/skiko-awt-runtime-macos-x64/0.9.3/33b53df6426efa34461268b1005cb8c9ceaffce1/skiko-awt-runtime-macos-x64-0.9.3.jar ]
      windows.amd64.inputs = ${app.inputs} [ ${gradle-cache}/caches/modules-2/files-2.1/org.jetbrains.skiko/skiko-awt-runtime-windows-x64/0.9.3/89198469fcb0543ccd8e09a0cfbe0334d4a1f6dd/skiko-awt-runtime-windows-x64-0.9.3.jar ]
}
conveyor.compatibility-level = 17
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;./gradlew proguardReleaseJars &amp;amp;&amp;amp; conveyor make site&lt;/code&gt; Proguarded app with conveyor with "native library extraction"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;49.4MB&lt;/strong&gt; example-1.0.0-mac-aarch64.zip&lt;br&gt;
&lt;strong&gt;109.2MB&lt;/strong&gt; Example.app&lt;/p&gt;

&lt;p&gt;Same conveyor.conf as above, but adds&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;include required("https://raw.githubusercontent.com/hydraulic-software/conveyor/master/configs/jvm/extract-native-libraries.conf")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;.zip format seems to really like when you enable native library extraction&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;./gradlew proguardReleaseJars &amp;amp;&amp;amp; conveyor make site&lt;/code&gt; Proguarded app with conveyor with &lt;code&gt;obfuscate.set(true)&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;54.6MB&lt;/strong&gt; example-1.0.0-mac-aarch64.zip&lt;br&gt;
&lt;strong&gt;102.3MB&lt;/strong&gt; Example.app&lt;/p&gt;

&lt;p&gt;Same conveyor.conf as above, but now we remove&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;include required("https://raw.githubusercontent.com/hydraulic-software/conveyor/master/configs/jvm/extract-native-libraries.conf")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;./gradlew proguardReleaseJars &amp;amp;&amp;amp; conveyor make site&lt;/code&gt; Proguarded app with conveyor with "native library extraction" with &lt;code&gt;obfuscate.set(true)&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;45.5MB&lt;/strong&gt; example-1.0.0-mac-aarch64.zip&lt;br&gt;
&lt;strong&gt;105.2MB&lt;/strong&gt; Example.app&lt;/p&gt;

&lt;p&gt;Same conveyor.conf as above, but adds&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;include required("https://raw.githubusercontent.com/hydraulic-software/conveyor/master/configs/jvm/extract-native-libraries.conf")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Short n' Sweet
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;./gradlew packageDmg&lt;/code&gt; No conveyor.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;64.9MB&lt;/strong&gt; for .dmg&lt;br&gt;
&lt;strong&gt;124.1MB&lt;/strong&gt; for .app (what you get when you open .dmg)&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;./gradlew packageReleaseDmg&lt;/code&gt; No conveyor, but runs proguard.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;51.9MB&lt;/strong&gt; for .dmg&lt;br&gt;
&lt;strong&gt;109.9MB&lt;/strong&gt; for .app (what you get when you open .dmg)&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;./gradlew packageReleaseDmg&lt;/code&gt; No conveyor, but runs proguard with &lt;code&gt;obfuscate.set(true)&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;48MB&lt;/strong&gt; for .dmg&lt;br&gt;
&lt;strong&gt;105.8MB&lt;/strong&gt; for .app (what you get when you open .dmg)&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;./gradlew desktopJar &amp;amp;&amp;amp; conveyor make site&lt;/code&gt; A regular unproguarded app with conveyor&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;71.5MB&lt;/strong&gt; example-1.0.0-mac-aarch64.zip&lt;br&gt;
&lt;strong&gt;120.9MB&lt;/strong&gt; Example.app&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;./gradlew desktopJar &amp;amp;&amp;amp; conveyor make site&lt;/code&gt; A regular unproguarded app with conveyor + conveyor's "native library extraction"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;62.3MB&lt;/strong&gt; example-1.0.0-mac-aarch64.zip&lt;br&gt;
&lt;strong&gt;123.8MB&lt;/strong&gt; Example.app&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;./gradlew proguardReleaseJars &amp;amp;&amp;amp; conveyor make site&lt;/code&gt; Proguarded app with conveyor&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;58.6MB&lt;/strong&gt; example-1.0.0-mac-aarch64.zip&lt;br&gt;
&lt;strong&gt;106.3MB&lt;/strong&gt; Example.app&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;./gradlew proguardReleaseJars &amp;amp;&amp;amp; conveyor make site&lt;/code&gt; Proguarded app with conveyor with conveyor's "native library extraction"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;49.4MB&lt;/strong&gt; example-1.0.0-mac-aarch64.zip&lt;br&gt;
&lt;strong&gt;109.2MB&lt;/strong&gt; Example.app&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;./gradlew proguardReleaseJars &amp;amp;&amp;amp; conveyor make site&lt;/code&gt; Proguarded and obfuscated app with conveyor&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;54.6MB&lt;/strong&gt; example-1.0.0-mac-aarch64.zip&lt;br&gt;
&lt;strong&gt;102.3MB&lt;/strong&gt; Example.app&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;./gradlew proguardReleaseJars &amp;amp;&amp;amp; conveyor make site&lt;/code&gt; Proguarded and obfuscated app with conveyor with conveyor's "native library extraction"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;45.5MB&lt;/strong&gt; example-1.0.0-mac-aarch64.zip&lt;br&gt;
&lt;strong&gt;105.2MB&lt;/strong&gt; Example.app&lt;/p&gt;

&lt;h2&gt;
  
  
  The winner!
&lt;/h2&gt;

&lt;p&gt;I care about initial download size. An app built with conveyor + conveyor's "native library extraction" + proguard + obfuscation (arguably the combo that everyone should aim for) comes in at the smallest download size of 45.5MB. And that contains extra code in there for sparkle (the update framework) that you don't get with the typical &lt;code&gt;gradlew package*&lt;/code&gt; tasks (and its also supposed to help with small delta updates). Way cool!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to profile your Android app's build time</title>
      <dc:creator>ColtonIdle</dc:creator>
      <pubDate>Fri, 14 Mar 2025 17:39:06 +0000</pubDate>
      <link>https://dev.to/coltonidle/how-to-profile-your-android-apps-build-time-1k28</link>
      <guid>https://dev.to/coltonidle/how-to-profile-your-android-apps-build-time-1k28</guid>
      <description>&lt;p&gt;If you've ever wondered "Will my project build faster with 8 gigs of ram vs 4" then this article is for you.&lt;/p&gt;

&lt;p&gt;Today we'll be talking about the standalone gradle-profiler tool. There's a nice little intro into using it here: &lt;a href="https://developer.android.com/studio/build/profile-your-build" rel="noopener noreferrer"&gt;https://developer.android.com/studio/build/profile-your-build&lt;/a&gt; but a lot of the stuff there still went over my head. I am but a measly android developer, who still feels like I need a PhD to understand gradle.&lt;/p&gt;

&lt;p&gt;I've got two typical scenarios I want to test that I run into every day.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Builds after a clean&lt;/li&gt;
&lt;li&gt;Incremental builds&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Builds after a clean seem to take my project around 3 minutes on average. This is completely unmeasured, but mostly this is what I see in the bottom right hand corner in Android Studio "You build finished in 3 minutes and 02 seconds". Going through a clean is pretty popular for me because sometimes things just don't work in the IDE (I think a lot of it is/might be due to kapt), but regardless I have to clean/invalidate caches/restart/hope that my build is fixed. And then re-run. This is the scenario I want to test first.&lt;/p&gt;

&lt;h1&gt;
  
  
  Install gradle-profiler
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/gradle/gradle-profiler
    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;gradle-profiler
    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./gradlew installDist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edit: Now available on brew&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;gradle-profiler
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  CD into the gradle-profiler
&lt;/h1&gt;

&lt;p&gt;CD into the profiler &lt;code&gt;build/install/gradle-profiler/bin&lt;/code&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Create a scenarios.txt
&lt;/h1&gt;

&lt;p&gt;A scenarios.txt lists all of the scenarios that the gradle-profiler will run through. I found that the one that is in the Android Studio docs wasn't that helpful to me in testing my first scenario. I did get a ton of help from &lt;a href="https://github.com/eskatos" rel="noopener noreferrer"&gt;Paul Merlin&lt;/a&gt; who works on the Gradle team.&lt;/p&gt;

&lt;p&gt;This is mine&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;clean_build_1gb &lt;span class="o"&gt;{&lt;/span&gt;
    tasks &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;":app:assembleFreeDebug"&lt;/span&gt;, &lt;span class="s2"&gt;"--rerun-tasks"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    jvm-args &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-Xmx1024m"&lt;/span&gt;, &lt;span class="s2"&gt;"-Dkotlin.daemon.jvm.options=-Xmx1024m"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

clean_build_2gb &lt;span class="o"&gt;{&lt;/span&gt;
    tasks &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;":app:assembleFreeDebug"&lt;/span&gt;, &lt;span class="s2"&gt;"--rerun-tasks"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    jvm-args &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-Xmx2048m"&lt;/span&gt;, &lt;span class="s2"&gt;"-Dkotlin.daemon.jvm.options=-Xmx2048m"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

clean_build_4gb &lt;span class="o"&gt;{&lt;/span&gt;
    tasks &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;":app:assembleFreeDebug"&lt;/span&gt;, &lt;span class="s2"&gt;"--rerun-tasks"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    jvm-args &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-Xmx4096m"&lt;/span&gt;, &lt;span class="s2"&gt;"-Dkotlin.daemon.jvm.options=-Xmx4096m"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

clean_build_8gb &lt;span class="o"&gt;{&lt;/span&gt;
    tasks &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;":app:assembleFreeDebug"&lt;/span&gt;, &lt;span class="s2"&gt;"--rerun-tasks"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    jvm-args &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-Xmx8192m"&lt;/span&gt;, &lt;span class="s2"&gt;"-Dkotlin.daemon.jvm.options=-Xmx8192m"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

clean_build_16gb &lt;span class="o"&gt;{&lt;/span&gt;
    tasks &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;":app:assembleFreeDebug"&lt;/span&gt;, &lt;span class="s2"&gt;"--rerun-tasks"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    jvm-args &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-Xmx16384m"&lt;/span&gt;, &lt;span class="s2"&gt;"-Dkotlin.daemon.jvm.options=-Xmx16384m"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

clean_build_32gb &lt;span class="o"&gt;{&lt;/span&gt;
    tasks &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;":app:assembleFreeDebug"&lt;/span&gt;, &lt;span class="s2"&gt;"--rerun-tasks"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    jvm-args &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-Xmx32768m"&lt;/span&gt;, &lt;span class="s2"&gt;"-Dkotlin.daemon.jvm.options=-Xmx32768m"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, I test from 1 to 32GB (my machine has a lot of ram), and I set the JVM args and kotlin jvm args to that same amount. I suppose that it would be good if I could tweak both of them to variable and just run this overnight and somehow have it run in a bunch of combinations to see what works best, but I haven't figure out how to do that yet.&lt;/p&gt;

&lt;p&gt;You can note that I also added "--rerun-tasks" argument because according to (Paul Merlin)[&lt;a href="https://github.com/eskatos" rel="noopener noreferrer"&gt;https://github.com/eskatos&lt;/a&gt;] this means "If you want to benchmark a "rebuild everything" scenario you can ... use --rerun-tasks."&lt;/p&gt;

&lt;h1&gt;
  
  
  Run the profiler
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./gradle-profiler --benchmark --project-dir ~/dev/rollertoaster --scenario-file ~/dev/rollertoaster/scenarios.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs the profiler, pointed at my project file, and points it at the scenarios file in my project. (I'm saving the scenarios there so that other people on my team can easily benchmark and update their local gradle.properties file. The entire thing that has caused me to really try the profiler was the fact that my CI has wildly different ram, vs my desktop, vs my laptop, and all of us adhering to a single gradle.properties felt stupid)&lt;/p&gt;

&lt;h1&gt;
  
  
  After
&lt;/h1&gt;

&lt;p&gt;Running the profiler could take some time as it runs a few iterations, but it will eventually give you a directory to open. In this directory you'll have an html document and you can pretty much look at the "Mean" column and choose the fastest one for you. Here I'm showing the .CSV output and you can see that my 1GB and 2GB builds failed completely, but to my surprise there wasn't much difference in adding RAM after 4GB.&lt;/p&gt;

&lt;p&gt;Then try no-op scenario.&lt;/p&gt;

&lt;p&gt;From Tony Robalik&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It is named noop (for "no-op"), which runs the :app:assembleDebug task. I call it "no-op" because there are no file changes in this scenario, so every assemble after the first should be a "no-op." This scenario tests whether your primary daily task is well-configured: the task and all its dependencies should all be UP-TO-DATE; this build should be very fast.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;noop {
  tasks = [":app:assembleDebug"]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Next up, incremental changes!
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;IN PROGRESS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqdfom9kuaua0wytt1wep.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqdfom9kuaua0wytt1wep.png" alt="Screen Shot 2021-04-27 at 3.08.16 AM" width="800" height="505"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks to &lt;a href="https://adambennett.dev/2020/08/disabling-jetifier/" rel="noopener noreferrer"&gt;Adam Bennet's article&lt;/a&gt; that introduced me to gradle-profiler and how simple it was to get started.&lt;/p&gt;

&lt;p&gt;Also see: &lt;a href="https://dev.to/autonomousapps/benchmarking-builds-with-gradle-profiler-oa8"&gt;https://dev.to/autonomousapps/benchmarking-builds-with-gradle-profiler-oa8&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I use this:&lt;br&gt;
&lt;a href="https://gist.github.com/ColtonIdle/6122faf5aa99740fe8e61487e9a60f27" rel="noopener noreferrer"&gt;https://gist.github.com/ColtonIdle/6122faf5aa99740fe8e61487e9a60f27&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to sign and notarize a macOS app with Conveyor</title>
      <dc:creator>ColtonIdle</dc:creator>
      <pubDate>Thu, 13 Mar 2025 15:32:19 +0000</pubDate>
      <link>https://dev.to/coltonidle/how-to-sign-and-notarize-a-macos-app-with-conveyor-1310</link>
      <guid>https://dev.to/coltonidle/how-to-sign-and-notarize-a-macos-app-with-conveyor-1310</guid>
      <description>&lt;p&gt;&lt;strong&gt;Pre-reqs&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On a mac (possibly not needed, but I didn't test on windows/linux)&lt;/li&gt;
&lt;li&gt;Want a notarized mac app (self-distribution, not on mac app store, although it may still work for mac app store, not sure) &lt;/li&gt;
&lt;li&gt;Need an apple dev account ($100 a year)&lt;/li&gt;
&lt;li&gt;Integrate conveyor via these instructions &lt;a href="https://conveyor.hydraulic.dev/17.100.1/tutorial/tortoise/2-gradle/" rel="noopener noreferrer"&gt;https://conveyor.hydraulic.dev/17.100.1/tutorial/tortoise/2-gradle/&lt;/a&gt; and this sample &lt;a href="https://github.com/hydraulic-software/compose-multiplatform-starter" rel="noopener noreferrer"&gt;https://github.com/hydraulic-software/compose-multiplatform-starter&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;code&gt;~/Library/Preferences/Hydraulic/Conveyor/&lt;/code&gt; Inside of that folder there will be &lt;code&gt;apple.csr&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Source: &lt;a href="https://conveyor.hydraulic.dev/17.0/running/#initial-setup-and-default-config" rel="noopener noreferrer"&gt;https://conveyor.hydraulic.dev/17.0/running/#initial-setup-and-default-config&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://developer.apple.com/account/resources/certificates/add" rel="noopener noreferrer"&gt;https://developer.apple.com/account/resources/certificates/add&lt;/a&gt; and click "Developer ID Application". The page will now show you two different certs. It seems like an "older" cert format is auto-selected, but the new one works and has a longer expiration.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvcpivd80r48vstpxnfpf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvcpivd80r48vstpxnfpf.png" alt="Image description" width="800" height="583"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Apple will give you a &lt;code&gt;.cer&lt;/code&gt;. Place it somewhere (I just put it in my Conveyor Prefs directory)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Now you need an api key to use apple's notarization service. Go to &lt;a href="https://appstoreconnect.apple.com/access/integrations/api" rel="noopener noreferrer"&gt;https://appstoreconnect.apple.com/access/integrations/api&lt;/a&gt; and create it with the "Developer" role.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Source: &lt;a href="https://conveyor.hydraulic.dev/17.0/configs/keys-and-certificates/#configure-apple-notarization" rel="noopener noreferrer"&gt;https://conveyor.hydraulic.dev/17.0/configs/keys-and-certificates/#configure-apple-notarization&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your final &lt;code&gt;defaults.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Your private root key, from which all other generated keys are deterministically derived.
app.signing-key = "keyring"

# Credentials needed for the macOS app approval process.
 app.mac.notarization {
   issuer-id = abc-123-123-123-ABC123
   key-id = ABC123123
   private-key = /Users/cidle/Library/Preferences/Hydraulic/Conveyor/AuthKey_ABC123.p8
 }

app.mac.certificate = /Users/cidle/Library/Preferences/Hydraulic/Conveyor/cert_from_apple.cer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: Even though the docs make it seem like you might need &lt;code&gt;app.mac.signing-key&lt;/code&gt;, you do not.&lt;/p&gt;

&lt;p&gt;Note 2: &lt;code&gt;app.mac.certificate&lt;/code&gt; can be a relative path (to the defaults.conf) it doesn't have to be absolute&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to setup compose desktop hot reload</title>
      <dc:creator>ColtonIdle</dc:creator>
      <pubDate>Tue, 11 Mar 2025 04:04:40 +0000</pubDate>
      <link>https://dev.to/coltonidle/how-to-setup-compose-desktop-hot-reload-232b</link>
      <guid>https://dev.to/coltonidle/how-to-setup-compose-desktop-hot-reload-232b</guid>
      <description>&lt;p&gt;Steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use Kotlin 2.1.20-RC (or any higher if released)&lt;/li&gt;
&lt;li&gt;Apply the plugin&lt;/li&gt;
&lt;li&gt;Wrap the content of a window with DevelopmentEntryPoint {}&lt;/li&gt;
&lt;li&gt;Use OptimizeNonSkippingGroups (optional, but a better experience for me).&lt;/li&gt;
&lt;li&gt;Then in a KMP project and IntelliJ, just using main function run gutter to run the APP.&lt;/li&gt;
&lt;li&gt;If you're using Kotlin JVM or Android Studio, then configure the jvmRun task or provide&lt;/li&gt;
&lt;li&gt;./gradlew jvmRun -DmainClass=path.toMain.MainKt&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Basically, running compose apps is the easiest if using KMP + IntelliJ.&lt;/p&gt;

&lt;p&gt;Note: Must use JVM that is shipped with Intellj. Navigate to Settings &amp;gt; Gradle &amp;gt; JDK and make sure IntelliJ JDK is selected&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Compose for Desktop: Window party tricks</title>
      <dc:creator>ColtonIdle</dc:creator>
      <pubDate>Mon, 03 Mar 2025 17:40:33 +0000</pubDate>
      <link>https://dev.to/coltonidle/compose-for-desktop-window-tricks-55mf</link>
      <guid>https://dev.to/coltonidle/compose-for-desktop-window-tricks-55mf</guid>
      <description>&lt;p&gt;Just a random collection of window tricks I've accumulated while trying to build a desktop app.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Size the window to your content
&lt;/h2&gt;

&lt;p&gt;Sometimes you just want your window to match the size of your content. I've found this to be true for settings windows for example. Simply use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;state&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rememberWindowState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DpSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Dp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Unspecified&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Dp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Unspecified&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure your root layouts don't have fillMaxSize() or else this won't work. You can optionally also make the Window non-resizable via &lt;code&gt;resizable = false&lt;/code&gt; on the Window.&lt;/p&gt;

&lt;p&gt;If you need the window size to adjust to your content size, checkout &lt;a href="https://gist.github.com/ColtonIdle/df23e3dc8e72569a28a4c64197bed14c" rel="noopener noreferrer"&gt;https://gist.github.com/ColtonIdle/df23e3dc8e72569a28a4c64197bed14c&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;More info: &lt;a href="https://kotlinlang.slack.com/archives/C01D6HTPATV/p1740892851067859" rel="noopener noreferrer"&gt;https://kotlinlang.slack.com/archives/C01D6HTPATV/p1740892851067859&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Start with the window centered
&lt;/h2&gt;

&lt;p&gt;Use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;Window&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rememberWindowState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;WindowPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Alignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Center&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Barely) More info: &lt;a href="https://kotlinlang.slack.com/archives/C01D6HTPATV/p1739326963488559" rel="noopener noreferrer"&gt;https://kotlinlang.slack.com/archives/C01D6HTPATV/p1739326963488559&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Remove title bar
&lt;/h2&gt;

&lt;p&gt;Use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;application&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Window&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;onCloseRequest&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;exitApplication&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxSize&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;background&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Red&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nc"&gt;LaunchedEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rootPane&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rootPane&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;putClientProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"apple.awt.transparentTitleBar"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;putClientProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"apple.awt.fullWindowContent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More info:&lt;br&gt;
&lt;a href="https://kotlinlang.slack.com/archives/C01D6HTPATV/p1739449034315049" rel="noopener noreferrer"&gt;https://kotlinlang.slack.com/archives/C01D6HTPATV/p1739449034315049&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Bring window to front and into focus
&lt;/h2&gt;

&lt;p&gt;If you need to bring it to the foreground and focussed&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;Desktop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDesktop&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;requestForeground&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More info:&lt;br&gt;
&lt;a href="https://github.com/JetBrains/compose-multiplatform/issues/4231#issuecomment-1952205605" rel="noopener noreferrer"&gt;https://github.com/JetBrains/compose-multiplatform/issues/4231#issuecomment-1952205605&lt;/a&gt;&lt;br&gt;
and&lt;br&gt;
&lt;a href="https://kotlinlang.slack.com/archives/C01D6HTPATV/p1739152698052949" rel="noopener noreferrer"&gt;https://kotlinlang.slack.com/archives/C01D6HTPATV/p1739152698052949&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Customize UI stuff on Java/macOS: &lt;a href="https://developer.apple.com/library/archive/technotes/tn2007/tn2196.html" rel="noopener noreferrer"&gt;https://developer.apple.com/library/archive/technotes/tn2007/tn2196.html&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Source: &lt;a href="https://kotlinlang.slack.com/archives/C01D6HTPATV/p1741607313291729" rel="noopener noreferrer"&gt;https://kotlinlang.slack.com/archives/C01D6HTPATV/p1741607313291729&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Custom title bar
&lt;/h2&gt;

&lt;p&gt;Apparently some stuff changed in later JDK versions?&lt;/p&gt;

&lt;p&gt;See: &lt;a href="https://gist.github.com/adrientetar/b039998a0752261d2ee7db9ade3e7c15" rel="noopener noreferrer"&gt;https://gist.github.com/adrientetar/b039998a0752261d2ee7db9ade3e7c15&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to use Hydraulic Conveyor with KMP Compose for Desktop</title>
      <dc:creator>ColtonIdle</dc:creator>
      <pubDate>Tue, 25 Feb 2025 19:57:54 +0000</pubDate>
      <link>https://dev.to/coltonidle/how-to-use-hydraulic-conveyor-with-kmp-compose-for-desktop-id4</link>
      <guid>https://dev.to/coltonidle/how-to-use-hydraulic-conveyor-with-kmp-compose-for-desktop-id4</guid>
      <description>&lt;ol&gt;
&lt;li&gt;Download a new project from &lt;a href="https://kmp.jetbrains.com/" rel="noopener noreferrer"&gt;https://kmp.jetbrains.com/&lt;/a&gt; and be sure to select a desktop application&lt;/li&gt;
&lt;li&gt;In your app level build.gradle add
&lt;code&gt;id("dev.hydraulic.conveyor") version "1.12"&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You may want to include it in your root build.gradle with &lt;code&gt;apply false&lt;/code&gt; if you get any issues in the future as per: &lt;a href="https://programminghard.dev/gradle-plugins-best-practices/" rel="noopener noreferrer"&gt;https://programminghard.dev/gradle-plugins-best-practices/&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In app level build.gradle.kts add (this does not go into any block. just goes at the top level of this file)
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version = "1.0"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is needed even though a version is already declared in the &lt;code&gt;nativeDistributions&lt;/code&gt; block.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In app build.gradle.kts under &lt;code&gt;jvm("desktop")&lt;/code&gt; add
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    jvmToolchain {
        languageVersion.set(JavaLanguageVersion.of(21))
        vendor.set(JvmVendorSpec.JETBRAINS)
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt; In app build.gradle.kts at the top level add a &lt;code&gt;dependencies&lt;/code&gt; block (it most likely wont be there unless you're building an android app). You will already have 2 other &lt;code&gt;dependencies&lt;/code&gt; blocks most likely, but those are nested inside of the &lt;code&gt;kotlin&lt;/code&gt; and &lt;code&gt;sourceSets&lt;/code&gt; blocks. This one is needed at the top level.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; dependencies {
     debugImplementation(compose.uiTooling) &amp;lt;=== This will only be here if you are building an android app

    // Use the configurations created by the Conveyor plugin to tell Gradle/Conveyor where to find the artifacts for each platform.
    linuxAmd64(compose.desktop.linux_x64)
    macAmd64(compose.desktop.macos_x64)
    macAarch64(compose.desktop.macos_arm64)
    windowsAmd64(compose.desktop.windows_x64)
 }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;In app level build.gradle.kts you will find
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;         nativeDistributions {
             targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
             packageName = "org.example.project"
             packageVersion = "1.0.0"
         }
     }
 }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;but for some reason conveyor doesn't work with package names that are reverse dns. Replace that with &lt;code&gt;packageName = "example-project"&lt;/code&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In app level build.gradle.kts at the top level you can add
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Work around temporary Compose bugs.
configurations.all {
    attributes {
        // https://github.com/JetBrains/compose-jb/issues/1404#issuecomment-1146894731
        attribute(Attribute.of("ui", String::class.java), "awt")
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;In settings.gradle.kts you can add
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;plugins {
    id("org.gradle.toolchains.foojay-resolver-convention") version("0.8.0")
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This has to be declared after your plugin dependency block.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add a minimal conveyor.conf file
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;include "#!./gradlew -q printConveyorConfig"

app {
  display-name = My Amazing Project
  site.base-url = localhost/some/path
}
conveyor.compatibility-level = 17

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See: &lt;a href="https://conveyor.hydraulic.dev/17.0/configs/" rel="noopener noreferrer"&gt;https://conveyor.hydraulic.dev/17.0/configs/&lt;/a&gt; for more configuration&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;./gradlew desktopJar&lt;/code&gt; (Conveyor doesn't trigger your build for you yet so you still need to run this)&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;conveyor make site&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;When you make an update to your desktop app, update the version number we declared earlier, run &lt;code&gt;./gradlew desktopJar&lt;/code&gt;, and then &lt;code&gt;conveyor make site --overwrite&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉&lt;/p&gt;

&lt;p&gt;Note: You may want to call &lt;code&gt;./gradlew proguardReleaseJars&lt;/code&gt; instead of &lt;code&gt;desktopJar&lt;/code&gt;, but you need to setup proguard. I have not done this yet, but I will link it here if I figure out how to do that.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to use htmx with ktor</title>
      <dc:creator>ColtonIdle</dc:creator>
      <pubDate>Wed, 17 Apr 2024 03:05:34 +0000</pubDate>
      <link>https://dev.to/coltonidle/how-to-use-htmx-with-ktor-3740</link>
      <guid>https://dev.to/coltonidle/how-to-use-htmx-with-ktor-3740</guid>
      <description>&lt;p&gt;1 Clone this repo &lt;a href="https://github.com/tom-delalande/html-to-kotlin-converter"&gt;https://github.com/tom-delalande/html-to-kotlin-converter&lt;/a&gt; and open in intellij&lt;br&gt;
2 In the root of that project folder, create input.txt and add the component/html that you want to convert (feel free to pick a component from tailwind), run main in that project and it'll be converted to kotlin ktor html DSL in output.txt (basically, that's the readme of that project lol)&lt;br&gt;
3 in your ktor project (make sure you already added &lt;a href="https://github.com/Kotlin/kotlinx.html"&gt;ktor-html from kotlin team&lt;/a&gt;), respond to a route like so&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;call.respondHtml(HttpStatusCode.OK) {
    head {
        title {
            +"tailwind sample"
        }
        script { src = "https://cdn.tailwindcss.com" }
    }
    body {
    //paste the output of the input.txt
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4 Some tags are missing, so feel free to add them yourself as such&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import kotlinx.html.FlowContent
import kotlinx.html.HTMLTag
import kotlinx.html.HtmlBlockTag
import kotlinx.html.HtmlTagMarker
import kotlinx.html.TagConsumer
import kotlinx.html.attributesMapOf
import kotlinx.html.visit

@HtmlTagMarker
inline fun FlowContent.path(classes : String? = null, crossinline block : PATH.() -&amp;gt; Unit = {}) : Unit = PATH(
    attributesMapOf("class", classes), consumer).visit(block)

@Suppress("unused")
open class PATH(initialAttributes : Map&amp;lt;String, String&amp;gt;, override val consumer : TagConsumer&amp;lt;*&amp;gt;) : HTMLTag("path", consumer, initialAttributes, null, false, false), HtmlBlockTag {

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;5 done!&lt;/p&gt;

&lt;p&gt;Thanks prime for the &lt;a href="https://www.youtube.com/watch?v=zJNkIJCQohU"&gt;video&lt;/a&gt;, and a big thanks to Tom Delalande for this video &lt;a href="https://www.youtube.com/watch?v=9OYn48xBzOY"&gt;https://www.youtube.com/watch?v=9OYn48xBzOY&lt;/a&gt; and the code snippets above. Join his discord here if you have more questions &lt;a href="https://discord.com/invite/Cg66xQ8KgP"&gt;https://discord.com/invite/Cg66xQ8KgP&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to deploy a Ktor app on Railway</title>
      <dc:creator>ColtonIdle</dc:creator>
      <pubDate>Wed, 10 Apr 2024 20:51:44 +0000</pubDate>
      <link>https://dev.to/coltonidle/how-to-deploy-a-ktor-app-on-railway-3jde</link>
      <guid>https://dev.to/coltonidle/how-to-deploy-a-ktor-app-on-railway-3jde</guid>
      <description>&lt;p&gt;Deploying Ktor onto Railway isn't too popular, but I have found two different ways to do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;use &lt;a href="https://start.ktor.io/" rel="noopener noreferrer"&gt;https://start.ktor.io/&lt;/a&gt; and just download the baseline project there&lt;/li&gt;
&lt;li&gt;On railway set build command to &lt;code&gt;./gradlew buildFatJar&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;On railway set deploy command to &lt;code&gt;./gradlew runFatJar&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;On railway Set a variable of PORT to 8080&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note: In railway free tier, &lt;code&gt;./gradlew runFatJar&lt;/code&gt; seems to sometimes time-out so you can instead use &lt;code&gt;java -jar thejarname-all.jar&lt;/code&gt; seems to work fine without causing issues&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;use &lt;a href="https://start.ktor.io/" rel="noopener noreferrer"&gt;https://start.ktor.io/&lt;/a&gt; and just download the baseline project there&lt;/li&gt;
&lt;li&gt;On railway set PORT variable to 8080&lt;/li&gt;
&lt;li&gt;Add a nixpacks.toml to the root of your project containing...
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[start]
cmd = 'java $JAVA_OPTS -jar build/libs/*-all.jar'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Basically the same as setting the start command in option 1)&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 3
&lt;/h2&gt;

&lt;p&gt;Docker or Docker Compose&lt;br&gt;
&lt;a href="https://ktor.io/docs/docker.html" rel="noopener noreferrer"&gt;https://ktor.io/docs/docker.html&lt;/a&gt;&lt;br&gt;
&lt;a href="https://ktor.io/docs/docker-compose.html" rel="noopener noreferrer"&gt;https://ktor.io/docs/docker-compose.html&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus tips:
&lt;/h2&gt;

&lt;p&gt;Downtime/503's should be non-existent if you use railway's health-check feature + set the overlap time. This will make sure that your old service is still running until the new service is actually up and running.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Set a healthcheck path at railway &amp;gt; [Your Project] &amp;gt; Settings &amp;gt; Healthcheck &lt;a href="https://docs.railway.app/reference/healthchecks" rel="noopener noreferrer"&gt;https://docs.railway.app/reference/healthchecks&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;del&gt;Set the &lt;code&gt;RAILWAY_DEPLOYMENT_OVERLAP_SECONDS&lt;/code&gt; &lt;a href="https://docs.railway.app/reference/variables#user-provided-configuration-variables" rel="noopener noreferrer"&gt;more info&lt;/a&gt;&lt;/del&gt; Edit: As of July 20th, 24 you don't need this &lt;strong&gt;if&lt;/strong&gt; you move to the new Edge Proxy (Beta) "Opt-in to try our latest proxy. Deploy faster, and help us build our next edge." Enable this under "Settings" of your service.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You can skip setting a PORT variable in Option 1, 2 and railway will randomly assign one. Then in your ktor code you can use &lt;code&gt;System.getenv("PORT")&lt;/code&gt; to get the port from railway.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Instead of using nixpacks.toml you can use railway.json since it will do schema validation for you.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "$schema": "https://schema.up.railway.app/railway.schema.json",
    "build": {
        "builder": "NIXPACKS",
        "buildCommand": "./gradlew -p server clean build -x check -x test"
    },
    "deploy": {
        "startCommand": "java $JAVA_OPTS -jar server/build/libs/*-all.jar"
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thanks &lt;a href="https://buymeacoffee.com/brody192" rel="noopener noreferrer"&gt;@Brody&lt;/a&gt; and &lt;a href="https://buymeacoffee.com/aleksr" rel="noopener noreferrer"&gt;@Aleks&lt;/a&gt; for all of your help!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>The unfortunate challenge in signing a new app to release on Google Play in Nov of 2021</title>
      <dc:creator>ColtonIdle</dc:creator>
      <pubDate>Fri, 19 Nov 2021 21:20:16 +0000</pubDate>
      <link>https://dev.to/coltonidle/the-unfortunate-challenge-in-signing-a-new-app-to-release-on-google-play-in-nov-of-2021-2a7f</link>
      <guid>https://dev.to/coltonidle/the-unfortunate-challenge-in-signing-a-new-app-to-release-on-google-play-in-nov-of-2021-2a7f</guid>
      <description>&lt;p&gt;I'm trying to deploy an app to Google Play and it's been challenging.&lt;/p&gt;

&lt;p&gt;First I'm greeted with this page. &lt;a href="https://twitter.com/wkalic"&gt;Wojtek Kaliciński&lt;/a&gt; has a &lt;a href="https://www.youtube.com/watch?v=odv_1fxt9BI"&gt;video&lt;/a&gt; on this and it was amazingly helpful in deciding the right choice for me. &lt;strong&gt;I wish this video was linked on the play console&lt;/strong&gt; 😄&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--e9Np6R9w--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zhmzm0tp2mzru9ef4esk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--e9Np6R9w--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zhmzm0tp2mzru9ef4esk.png" alt="App Signing Preferences Image" width="880" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My choice is to go with "Export and upload a key from Java keystore".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--dbrzawcq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/05y7fl5a1j3zts43pa9e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--dbrzawcq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/05y7fl5a1j3zts43pa9e.png" alt="My Choice" width="880" height="660"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, I went to download the PEPK tool, but was greeted with this 404 page. After &lt;del&gt;complaining&lt;/del&gt; notifying @googleplay, @googleplaydev and Wojtek about it on Twitter... Wojtek got back to me first to tell me it was fixed. Hooray Wojtek and developer relations!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--otEjHahw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hjzaapxy1ed0rmpm1x6g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--otEjHahw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hjzaapxy1ed0rmpm1x6g.png" alt="404 on PEPK jar" width="880" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now that I actually have the pepk.jar I stumbled a bit more unfortunately. Firstly, I went to create the keystore.&lt;/p&gt;

&lt;p&gt;I had a few problems with this dialog and overall I feel like it should get some attention from the AS team. First is when you try to choose a keystore path, it just creates a file with no extension. This is weird to me because I (think) that the file should be a .jks extension(but what do I know)? I don't actually know for sure if it's important, but it feels weird that the wizard doesn't default to it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ABlRPY6W--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pu5cclletz1k9uu013jq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ABlRPY6W--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pu5cclletz1k9uu013jq.png" alt="Image description" width="880" height="880"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: Wojtek said "the extension of the keystore doesn't really matter, that said on Linux it did create .jks for me" With that said, I'm going to always use .jks as that shows me what the intent of the file is. I do wish that they just used .jks everywhere instead of jumping between a bunch of different name schemes.&lt;/p&gt;

&lt;p&gt;The next issue is adding a password. Following the &lt;a href="https://developer.android.com/studio/publish/app-signing"&gt;docs&lt;/a&gt;: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Create and confirm a secure password for your key. This should be different from the password you chose for your keystore.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you actually try to put a different password for the keystore and the alias you're greeted with this.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--w2-QVVnT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/15e27emgp5mb50jgc5by.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--w2-QVVnT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/15e27emgp5mb50jgc5by.png" alt="Image description" width="880" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a non-expert in security... I don't know what the hell is going on. I suppose that I'll just try to create the keystore and alias with the same password.&lt;/p&gt;

&lt;p&gt;Someone should file a bug for that. Oh right! I &lt;a href="https://issuetracker.google.com/issues/197945015"&gt;did&lt;/a&gt; a few months back.&lt;/p&gt;

&lt;p&gt;Okay... so I have a keystore... now it's time to run the pepk command we saw at the top!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Run the tool using the command below to export and encrypt your private key. Replace the arguments, and enter your keystore and key passwords when prompted.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Okay... so I have to replace the bolded text...&lt;/p&gt;

&lt;p&gt;--keystore=&lt;strong&gt;foo.keystore&lt;/strong&gt; --alias=&lt;strong&gt;foo&lt;/strong&gt; --output=&lt;strong&gt;output.zip&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Okay... so I'm having this jks "issue" again? Is it .jks or is it .keystore or does it not have an ext at all? The alias (I think) should just be key0 (I left it as the default alias name in the AS keystore generation wizard) and the output.zip name seems fine.&lt;/p&gt;

&lt;p&gt;Okay... so I will now run this command. Note that I added the extension .jks to the &lt;code&gt;prod&lt;/code&gt; file that was generated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ java -jar pepk.jar --keystore=prod.jks --alias=key0 --output=output.zip --include-cert --encryptionkey=abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Another error!&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: No value provided for flag: include-cert
USAGE:
       java -jar pepk.jar --keystore &amp;lt;release_keystore&amp;gt; --alias &amp;lt;key_alias&amp;gt; --encryptionkey=&amp;lt;encryption_key_hex&amp;gt; --output=&amp;lt;output_file&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Okay... so now I'm lost again. Is include-cert needed or not?&lt;/p&gt;

&lt;p&gt;For now... let's just drop that flag and see what happens.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ java -jar pepk.jar --keystore=prod.jks --alias=key0 --output=output.zip --encryptionkey=abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: The problem was that I was using Java 15. I downloaded a Java 11 jar and used that instead and everything worked! I &lt;a href="https://issuetracker.google.com/issues/207325146"&gt;filed a bug&lt;/a&gt; to update pepk tool, or to add a warning that the pepk tool only works with (presumably) Java 11 and lower.&lt;/p&gt;

&lt;p&gt;It asks me for the &lt;code&gt;store&lt;/code&gt; password and then a &lt;code&gt;key&lt;/code&gt; password. Now I'm starting to feel really bad that I created both store and key with the same password. The docs said to use a different password too. Oh well. I'll accept the risk I suppose.&lt;/p&gt;

&lt;p&gt;Hooray! I think we're getting somewhere! &lt;code&gt;output.zip&lt;/code&gt; was generated!&lt;/p&gt;

&lt;p&gt;One last step! Wojtek's video said that it's recommended to create a new upload key. Let's do that!&lt;/p&gt;

&lt;p&gt;The screenshot above says "For increased security, create a new upload key (optional)." so it sounds like I'll want it!&lt;/p&gt;

&lt;p&gt;All I need is a single command, and replace the bolded keywords...&lt;/p&gt;

&lt;p&gt;keytool -export -rfc -keystore &lt;strong&gt;upload-keystore.jks&lt;/strong&gt; -alias &lt;strong&gt;upload&lt;/strong&gt; -file &lt;strong&gt;upload_certificate.pem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Okay. Now I'm confused again... It's talking about an upload-keystore.jks... is this different than the foo.keystore they were talking about? I don't know... but I'm going to assume that it's actually the same keystore. All this time... I'm still confused if I should be using &lt;code&gt;prod&lt;/code&gt;, &lt;code&gt;prod-keystore&lt;/code&gt; or &lt;code&gt;prod.jks&lt;/code&gt;. Someone send help!!!&lt;/p&gt;

&lt;p&gt;Okay. What's the alias? &lt;code&gt;upload&lt;/code&gt;? Should I use &lt;code&gt;key0&lt;/code&gt;? Send more help!!!&lt;/p&gt;

&lt;p&gt;The command I ended up with is&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;keytool -export -rfc -keystore prod.jks -alias key0 -file upload_certificate.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can hopefully tell... I opted not to create a new keystore in the AS wizard... I just used the keystore I already created and I'm assuming that it'll create the upload key from that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: As per Wojtek, you should be using a new key/alias or a new keystore entirely.&lt;/p&gt;

&lt;p&gt;"You can run it again and generate a new keystore with a new key. A keystore can contain multiple keys, but it might be easier to just  keep them separate. A keystore is just a file container for keys" -Wojtek&lt;/p&gt;

&lt;p&gt;"it" in the context above is the "AS keystore generation wizard"&lt;/p&gt;

&lt;h1&gt;
  
  
  Upload time!
&lt;/h1&gt;

&lt;p&gt;I upload the generated zip... and the key certificate... and...&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--sz-BkhKv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/947dnc1x6tf9psampdzk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--sz-BkhKv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/947dnc1x6tf9psampdzk.png" alt="Image description" width="638" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  @!%@
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;I give up&lt;/strong&gt; (for now). If someone from the Play Console or Android Studio team in general read this... please send help. :sweat-smile:&lt;/p&gt;

&lt;h1&gt;
  
  
  Update! It works!
&lt;/h1&gt;

&lt;p&gt;With some help on Twitter from Wojtek I was able to figure everything out.&lt;/p&gt;

&lt;p&gt;The gist of it is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The pepk tool is no longer 404'ing&lt;/li&gt;
&lt;li&gt;The pepk tool doesn't work with Java 15, but worked with Java 11.&lt;/li&gt;
&lt;li&gt;The docs still say to use a different password for the keystore and alias, but it seems that using the same one is fine&lt;/li&gt;
&lt;li&gt;To use the additional security of an upload key, you can just generate another keystore + alias combo for uploading.&lt;/li&gt;
&lt;li&gt;At the end of these steps you'll have a

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;app-signing.jks&lt;/code&gt; that Google will use to sign your app, and you can use this for other stores/dist methods. Never lose this.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app-upload.jks&lt;/code&gt; is what you will use to sign your app when uploading to google play, and google will then use the &lt;code&gt;app-signing.jks&lt;/code&gt; to sign on your behalf. If this upload key is lost or falls into the wrong hands, you can request a new one!&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;output.zip&lt;/code&gt; which is the output of the pepk command. After you upload this, you are safe to delete it. It is only necessary in order to give google your encrypted private key.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;upload.pem&lt;/code&gt; is your public key that you give google so they know when they receive a valid app upload that was signed with your private key. You are safe to delete this as well after you upload it.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
    </item>
  </channel>
</rss>
