# Jake Wharton

Blog posts, presentations, GitHub, and more.

This is one page of public article previews, not the complete archive. Follow Next page to continue. Summaries are not the original full articles.

## Compose & kotlinx.html

DevFeed: [Compose & kotlinx.html](<https://devfeed.tech/articles/compose-kotlinx-html-20925.md>)

Original publisher: [Read original article](<https://jakewharton.com/compose-and-kotlinx-html/>)

Published: 2026-08-26T00:00:00Z

Content type: article

Language: en

Sources: [Jake Wharton](<https://devfeed.tech/sources/jake-wharton.md>)

Topics: [Compose](<https://devfeed.tech/topics/compose.md>), [HTML](<https://devfeed.tech/topics/html.md>), [WebSocket](<https://devfeed.tech/topics/websocket.md>), [Document Object Model (DOM)](<https://devfeed.tech/topics/dom.md>), [ui](<https://devfeed.tech/topics/ui.md>), [client](<https://devfeed.tech/topics/client.md>)

Tags: [backend](<https://devfeed.tech/tags/backend.md>), [compose](<https://devfeed.tech/tags/compose.md>), [html](<https://devfeed.tech/tags/html.md>), [sse](<https://devfeed.tech/tags/sse.md>), [ui](<https://devfeed.tech/tags/ui.md>), [web](<https://devfeed.tech/tags/web.md>), [websocket](<https://devfeed.tech/tags/websocket.md>)

### AI overview

The article presents an approach for keeping server-rendered HTML admin pages fresh without abandoning static HTML. It uses Molecule on the JVM to observe state, kotlinx.html to render HTML fragments, Ktor to stream updates over WebSockets, and client-side JavaScript with Idiomorph to patch the DOM. It also discusses using server-sent events for unidirectional updates and the scalability tradeoffs of long-lived connections.

### Source excerpt

Let's render a simple page with Ktor server and kotlinx.html: get("/users.html") { call.respondHtml { myLayout(title = "Users") { userList( users = db.users.value, ) } } } A reusable myLayout provides scaffolding, userList encapsulates the specific page content, and db.users is a StateFlow<List<User>> from the persistence layer. I've been doing this over and over to create admin dashboard pages for a project. It works great right up until you leave it open for a minute or two, and its content becomes stale. Client-side frameworks exist within the ecosystem to "solve" this, such as Compose for HTML or Compose UI for Web. If your house has a leaky pipe you can also "solve" that by moving to a new house. I simply will not bring myself to abandoning HTML let alone delivery of static HTML in the response. Efforts are underway to adapt Compose for HTML for so-called isomorphic rendering. This would involve performing the initial composition on the server to produce the static HTML for the HTTP response. Then, client side as JS, mounting the same rendering code to reproduce the DOM tree and incrementally update it for future state changes. Tomorrow's Compose today Instead of waiting, I brought my own Compose on the JVM from home in the form of Molecule. Instead of managing a UI tree over time, Molecule manages a single piece of state over time. We can use that to render an HTML fragment on the server as a string, and then stream that to the client. webSocket("/users.ws") { launchMolecule(Immediate) { val users by db.users.collectAsState() createHTML().userList( users = users, ) }.collect(::send) } That's it. Ktor gives us the webSocket, Molecule runs the StateFlow<String> which is piped into it, and kotlinx.html renders the HTML fragment of our existing content function. In the initial HTML payload you need to wire this up somehow. Something like: script { unsafe { +""" |const content = document.getElementById("content"); |const socket = new WebSocket("ws://" + location.ho

## Live coding with dir stepper

DevFeed: [Live coding with dir stepper](<https://devfeed.tech/articles/live-coding-with-dir-stepper-20947.md>)

Original publisher: [Read original article](<https://jakewharton.com/live-coding-with-dir-stepper/>)

Published: 2026-07-16T00:00:00Z

Content type: article

Language: en

Sources: [Jake Wharton](<https://devfeed.tech/sources/jake-wharton.md>)

Topics: [intellij-platform](<https://devfeed.tech/topics/intellij-platform.md>), [IntelliJ IDEA](<https://devfeed.tech/topics/intellij-idea.md>), [Git](<https://devfeed.tech/topics/git.md>), [ide](<https://devfeed.tech/topics/ide.md>), [Code](<https://devfeed.tech/topics/code.md>), [Documentation](<https://devfeed.tech/topics/documentation.md>)

Tags: [code](<https://devfeed.tech/tags/code.md>), [coding](<https://devfeed.tech/tags/coding.md>), [git](<https://devfeed.tech/tags/git.md>), [ide](<https://devfeed.tech/tags/ide.md>), [intellij-idea](<https://devfeed.tech/tags/intellij-idea.md>), [intellij-platform](<https://devfeed.tech/tags/intellij-platform.md>), [kotlinconf](<https://devfeed.tech/tags/kotlinconf.md>), [plugin](<https://devfeed.tech/tags/plugin.md>), [presentation](<https://devfeed.tech/tags/presentation.md>), [procedural](<https://devfeed.tech/tags/procedural.md>), [refactoring](<https://devfeed.tech/tags/refactoring.md>)

### AI overview

The article describes creating a workflow for live-coding presentations after finding that Git commits and interactive rebases were cumbersome for managing sequential coding steps. It explores using IntelliJ IDEA as an extensible platform for a tool that can move through steps, jump to completed states, refactor steps, and keep the process unobtrusive for the presenter and audience.

### Source excerpt

Two months ago I gave my first ever live programming talk at KotlinConf. It was called "Talking to terminals (and how they talk back)", and you can watch it here. I've previously done slide-heavy talks which contained small demos or navigating through an existing codebase. Since this talk was starting from nothing, I decided to just write all the code from nothing. This meant spending the majority of the time in the IDE, but also figuring out how to remember what I was supposed to be writing at each step. Making the wrong choice The exceedingly obvious solution to having a series of steps in a live coding presentation is a git repo and a commit for each step. Unfortunately, this is a huge pain in the ass and really doesn't work at all (at least not for me). I started with commits, but by the 7th or 8th commit I was spending more than half my time doing interactive rebases over the entire history. Need a function in step 8 that should've been extracted between step 3 and 4? That's another 10 minutes rewriting history. Because it's a small set of files that are changing, every commit conflicts with any changes to history. It's also not clear how you actually move through the history. We want to start from nothing and move forward through history. Generally, git helps you jump backwards from the latest, not stepping forward from the oldest. It's not impossible, it's just clearly going against the grain. The problem, defined Now thoroughly enjoying this distraction from writing the actual talk, I distilled what I was trying to solve. Ability to manually write the changes required or to simply jump to the finished step. Some steps are important to watch unfold while others are procedural. This also can help with time management when I (inevitably) run low on time. Easily refactor steps as I go. Later steps often requiring refactoring previous ones to minimize the diff. As the talk evolves I want to add or merge steps. Nearly invisible to me and the viewer. I don't want t

## Retrofit, OkHttp, Okio, and SQL Delight Move to a New GitHub Organization and Join the Commonhaus Foundation

DevFeed: [Retrofit, OkHttp, Okio, and SQL Delight Move to a New GitHub Organization and Join the Commonhaus Foundation](<https://devfeed.tech/articles/the-lysine-contingency-20978.md>)

Original publisher: [Read original article](<https://jakewharton.com/the-lysine-contingency/>)

Published: 2026-06-17T00:00:00Z

Content type: news

Language: en

Sources: [Jake Wharton](<https://devfeed.tech/sources/jake-wharton.md>)

Topics: [Development](<https://devfeed.tech/topics/development.md>), [GitHub](<https://devfeed.tech/topics/github.md>), [Open Source](<https://devfeed.tech/topics/open-source.md>)

Tags: [announce](<https://devfeed.tech/tags/announce.md>), [announcement](<https://devfeed.tech/tags/announcement.md>), [github](<https://devfeed.tech/tags/github.md>), [open-source](<https://devfeed.tech/tags/open-source.md>), [organization](<https://devfeed.tech/tags/organization.md>), [projects](<https://devfeed.tech/tags/projects.md>)

### AI overview

Retrofit, OkHttp, Okio, and SQL Delight are moving to a new GitHub organization and joining the Commonhaus Foundation. The transfer is underway, with project assets and infrastructure still being moved.

### Source excerpt

I'm happy to announce that Retrofit, OkHttp, and Okio are moving to a new GitHub organization, and along with SQL Delight are all joining the Commonhaus Foundation. You can read their announcement of the move. This move is meant to reinforce our commitment to these projects' longevity and honor the fact that they've long since outgrown the stewardship of a single company. The transfer of these projects is already underway! Assets and infrastructure are still being moved, so please bear with us as we get everything back up and running. On behalf of myself, Jesse Wilson, and Alec Kazakova, thank you to everyone at Square, Cash App, and externally who contributed to these projects over the years. We look forward to continuing to work with you at their new home at lysine.dev. So what's with the name and "the lysine contingency"? Well... I first met Jesse Wilson in 2012 when Bob Lee invited me to a live-action recreation of Jurassic Park which they were going to see. It was certainly an experience. We became coworkers at Square that year. Thirteen years later, in November of last year, Jesse and I got to quit on the same day. Part of leaving was our commitment to maintain some of the more notable open source projects we built. We assumed the company would continue its stewardship in good faith. It did not. The "lysine contingency" is a plan in Jurassic Park for if the dinosaurs were to escape to limit the amount of damage they could do. Ours is a plan to limit the amount of damage our old company can do. These projects are now free to live and thrive on their own. And maybe some of the others can join them, in time.

## An update on Android KTX

DevFeed: [An update on Android KTX](<https://devfeed.tech/articles/an-update-on-android-ktx-20915.md>)

Original publisher: [Read original article](<https://jakewharton.com/an-update-on-android-ktx/>)

Published: 2026-04-01T00:00:00Z

Content type: article

Language: en

Sources: [Jake Wharton](<https://devfeed.tech/sources/jake-wharton.md>)

Topics: [Android](<https://devfeed.tech/topics/android.md>), [Kotlin](<https://devfeed.tech/topics/kotlin.md>), [Library](<https://devfeed.tech/topics/library.md>)

Tags: [android](<https://devfeed.tech/tags/android.md>), [kotlin](<https://devfeed.tech/tags/kotlin.md>), [library](<https://devfeed.tech/tags/library.md>), [update](<https://devfeed.tech/tags/update.md>)

### AI overview

Android KTX extension libraries are being wound down because their Kotlin extensions have been merged into the corresponding AndroidX libraries. The article lists obsolete or soon-to-be-obsolete KTX modules and notes that developers may eventually receive Lint warnings to help migrate their code.

### Source excerpt

Eight years ago we launched a library of Kotlin extensions for the Android platform, Android KTX. In the time since, the library became Core KTX, and numerous other KTX libraries were written for other AndroidX libraries. And while KTX's approach to adding Kotlin niceties was built on a strong technology foundation, we've made the difficult decision to begin winding down our KTX extension libraries1. That's right folks-toss another entry on the Killed By Google board! Despite the date this is no joke. However, mourn not, friends. The KTX libraries were killed because the adoption of Kotlin has been such a resounding success. All extensions have now been merged directly into their respective main library. Woo! Below is a table of every library which had a -ktx module and the first version where it became empty and thus obsolete. KTX library Obsolete in version activity-ktx 1.9.0 appsearch-ktx None, empty2 collection-ktx 1.3.0 concurrent-futures-ktx 1.4.0-alpha0134 core-ktx 1.19.0-alpha0134 dynamicanimation-ktx 1.2.0-alpha0134 fragment-ktx 1.9.0-alpha0134 lifecycle-livedata-ktx 2.7.0 lifecycle-livedata-core-ktx 2.8.0 lifecycle-reactivestreams-ktx 2.6.0 lifecycle-runtime-ktx 2.8.0 lifecycle-viewmodel-ktx 2.8.0 loader-ktx 1.2.0-alpha0134 navigation-common-ktx 2.4.0 navigation-fragment-ktx 2.4.0 navigation-runtime-ktx 2.4.0 navigation-ui-ktx 2.4.0 paging-common-ktx 3.0.0 paging-runtime-ktx 3.0.0 paging-rxjava2-ktx 3.0.0 palette-ktx 1.1.0-alpha0134 preference-ktx 1.3.0-alpha0134 savedstate-ktx 1.3.0 security-crypto-ktx None, deprecated5 sqlite-ktx 2.7.0-alpha0334 tracing-ktx 1.3.0 transition-ktx 1.8.0-alpha0134 watchface-complications-data-source-ktx 1.4.0-alpha0134 work-runtime-ktx 2.9.0 There is a feature request on Lint to provide a warning when you are declaring a KTX library equal to or newer than when it became obsolete. Hopefully this will be implemented and can aid in migrating your codebase over time. I had the privilege of starting the KTX libraries. And I also

## Let's defuse the Compose BOM

DevFeed: [Let's defuse the Compose BOM](<https://devfeed.tech/articles/let-s-defuse-the-compose-bom-20931.md>)

Original publisher: [Read original article](<https://jakewharton.com/defuse-the-compose-bom/>)

Published: 2025-12-03T00:00:00Z

Content type: opinion

Language: en

Sources: [Jake Wharton](<https://devfeed.tech/sources/jake-wharton.md>)

Topics: [Compose](<https://devfeed.tech/topics/compose.md>), [Gradle](<https://devfeed.tech/topics/gradle.md>), [Maven](<https://devfeed.tech/topics/maven.md>), [Library](<https://devfeed.tech/topics/library.md>)

Tags: [build](<https://devfeed.tech/tags/build.md>), [compose](<https://devfeed.tech/tags/compose.md>), [dependency](<https://devfeed.tech/tags/dependency.md>), [gradle](<https://devfeed.tech/tags/gradle.md>), [libraries](<https://devfeed.tech/tags/libraries.md>)

### AI overview

The article argues that Compose BOMs may be unnecessary for Gradle users because AndroidX libraries publish peer dependency constraints in Gradle module metadata, allowing Gradle to align artifacts within a library group automatically. It also discusses Renovate and Dependabot for automated dependency updates and Gradle version catalogs for centralized version management.

### Source excerpt

Many people rely on the Compose bill of materials (BOM) artifact to provide the complete set of Compose dependency versions. If we use Compose's foundation 1.8.0 but a transitive dependency bumps foundation-layout, there's a risk that these two versions are incompatible with each other despite otherwise being stable libraries. The Compose BOM will unify the versions so that all are guaranteed to work with each other. Since Compose comprises about 15 individual libraries, the Compose BOM provides us with only a single version that we have to manually change when upgrading. Nice and simple. But wait... We don't really need those things! Every AndroidX library automatically bundles peer dependency constraints into its Gradle module metadata which ensures that within a library group all artifacts resolve to the same version. Here's a fragment from the Gradle module metadata for foundation-layout v1.10.0: "dependencyConstraints": [ { "group": "androidx.compose.foundation", "module": "foundation", "version": { "requires": "1.10.0" }, "reason": "foundation-layout is in atomic group androidx.compose.foundation" }, { "group": "androidx.compose.foundation", "module": "foundation-lint", "version": { "requires": "1.10.0" }, "reason": "foundation-layout is in atomic group androidx.compose.foundation" } ], This means that in the scenario above, with a mismatched transitive dependency bump, the module metadata instructs Gradle to automatically bump all artifacts in that group. No manual action or BOM usage required. As to the single version, did you know there's actually only five library groups in the Compose BOM? Despite encompassing about 15 libraries, it's only actually defining four distinct versions (Compose UI and Material library groups share a version). Tools like Renovate or Dependabot can track the libraries in use and query upstream Maven repositories for new versions. When one is available, a PR is automatically created bumping the affected libraries. No manual version

## You should use AndroidX betas

DevFeed: [You should use AndroidX betas](<https://devfeed.tech/articles/you-should-use-androidx-betas-20984.md>)

Original publisher: [Read original article](<https://jakewharton.com/you-should-use-androidx-betas/>)

Published: 2025-11-19T00:00:00Z

Content type: article

Language: en

Sources: [Jake Wharton](<https://devfeed.tech/sources/jake-wharton.md>)

Topics: [Library](<https://devfeed.tech/topics/library.md>), [Compose](<https://devfeed.tech/topics/compose.md>), [releases](<https://devfeed.tech/topics/releases.md>), [Tooling](<https://devfeed.tech/topics/tooling.md>)

Tags: [bugs](<https://devfeed.tech/tags/bugs.md>), [compose](<https://devfeed.tech/tags/compose.md>), [features](<https://devfeed.tech/tags/features.md>), [library](<https://devfeed.tech/tags/library.md>), [releases](<https://devfeed.tech/tags/releases.md>)

### AI overview

The article explains that AndroidX uses stricter versioning than typical libraries: beta releases are API-stable and generally production-ready, while release candidates and even earlier builds are extensively tested. It argues that developers should consider using AndroidX betas to access fixes and features sooner, while recognizing that stable releases are spaced farther apart.

### Source excerpt

Did you know the versioning of AndroidX libraries and their stability guarantees are different from most libraries? Their betas and RCs are actually production-ready, and you should be using them! In a "normal" library, such as the ones I release, features are added and known bugs are fixed to produce a stable release which might be released as version 1.2.0. If any bugs are found in that release, they get fixed and put into a version 1.2.1. If new APIs are added, the next version becomes 1.3.0. This is basic semantic versioning. AndroidX does not do versioning this way. When a library has its features added and its known bugs fixed they promote that library to beta01. This artifact is now API stable! Don't believe me? This is documented in their guidelines. They also have tooling which validates that you cannot break APIs or even introduce new APIs once an artifact has reached beta. Thus, when AndroidX releases a 1.2.0-beta01 of some library, it is equivalent to a normal library releasing a 1.2.0. This is still semantic versioning, but it's a more strict subset that imposes restrictions on prerelease versions. Why do they do this? The motivations are simple: they want the stable versions to be extremely stable. That is to say, to have most of the bugs that would otherwise necessitate subsequent patch releases to be caught in the ramp-up to 1.2.0. Here's all those words in chart form: Normal library AndroidX library 1.2.0-RC 1.2.0-alpha01 1.2.0 1.2.0-beta01 1.2.1 1.2.0-beta02 (etc.) 1.2.2 1.2.0-rc01 (etc.) 1.2.0 (same bits as final RC) Wondering if anyone else uses these betas? All of Google's first-party apps ship against the code in AndroidX HEAD. Not only are they relying on these beta and RC versions, they build, test, and ship with the alpha versions and random commits in-between. By the time a library even reaches -beta01 it has already been widely tested and deployed. AndroidX is to Google's apps as what all of your util- and common- modules are to your app:

## Custom short-link redirector

DevFeed: [Custom short-link redirector](<https://devfeed.tech/articles/custom-short-link-redirector-20927.md>)

Original publisher: [Read original article](<https://jakewharton.com/custom-short-link-redirector/>)

Published: 2025-10-14T00:00:00Z

Content type: tutorial

Language: en

Sources: [Jake Wharton](<https://devfeed.tech/sources/jake-wharton.md>)

Topics: [Netlify](<https://devfeed.tech/topics/netlify.md>), [Git](<https://devfeed.tech/topics/git.md>), [hosting](<https://devfeed.tech/topics/hosting.md>), [GitHub](<https://devfeed.tech/topics/github.md>), [HTTP](<https://devfeed.tech/topics/http.md>)

Tags: [dns](<https://devfeed.tech/tags/dns.md>), [git](<https://devfeed.tech/tags/git.md>), [github](<https://devfeed.tech/tags/github.md>), [hosting](<https://devfeed.tech/tags/hosting.md>), [how-to](<https://devfeed.tech/tags/how-to.md>), [http](<https://devfeed.tech/tags/http.md>), [netlify](<https://devfeed.tech/tags/netlify.md>)

### AI overview

A tutorial explaining how to replace Bit.ly with a custom short-link redirector built on Netlify. It covers storing redirect rules in a Git repository, automatically deploying them through Netlify, configuring a custom domain and DNS, and using HTTP 302 redirects. The solution can be implemented on Netlify's free tier.

### Source excerpt

I used to use Bit.ly to put links into my presentations. Their service allowed you to customize the path portion of the link, so I was able to create links like bit.ly/ok-libs. If you were attending the talks live, watching the recording, or browsing the slides, the short URL was easy to type into a browser. Unfortunately, the path customization of Bit.ly is a global namespace. Short and memorable paths became increasingly hard to find. Ten years ago I bought the jakes.link URL to solve this problem. Bit.ly let you point custom domains at their service, and each then gets its own path namespace without risk of collision. A few months ago bit.ly announced that they would show a preview page with ads before redirecting. Gross. This happens for links on their domain and on custom domains for all free users. I'd be happy to pay a few bucks a year to avoid this, but the cheapest plan which supports custom domains is $350/year (paid annually). That's nothing short of ridiculous for the one or two links per year which I create. Netlify I use Netlify for hosting this site because one of its features is server-side redirects. This ensures that I can keep old URLs working, because a good URL is forever. But it also makes Netlify a great candidate for a build-your-own short-link redirector. Here's how I migrated jakes.link in three steps: Create a git repo with a _redirects file following Netlify's redirect documentation. / https://jakewharton.com 302 /how-to https://jakewharton.com/custom-short-link-redirector/ 302 (I use HTTP 302 redirects so that if third-party content moves over time I can at least update my redirects.) Create a project on Netlify and link it to the git repo (on GitHub or wherever else). It will automatically deploy your redirects to a subdomain on their domain which you can use to test (e.g., jakes-link.netlify.app). Pushes to the git repo will now be automatically deployed. In the "Domain management" section on the Netlify project, add your custom domain

## Fan-in to a single required GitHub Action

DevFeed: [Fan-in to a single required GitHub Action](<https://devfeed.tech/articles/fan-in-to-a-single-required-github-action-20936.md>)

Original publisher: [Read original article](<https://jakewharton.com/fan-in-to-a-single-required-github-action/>)

Published: 2025-05-07T00:00:00Z

Content type: tutorial

Language: en

Sources: [Jake Wharton](<https://devfeed.tech/sources/jake-wharton.md>)

Topics: [GitHub Actions](<https://devfeed.tech/topics/github-actions.md>), [GitHub](<https://devfeed.tech/topics/github.md>), [Pull Request](<https://devfeed.tech/topics/pull-request.md>), [issue tracker](<https://devfeed.tech/topics/issue-tracker.md>)

Tags: [github](<https://devfeed.tech/tags/github.md>), [github-actions](<https://devfeed.tech/tags/github-actions.md>), [jobs](<https://devfeed.tech/tags/jobs.md>), [pull-request](<https://devfeed.tech/tags/pull-request.md>)

### AI overview

This article explains how to fan in multiple GitHub Actions jobs into one required status check for pull requests. It addresses the problem of skipped dependent jobs being reported as successful and shows how to make a final job run unless canceled, verify the results of its required jobs, and use that job as the sole required check. It also describes a simpler failure-only alternative with fewer extension options.

### Source excerpt

It doesn't take long for a project to spawn multiple jobs in their GitHub Actions. Parallelization can lead to huge speedups for PRs. Job grouping makes it easier to conditionally enable or disable multiple steps. Each time you add a new job, however, you have to mark it as required in branch protection to prevent failing PRs from accidentally merging. Being a clever person, you might create a final job which lists all the other jobs as required, and then mark that as the single required job. jobs: # ... final-status: needs: - build - unit-tests - emulator-tests - screenshot-tests # ... Unfortunately, this does not work in practice. GitHub will skip the 'final-status' job if any of its 'needs' fail, and skipped jobs are treated as passing according to the docs: A job that is skipped will report its status as "Success". It will not prevent a pull request from merging, even if it is a required check. To work around this undesirable behavior, first, change the job to always run (unless canceled): final-status: + if: ${{ !cancelled() }} needs: - build - unit-tests - emulator-tests - screenshot-tests ... Next, add a step which ensures the status of each 'needs' job was successful: steps: - name: Check run: | results=$(tr -d '\n' <<< '${{ toJSON(needs.*.result) }}') if ! grep -q -v -E '(failure|cancelled)' <<< "$results"; then echo "One or more required jobs failed" exit 1 fi Finally, you can mark this job the only required one. It will now successfully reflect the status of all jobs. You can also hang additional steps on it, or even entire subsequent jobs (provided they aren't needed for PRs). I'm using this setup on a few repos such as Mosaic where you can also see a downstream 'publish' job which only runs on the integration branch. An alternative is to have a final job which only runs when one of its 'needs' fails and then to fail itself. An example of this strategy was posted on the Actions issue tracker. This approach is simpler, but precludes any additional steps or jobs

## Compile-time validation of JNI signatures

DevFeed: [Compile-time validation of JNI signatures](<https://devfeed.tech/articles/compile-time-validation-of-jni-signatures-20924.md>)

Original publisher: [Read original article](<https://jakewharton.com/compile-time-validation-of-jni-signatures/>)

Published: 2025-03-12T00:00:00Z

Content type: tutorial

Language: en

Sources: [Jake Wharton](<https://devfeed.tech/sources/jake-wharton.md>)

Topics: [Java](<https://devfeed.tech/topics/java.md>), [C](<https://devfeed.tech/topics/c.md>), [Gradle](<https://devfeed.tech/topics/gradle.md>), [Code](<https://devfeed.tech/topics/code.md>)

Tags: [c](<https://devfeed.tech/tags/c.md>), [compiler](<https://devfeed.tech/tags/compiler.md>), [exception](<https://devfeed.tech/tags/exception.md>), [gradle](<https://devfeed.tech/tags/gradle.md>), [java](<https://devfeed.tech/tags/java.md>), [validation](<https://devfeed.tech/tags/validation.md>)

### AI overview

This article explains how to use Java's JNI header generation to validate native method signatures at compile time. It shows how javac's -h flag generates C headers from Java declarations, allowing the native compiler to detect mismatches before runtime, including when Gradle is used.

### Source excerpt

JNI allows managed code inside the JVM or ART to call into native code. Java methods can be declared as native, and then a corresponding C function1 can be written and automatically wired together when the native library is loaded. Native code lacks mechanisms like packages and overloads, so a special format is used to encode the Java method signature. A Java method defined as: package com.example; class Things { static native long createThing(String name, int count); } Requires a matching C declaration which looks like: jlong Java_com_example_Things_createThing( JNIEnv *env, jclass type, jstring name, jint count) { // ... } If you add parameter overloading into the mix, the C declaration must include the parameter signature as well: jlong Java_com_example_Things_createThing_Ljava_lang_String_2I( JNIEnv *env, jclass type, jstring name, jint count) { // ... } Woof! And if you get any part of the encoding wrong, the method call will fail at runtime: Exception in thread "main" java.lang.UnsatisfiedLinkError: 'long Things.createThing(java.lang.String, int)' at Things.createThing(Native Method) at Main.main(example.java:6) In my experience, these signatures do not change frequently. Once they're correct you can mostly just leave them untouched. However, it's a class of problem that would be nice to eliminate completely. Especially if within your projects they do change frequently. JNI header generation When compiling native code, a header represents a series of functions implemented somewhere else. It allows consumers of a library to compile against its API without requiring the full implementation. When compiling the library itself, the compiler requires all header functions have corresponding implementations. Defining a manually-written header for our C functions would be redundant and subject to all the same problems above. Instead, we want to automatically derive the header from the corresponding Java code. As of Java 8, javac can do this for us with its -h flag. Let's l

## Deprecating idling resource libraries

DevFeed: [Deprecating idling resource libraries](<https://devfeed.tech/articles/deprecating-idling-resource-libraries-20933.md>)

Original publisher: [Read original article](<https://jakewharton.com/deprecating-idling-resource-libraries/>)

Published: 2025-02-19T00:00:00Z

Content type: article

Language: en

Sources: [Jake Wharton](<https://devfeed.tech/sources/jake-wharton.md>)

Topics: [Testing](<https://devfeed.tech/topics/testing.md>), [Monitoring](<https://devfeed.tech/topics/monitoring.md>), [Compose](<https://devfeed.tech/topics/compose.md>), [ui](<https://devfeed.tech/topics/ui.md>)

Tags: [blog-post](<https://devfeed.tech/tags/blog-post.md>), [compose](<https://devfeed.tech/tags/compose.md>), [espresso](<https://devfeed.tech/tags/espresso.md>), [monitoring](<https://devfeed.tech/tags/monitoring.md>), [testing](<https://devfeed.tech/tags/testing.md>), [tests](<https://devfeed.tech/tags/tests.md>), [ui](<https://devfeed.tech/tags/ui.md>)

### AI overview

The article announces the deprecation of the RxIdler and okhttp-idling-resource libraries. It argues that tests should wait for user-visible UI conditions rather than internal application mechanisms, and points to Compose testing APIs and custom condition-waiting approaches for View-based layouts.

### Source excerpt

When Espresso was made public a decade ago, one of its banner features was the "idling resource" concept. This monitored the main thread and any background thread pools to prevent your test from progressing until the app became idle. Waiting until idle generally increased the stability of tests since at that point the UI should be stable. We released RxIdler and okhttp-idling-resource for monitoring RxJava schedulers and OkHttp's dispatcher, respectively. Today I am deprecating both libraries. In the years since their release, I have become disillusioned with the idling resource mechanism-and I'm not alone. Like using R.id to target views, idling resources expose the internals of your application to the testing framework in a way that no real user can match. The point of building tests in the robot pattern was to describe interaction at a high-level. If you can't read a UI test to someone over the phone interacting with the real app then it probably encodes implementation detail. "Okay dad, now wait for OkHttp's Dispatcher to report itself as idle before clicking 'continue'." Yeah... no. What do we do as real users? We wait until some UI condition is met which signals our ability to progress. "Okay dad, now wait for the 'continue' button to turn green before clicking it." Much better. We don't care how the application is performing the work nor the means by which it signals the UI that it is complete. Moreover, test failures that occur based on condition waits are failures which can occur in the wild. I've been sitting on these deprecations and this blog post for a few years now. Telling you to switch to a new technique without actually demonstrating it is not great. Turns out that around the same time Google was also changing their tune on idling resources. That guidance has since been promoted to the official documentation as well. These links demonstrate how to wait on conditions using new built-in Compose testing APIs. For View-based layouts, you can write a custo

## Using Renovate to update build JDK

DevFeed: [Using Renovate to update build JDK](<https://devfeed.tech/articles/using-renovate-to-update-build-jdk-20982.md>)

Original publisher: [Read original article](<https://jakewharton.com/using-renovate-to-update-build-jdk/>)

Published: 2025-01-08T00:00:00Z

Content type: tutorial

Language: en

Sources: [Jake Wharton](<https://devfeed.tech/sources/jake-wharton.md>)

Topics: [Java](<https://devfeed.tech/topics/java.md>), [GitHub Actions](<https://devfeed.tech/topics/github-actions.md>), [ci](<https://devfeed.tech/topics/ci.md>), [Spring Boot](<https://devfeed.tech/topics/spring-boot.md>), [JSON](<https://devfeed.tech/topics/json.md>), [Development](<https://devfeed.tech/topics/development.md>), [Homebrew](<https://devfeed.tech/topics/homebrew.md>), [toolchains](<https://devfeed.tech/topics/toolchains.md>)

Tags: [build](<https://devfeed.tech/tags/build.md>), [ci](<https://devfeed.tech/tags/ci.md>), [dependencies](<https://devfeed.tech/tags/dependencies.md>), [github-actions](<https://devfeed.tech/tags/github-actions.md>), [gradle](<https://devfeed.tech/tags/gradle.md>), [java](<https://devfeed.tech/tags/java.md>), [jdk](<https://devfeed.tech/tags/jdk.md>), [json](<https://devfeed.tech/tags/json.md>), [jvm](<https://devfeed.tech/tags/jvm.md>), [renovate](<https://devfeed.tech/tags/renovate.md>), [toolchains](<https://devfeed.tech/tags/toolchains.md>), [update](<https://devfeed.tech/tags/update.md>)

### AI overview

This article explains how to use Renovate to keep the JDK used by a GitHub Actions CI build updated automatically. It moves the Java version into a .github/.java-version file, configures setup-java to read that file, and adds a custom regex manager in renovate.json to update the file. The approach allows local development with the latest JDK while preserving compatibility with older Java versions for targeting and testing.

### Source excerpt

You want to be using the latest JDK for development. Don't use Gradle toolchains, they'll needlessly force you to use old JDKs. You can still target and test on old JVM versions but develop with the latest and greatest. Java and the JDK are literally built for this. Locally this hasn't been a problem. Homebrew (or your favorite equivalent) will keep your default JDK at the latest. Keeping my GitHub actions up-to-date, however, frequently slips my mind. I find projects using 19 or 20 simply because I haven't touched the CI build in the two years since 19 or 20 was the latest. We're already using Renovate to keep dependencies up to date. With a little extra programming in JSON (wince) we can have the JDK version updated to latest as well. First, migrate the existing build JDK version in your GitHub Action to a .github/.java-version file1. 21 Next, change the setup-java action to use this file rather than a hard-coded version. - uses: actions/setup-java@v4 with: distribution: 'zulu' - java-version: 21 + java-version-file: .github/.java-version Finally, in your renovate.json52, add a custom manager to update this file3. ignorePresets: [ // Ensure we get the latest version and are not pinned to old versions. 'workarounds:javaLTSVersions', ], customManagers: [ // Update .java-version file with the latest JDK version. { customType: 'regex', fileMatch: [ '\\.java-version$', ], matchStrings: [ '(?<currentValue>.*)\\n', ], datasourceTemplate: 'java-version', depNameTemplate: 'java', // Only write the major version. extractVersionTemplate: '^(?<version>\\d+)', }, ], Commit, push, and wait for Renovate to send you a PR4. Now your CI build automatically tracks the latest JDK. I'm putting the .java-version file into the .github/ folder because I don't want to force this version on people using jenv or the like. The whole point of this setup is you can build with any version of Java newer than our very, very old baseline of Java 8 (although things like Gradle have a higher minimum

## Nonsensical Maven is still a Gradle problem

DevFeed: [Nonsensical Maven is still a Gradle problem](<https://devfeed.tech/articles/nonsensical-maven-is-still-a-gradle-problem-20949.md>)

Original publisher: [Read original article](<https://jakewharton.com/nonsensical-maven-is-still-a-gradle-problem/>)

Published: 2024-03-28T00:00:00Z

Content type: article

Language: en

Sources: [Jake Wharton](<https://devfeed.tech/sources/jake-wharton.md>)

Topics: [Maven](<https://devfeed.tech/topics/maven.md>), [Gradle](<https://devfeed.tech/topics/gradle.md>)

Tags: [build-system](<https://devfeed.tech/tags/build-system.md>), [dependencies](<https://devfeed.tech/tags/dependencies.md>), [dependency](<https://devfeed.tech/tags/dependency.md>), [jvm](<https://devfeed.tech/tags/jvm.md>)

### AI overview

The article argues that Maven's default dependency resolution strategy is problematic for libraries built with Gradle. It explains dependency graphs and contrasts Gradle's default preference for newer versions with Maven's "nearest definition" rule, where declaration order can cause an older transitive dependency to win.

### Source excerpt

There was a time when I used Maven heavily, but today all the libraries I work on build with Gradle. Even though I'm publishing with Gradle, consumers can use Gradle, Maven, Bazel, jars in libs/ (but please don't), or anything else. That's a huge JVM ecosystem win! In general, I don't have to think about what build system someone is using. I'm not here to debate subjective pros and cons of one versus any other. There is one notable exception, however. Maven's dependency resolution strategy is objectively bonkers. And if we want to support Maven consumers, we need to think about it. If you already are familiar with the concept of dependency resolution, you can skip to the nonsense. Dependency resolution primer Chances are your build system of choice (or a separate dependency resolver tool) gives you a declarative way to describe your dependencies. At build time, those declarations are resolved to .jars which can be put on the compiler classpath. Sometimes we call this a dependency tree, but it's actually a dependency graph, as separate nodes can converge back to something common to both. Project (build.gradle) ├── A │ └── B │ └── C v1.0 └── D └── C v1.0 If library B and library D agree on the version of library C, then that is the .jar version which is used. If they disagree on versions, some policy needs to decide the appropriate single version to use. Pop quiz: If library B wants version 1.1 of library C, and library D wants version 1.0 of library C, which single version of C should we use? Project (build.gradle) ├── A │ └── B │ └── C v1.1 └── D └── C v1.0 This is not a trick question. Hopefully the answer feels obvious: you use the newer version, 1.1. That version is probably compatible with 1.0, so it's safe for both library B and library D to use. We can't know for sure, to be clear, but it's a safe choice. This behavior is the default in many dependency resolvers, including the one inside Gradle. The nonsense When building with Maven, given two dependencies who

## Gradle toolchains are rarely a good idea

DevFeed: [Gradle toolchains are rarely a good idea](<https://devfeed.tech/articles/gradle-toolchains-are-rarely-a-good-idea-20938.md>)

Original publisher: [Read original article](<https://jakewharton.com/gradle-toolchains-are-rarely-a-good-idea/>)

Published: 2024-03-21T00:00:00Z

Content type: opinion

Language: en

Sources: [Jake Wharton](<https://devfeed.tech/sources/jake-wharton.md>)

Topics: [Gradle](<https://devfeed.tech/topics/gradle.md>), [toolchains](<https://devfeed.tech/topics/toolchains.md>), [Java](<https://devfeed.tech/topics/java.md>), [Containers](<https://devfeed.tech/topics/containers.md>), [ci](<https://devfeed.tech/topics/ci.md>), [Homebrew](<https://devfeed.tech/topics/homebrew.md>), [Kotlin](<https://devfeed.tech/topics/kotlin.md>)

Tags: [ci](<https://devfeed.tech/tags/ci.md>), [containers](<https://devfeed.tech/tags/containers.md>), [go](<https://devfeed.tech/tags/go.md>), [gradle](<https://devfeed.tech/tags/gradle.md>), [java](<https://devfeed.tech/tags/java.md>), [toolchains](<https://devfeed.tech/tags/toolchains.md>)

### AI overview

The article argues that Gradle Java toolchains are often counterproductive. Using an old JDK can produce outdated, non-searchable Javadoc, ignore container resource limits, and increase the risk of compiler and JVM bugs. The author recommends building with a modern JDK while controlling the target language level through compiler configuration, and keeping installed JDKs up to date.

### Source excerpt

The last post featured some Kotlin code inadvertently targeting a new Java API when the build JDK was bumped to 21. This can be solved with the -Xjdk-release Kotlin compiler flag, or by using Gradle toolchains to build with an old JDK. If you read the Gradle docs... Using Java toolchains is a preferred way to target a language version ...or the Android docs... We recommend that you always specify the Java toolchain ...you wouldn't be blamed for thinking Java toolchains are the way to go! However, Java toolchains are rarely a good idea. Let's look at why. Bad docs Last week I released a new version of Retrofit which uses a Java toolchain to target Java 8. Its use of toolchains was contributed a while ago, and I simply forgot to remove it. As a consequence, its Javadoc was built using JDK 8 and is thus not searchable. Searchable Javadoc came in JEP 225 with JDK 9. The next release of Retrofit will be made without a toolchain and with the latest JDK. Its docs will have all the Javadoc advancements from the last 10 years including search and better modern HTML/CSS. Resource ignorance Old JVMs were somewhat notorious for being ignorant to resource limitations imposed by the system. The rise of containers, especially on CI systems, means your process resource limits are different from those of the host OS. JDK 10 kicked things into high gear with cgroups support and JDK 15 extended that to cgroups2. Both of those changes were backported to the 8 and 11 branches, but since Gradle toolchains will use an already-installed JDK if available you have to have kept your JDK 8 and/or JDK 11 up-to-date. Have you? Not to stray too far off-topic, but if you installed it with SDKMAN! or similar JDK management tools there's a good chance it's wildly out of date. I keep all my JDKs up-to-date by installing them through a Homebrew tap which itself updates automatically using the Azul Zulu API. As long as I do a brew upgrade every so often, each major JDK release that I have installed will be upd

## Kotlin's JDK release compatibility flag

DevFeed: [Kotlin's JDK release compatibility flag](<https://devfeed.tech/articles/kotlin-s-jdk-release-compatibility-flag-20945.md>)

Original publisher: [Read original article](<https://jakewharton.com/kotlins-jdk-release-compatibility-flag/>)

Published: 2024-03-13T00:00:00Z

Content type: tutorial

Language: en

Sources: [Jake Wharton](<https://devfeed.tech/sources/jake-wharton.md>)

Topics: [Kotlin](<https://devfeed.tech/topics/kotlin.md>), [Java](<https://devfeed.tech/topics/java.md>), [Android](<https://devfeed.tech/topics/android.md>), [Compiler](<https://devfeed.tech/topics/compiler.md>), [Code](<https://devfeed.tech/topics/code.md>)

Tags: [android](<https://devfeed.tech/tags/android.md>), [app](<https://devfeed.tech/tags/app.md>), [code](<https://devfeed.tech/tags/code.md>), [compatibility](<https://devfeed.tech/tags/compatibility.md>), [compiler](<https://devfeed.tech/tags/compiler.md>), [exception](<https://devfeed.tech/tags/exception.md>), [ide](<https://devfeed.tech/tags/ide.md>), [java](<https://devfeed.tech/tags/java.md>), [jdk](<https://devfeed.tech/tags/jdk.md>), [kotlin](<https://devfeed.tech/tags/kotlin.md>)

### AI overview

This article explains how compiling Kotlin code with JDK 21 can expose newer JDK APIs even when the Kotlin JVM bytecode target is set to Java 8. That caused a Kotlin extension call to resolve to the newer List member method, producing NoSuchMethodError on Android. It presents Kotlin's jvm-target validation flag, which restricts compilation against APIs unavailable in the intended target JDK.

### Source excerpt

Yesterday, our Android app crashed with a weird NoSuchMethodError. java.lang.NoSuchMethodError: No interface method removeFirst()Ljava/lang/Object; in class Ljava/util/List; or its super classes (declaration of 'java.util.List' appears in /apex/com.android.art/javalib/core-oj.jar) at app.cash.redwood.lazylayout.widget.LazyListUpdateProcessor.onEndChanges(SourceFile:165) at app.cash.redwood.lazylayout.view.ViewLazyList.onEndChanges(SourceFile:210) at app.cash.redwood.protocol.widget.ProtocolBridge.sendChanges(SourceFile:125) at app.cash.redwood.treehouse.ViewContentCodeBinding.receiveChangesOnUiDispatcher(SourceFile:419) at app.cash.redwood.treehouse.ViewContentCodeBinding$sendChanges$1.invokeSuspend(SourceFile:383) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(SourceFile:33) at kotlinx.coroutines.DispatchedTask.run(SourceFile:104) at android.os.Handler.handleCallback(Handler.java:938) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loop(Looper.java:250) at android.app.ActivityThread.main(ActivityThread.java:7868) The offending code is written in Kotlin, and looks like this: The IDE showing an italicized blue style for removeFirst means it's a Kotlin extension function which compiles down to a static helper in the bytecode. However, the exception clearly indicates we are calling a member function on List directly. What gives? In JDK 21, as part of the sequenced collection effort, the List interface added removeFirst() and removeLast() methods. According to the Kotlin docs on extension functions: If a class has a member function, and an extension function is defined which has the same receiver type, the same name, and is applicable to given arguments, the member always wins. When we bumped our build JDK to 21, the new member became available and accidentally took precedence. Oops! But wait, we set our Kotlin jvmTarget to 1.8 in order to be backwards compatible. Is that not enough? val javaVersion = JavaVersion.VERSION_1_

## Perils of duplicate finding

DevFeed: [Perils of duplicate finding](<https://devfeed.tech/articles/perils-of-duplicate-finding-20952.md>)

Original publisher: [Read original article](<https://jakewharton.com/perils-of-duplicate-finding/>)

Published: 2024-02-14T00:00:00Z

Content type: article

Language: en

Sources: [Jake Wharton](<https://devfeed.tech/sources/jake-wharton.md>)

Topics: [Kotlin](<https://devfeed.tech/topics/kotlin.md>), [Code](<https://devfeed.tech/topics/code.md>), [Java](<https://devfeed.tech/topics/java.md>)

Tags: [code](<https://devfeed.tech/tags/code.md>), [kotlin](<https://devfeed.tech/tags/kotlin.md>), [refactor](<https://devfeed.tech/tags/refactor.md>)

### AI overview

A Kotlin article examines several ways to find duplicated integers in a collection. It explains why collection subtraction and MutableList.removeAll produce unexpected results, then refactors toward approaches that correctly track previously seen values.

### Source excerpt

Given an array of integers ([1, 2, 3, 1, 3, 1]), find the elements which are duplicated. No, we're not interviewing. I'm trying to prevent a user from specifying a reserved value twice. Elsewhere in the file I already have duplicate detection for object tags. val dupes: Map<Int, List<Widget>> = widgets.groupBy(Widget::tag) .filterValues { it.size > 1 } I can do the same technique for the integer array with an identity function and grabbing the resulting keys. val dupes: Set<Int> = ints.groupBy { it } .filterValues { it.size > 1 } .keys This prints [1, 3]. So... done? Yes! But no, using the map seems wasteful, right? Attempt 1 My first attempt to avoid the map was to remove the set of integers from a list of them. This should result in a list of any duplicated elements. val dupes: List<Int> = ints.toList() - ints.toSet() No matter the content of ints, this will always print []. Why? The minus operator says that it "returns a list containing all elements of the original collection except the elements contained in the given elements collection". So it removes all occurrences of each element in the set from the list. This is some surprising behavior to hide behind an operator whose signature operates on an Iterable receiver and Collection argument. Attempt 2 Second attempt switches to MutableList.removeAll which takes a collection of elements. The MutableList.remove function only removes the first occurrence of an element, so this should remove the first occurrence of each element in the set. val dupes: List<Int> = ints.toMutableList() .apply { removeAll(ints.toSet()) } This once again prints []. But why? Kotlin made me a liar. MutableList.remove does indeed only remove the first occurrence of the element. MutableList.removeAll, however, removes all occurrences of each element in the supplied collection. That's quite the subtle asymmetry. There is no function for removing all occurrences of a single element. Nor a function to remove only the first occurrences of each elem

## Intermediate collection avoidance

DevFeed: [Intermediate collection avoidance](<https://devfeed.tech/articles/intermediate-collection-avoidance-20942.md>)

Original publisher: [Read original article](<https://jakewharton.com/intermediate-collection-avoidance/>)

Published: 2024-02-07T00:00:00Z

Content type: article

Language: en

Sources: [Jake Wharton](<https://devfeed.tech/sources/jake-wharton.md>)

Topics: [Kotlin](<https://devfeed.tech/topics/kotlin.md>), [IntelliJ IDEA](<https://devfeed.tech/topics/intellij-idea.md>), [Code](<https://devfeed.tech/topics/code.md>), [Compose](<https://devfeed.tech/topics/compose.md>)

Tags: [benchmarks](<https://devfeed.tech/tags/benchmarks.md>), [compose](<https://devfeed.tech/tags/compose.md>), [intellij](<https://devfeed.tech/tags/intellij.md>), [java](<https://devfeed.tech/tags/java.md>), [kotlin](<https://devfeed.tech/tags/kotlin.md>), [memory](<https://devfeed.tech/tags/memory.md>), [performance](<https://devfeed.tech/tags/performance.md>), [refactor](<https://devfeed.tech/tags/refactor.md>)

### AI overview

This article explains how to avoid intermediate iterators and collections in Kotlin collection operations. It presents fused string joining, array initialization, and pre-sized list initialization as shorter, faster approaches that can reduce allocations, while noting that indexed access is important for performance and that these techniques are best suited to controlled internal usage.

### Source excerpt

Given a list of users, extract their names and join them into a comma-separated list. Kotlin's extension functions on collections make this trivial. users.map { it.name }.joinToString() Writing this in IntelliJ IDEA produces a "weak warning" offering advice. Call chain on collection type may be simplified An intention action will refactor the code for you to a more efficient form. users.joinToString() { it.name } Mapping the user to their name now occurs during construction of the joined string rather than as a discrete operation. The additional iterator and intermediate collection produced by the map is eliminated. This code is both shorter and faster, and the IDE helps you discover this superior form. Two similar fused operations that I like but which don't benefit from IDE advice are array and pre-sized list initialization with a lambda. If we wanted to create an array of our user's names, instead of doing users.map { it.name }.toTypedArray() we can use Array(users.size) { users[it].name } This again trades the intermediate iterator and collection within map for an indexed loop. Primitive array versions are also available. IntArray(users.size) { users[it].age } Arrays are not used too often. Mostly for memory-sensitive or performance-sensitive code, or when calling out to a Java API. Thankfully this lambda-accepting initializer is also available for pre-sized lists. MutableList(users.size) { users[it].name } Use this to initialize element default values, compute elements based on the index, or derive data from another source. In the case of deriving data, the source needs to support random access in order to actually result in a more efficient computation.1 If you use a list backed by an alternate structure (linked, persistent, etc.) performance will be abysmal. This technique works best for internal library usage and should not be used when you don't control the original list. Benchmark Score Error Units --------------------------------------------- ---------- -

## A stable, multiplatform Molecule 1.0

DevFeed: [A stable, multiplatform Molecule 1.0](<https://devfeed.tech/articles/a-stable-multiplatform-molecule-1-0-20889.md>)

Original publisher: [Read original article](<https://code.cash.app/molecule-1-0>)

Author: Jake Wharton

Published: 2023-07-19T00:00:00Z

Content type: release

Language: en

Sources: [Jake Wharton](<https://devfeed.tech/sources/jake-wharton.md>)

Topics: [Molecule](<https://devfeed.tech/topics/molecule.md>), [Compose](<https://devfeed.tech/topics/compose.md>), [Kotlin Multiplatform](<https://devfeed.tech/topics/kotlin-multiplatform.md>), [multiplatform](<https://devfeed.tech/topics/multiplatform.md>), [Kotlin](<https://devfeed.tech/topics/kotlin.md>), [Testing](<https://devfeed.tech/topics/testing.md>)

Tags: [compose](<https://devfeed.tech/tags/compose.md>), [kotlin](<https://devfeed.tech/tags/kotlin.md>), [kotlin-multiplatform](<https://devfeed.tech/tags/kotlin-multiplatform.md>), [multiplatform](<https://devfeed.tech/tags/multiplatform.md>), [swiftui](<https://devfeed.tech/tags/swiftui.md>), [testing](<https://devfeed.tech/tags/testing.md>), [tests](<https://devfeed.tech/tags/tests.md>), [ui](<https://devfeed.tech/tags/ui.md>), [web](<https://devfeed.tech/tags/web.md>)

### AI overview

Cash App Code announces Molecule 1.0, the first stable release of its Compose-based library for managing application state. The release adds Kotlin Multiplatform targets including JVM, JavaScript, and native, plus an immediate recomposition mode that does not require a frame clock. The post describes reuse outside Compose UI, notifications and widgets, unit testing with Turbine, and use across Android, iOS, and the web.

### Source excerpt

This post was published externally on Cash App Code Blog. Read it at https://code.cash.app/molecule-1-0.

## Native UI and multiplatform Compose with Redwood

DevFeed: [Native UI and multiplatform Compose with Redwood](<https://devfeed.tech/articles/native-ui-and-multiplatform-compose-with-redwood-20890.md>)

Original publisher: [Read original article](<https://code.cash.app/native-ui-and-multiplatform-compose-with-redwood>)

Author: Jake Wharton

Published: 2023-07-05T00:00:00Z

Content type: release

Language: en

Sources: [Jake Wharton](<https://devfeed.tech/sources/jake-wharton.md>)

Topics: [Compose](<https://devfeed.tech/topics/compose.md>), [Kotlin](<https://devfeed.tech/topics/kotlin.md>), [multiplatform](<https://devfeed.tech/topics/multiplatform.md>), [Kotlin Multiplatform](<https://devfeed.tech/topics/kotlin-multiplatform.md>), [Android](<https://devfeed.tech/topics/android.md>), [iOS](<https://devfeed.tech/topics/ios.md>), [Mobile](<https://devfeed.tech/topics/mobile.md>), [ui](<https://devfeed.tech/topics/ui.md>), [LLVM](<https://devfeed.tech/topics/llvm.md>)

Tags: [android](<https://devfeed.tech/tags/android.md>), [compose](<https://devfeed.tech/tags/compose.md>), [kotlin](<https://devfeed.tech/tags/kotlin.md>), [kotlin-multiplatform](<https://devfeed.tech/tags/kotlin-multiplatform.md>), [llvm](<https://devfeed.tech/tags/llvm.md>), [mobile](<https://devfeed.tech/tags/mobile.md>), [multiplatform](<https://devfeed.tech/tags/multiplatform.md>), [native](<https://devfeed.tech/tags/native.md>), [ui](<https://devfeed.tech/tags/ui.md>)

### AI overview

Cash App describes Redwood, its multiplatform mobile UI approach using native UI toolkits on each platform, reusable components, Kotlin, and Compose. The article explains Redwood's schema-generated interfaces and platform bindings, and announces Redwood 0.5 as a beta release.

### Source excerpt

This post was published externally on Cash App Code Blog. Read it at https://code.cash.app/native-ui-and-multiplatform-compose-with-redwood.

## Flow testing with Turbine

DevFeed: [Flow testing with Turbine](<https://devfeed.tech/articles/flow-testing-with-turbine-20886.md>)

Original publisher: [Read original article](<https://code.cash.app/flow-testing-with-turbine>)

Author: Jake Wharton

Published: 2023-06-21T00:00:00Z

Content type: tutorial

Language: en

Sources: [Jake Wharton](<https://devfeed.tech/sources/jake-wharton.md>)

Topics: [Testing](<https://devfeed.tech/topics/testing.md>), [Coroutines](<https://devfeed.tech/topics/coroutines.md>), [Library](<https://devfeed.tech/topics/library.md>), [Kotlin](<https://devfeed.tech/topics/kotlin.md>), [Kotlin Multiplatform](<https://devfeed.tech/topics/kotlin-multiplatform.md>)

Tags: [coroutines](<https://devfeed.tech/tags/coroutines.md>), [kotlin](<https://devfeed.tech/tags/kotlin.md>), [kotlin-multiplatform](<https://devfeed.tech/tags/kotlin-multiplatform.md>), [library](<https://devfeed.tech/tags/library.md>), [multiplatform](<https://devfeed.tech/tags/multiplatform.md>), [skip](<https://devfeed.tech/tags/skip.md>), [suspend](<https://devfeed.tech/tags/suspend.md>), [test](<https://devfeed.tech/tags/test.md>), [testing](<https://devfeed.tech/tags/testing.md>), [timeout](<https://devfeed.tech/tags/timeout.md>)

### AI overview

Cash App introduces Turbine 1.0, a library for testing Kotlin kotlinx.coroutines Flow by converting push-based streams into pull-based suspend functions. Turbine suspends while waiting for events and fails tests when unexpected events or timeouts occur. It also supports error handling, skipping items, cancellation, standalone adapters for callbacks, and utilities for testing multiple streams.

### Source excerpt

This post was published externally on Cash App Code Blog. Read it at https://code.cash.app/flow-testing-with-turbine.

## Using jlink to cross-compile minimal JREs

DevFeed: [Using jlink to cross-compile minimal JREs](<https://devfeed.tech/articles/using-jlink-to-cross-compile-minimal-jres-20981.md>)

Original publisher: [Read original article](<https://jakewharton.com/using-jlink-to-cross-compile-minimal-jres/>)

Published: 2023-01-16T00:00:00Z

Content type: tutorial

Language: en

Sources: [Jake Wharton](<https://devfeed.tech/sources/jake-wharton.md>)

Topics: [Java](<https://devfeed.tech/topics/java.md>), [Arm](<https://devfeed.tech/topics/arm.md>), [Linux](<https://devfeed.tech/topics/linux.md>), [Containers](<https://devfeed.tech/topics/containers.md>), [compose-for-desktop](<https://devfeed.tech/topics/compose-for-desktop.md>), [ssh](<https://devfeed.tech/topics/ssh.md>)

Tags: [arm](<https://devfeed.tech/tags/arm.md>), [compose](<https://devfeed.tech/tags/compose.md>), [containers](<https://devfeed.tech/tags/containers.md>), [java](<https://devfeed.tech/tags/java.md>), [jdk](<https://devfeed.tech/tags/jdk.md>), [linux](<https://devfeed.tech/tags/linux.md>), [ssh](<https://devfeed.tech/tags/ssh.md>)

### AI overview

This tutorial demonstrates using jlink and jdeps to create minimal Java runtimes tailored to an application. It shows how to reduce a simple runtime from 136 MiB to 28 MiB and use platform-specific JDKs to cross-compile runtimes for Linux x64 and other targets.

### Source excerpt

jlink is a JDK tool to create bespoke, minimal JREs for your applications. Let's try it with a "Hello, world!" program: class Main { public static void main(String... args) { System.out.println("Hello, world!"); } } My laptop is an M1 Mac and I have downloaded the Azul Zulu JDK 19 build for it. With the JDK I can both compile Java and then run the resulting program. $ mkdir out $ zulu19.30.11-ca-jdk19.0.1-macosx_aarch64/bin/javac -d out in/Main.java $ zulu19.30.11-ca-jdk19.0.1-macosx_aarch64/bin/java -cp out Main Hello, world! Azul Zulu also provides a JRE that I can use to run compiled programs. $ zulu19.30.11-ca-jre19.0.1-macosx_aarch64/bin/java -cp out Main Hello, world! Note the slight change in folder name ("jdk" -> "jre"). If we were shipping this to end-users it would be an easy win for binary size. $ du -hs zulu* 329M zulu19.30.11-ca-jdk19.0.1-macosx_aarch64 136M zulu19.30.11-ca-jre19.0.1-macosx_aarch64 But 136MiB just for "Hello, world"? Don't tell Reddit or Hacker News! Thankfully, jlink is here to help us build a minimal JRE with only what we need. Given our program, a sibling tool, jdeps, lists the Java modules which are required. $ zulu19.30.11-ca-jdk19.0.1-macosx_aarch64/bin/jdeps \ --print-module-deps \ out/Main.class java.base Our program is so simple that it only needs the "base" module. Now with jlink we can produce a minimal JRE. $ zulu19.30.11-ca-jdk19.0.1-macosx_aarch64/bin/jlink \ --compress 2 \ --strip-debug \ --no-header-files \ --no-man-pages \ --output zulu-hello-jre \ --add-modules java.base $ du -hs zulu* 28M zulu-hello-jre 329M zulu19.30.11-ca-jdk19.0.1-macosx_aarch64 136M zulu19.30.11-ca-jre19.0.1-macosx_aarch64 28MiB won't win any language wars, but it's a massive 80% savings over the full JRE. $ zulu-hello-jre/bin/java -cp out Main Hello, world! We can ship it to our client and call it a day, right? $ tar -czf hello.tgz zulu-hello-jre out $ scp hello.tgz jw@server: hello.tgz 100% 14MB 2.0MB/s 00:07 $ ssh jw@server "tar xzf hello.tgz &&

## Report card: Java 19 and the end of Kotlin

DevFeed: [Report card: Java 19 and the end of Kotlin](<https://devfeed.tech/articles/report-card-java-19-and-the-end-of-kotlin-20968.md>)

Original publisher: [Read original article](<https://jakewharton.com/report-card-java-19-and-the-end-of-kotlin/>)

Published: 2022-09-20T00:00:00Z

Content type: opinion

Language: en

Sources: [Jake Wharton](<https://devfeed.tech/sources/jake-wharton.md>)

Topics: [Java](<https://devfeed.tech/topics/java.md>), [Kotlin](<https://devfeed.tech/topics/kotlin.md>)

Tags: [developer](<https://devfeed.tech/tags/developer.md>), [feature](<https://devfeed.tech/tags/feature.md>), [future](<https://devfeed.tech/tags/future.md>), [java](<https://devfeed.tech/tags/java.md>), [java-language](<https://devfeed.tech/tags/java-language.md>), [jdk](<https://devfeed.tech/tags/jdk.md>), [kotlin](<https://devfeed.tech/tags/kotlin.md>), [project-valhalla](<https://devfeed.tech/tags/project-valhalla.md>), [report](<https://devfeed.tech/tags/report.md>), [update](<https://devfeed.tech/tags/update.md>), [valhalla](<https://devfeed.tech/tags/valhalla.md>)

### AI overview

The article reviews how Java 19 compares with predictions made three years earlier, grading features such as local methods, text blocks, records, sealed hierarchies, type patterns, and virtual threads. It concludes that Java remains strong, Kotlin has continued to evolve, and developers should use the latest JDK rather than wait for an OpenJDK LTS release.

### Source excerpt

Three years ago I gave the talk "What's new in Java 19: The end of Kotlin?" which forecasted what a future Java language would look like in September 2022 when Java 19 was released. Check your calendars, folks. It's September 2022 right now and Java 19 was released today! As expected my predictions were not perfect, but I'm pretty happy with the results. Let's check in with each feature and see how my predictions fared report-card style1. Local methods This feature allows for methods to be declared inside of other methods making them effectively private to that method. public static boolean anyMatch(Graph graph, Predicate<Node> predicate) { var seen = new HashSet<Node>(); boolean hasMatch(Node node) { if (!seen.add(node)) return false; // already seen if (predicate.test(node)) return true; // match! return node.getNodes().stream().anyMatch(n -> hasMatch(n)); } return hasMatch(getRoot()); } Grade: F 🔴 Working support for local methods was added to a branch in Project Amber in October 2019. It seemed like a slam dunk, but a JEP for the feature was never created. The branch still sits in the Project Amber repo unchanged in three years. If I had to guess, all eyes in Amber are focused on pattern matching and its related features. Hopefully someday local methods can be picked back up as a proposed feature. Text blocks A multiline string literal for when one line just isn't enough. System.out.println(""" SELECT * FROM users WHERE name LIKE 'Jake %' """); Grade: A 🟢 Delivered in Java 15. Records A read-only type that exists solely for carrying data with strong, semantic names. record Person(String name, int age) { } Grade: A 🟢 Delivered in Java 16. Sealed hierarchies Define the list of permitted subtypes of your class or interface and prevent any others. sealed interface Developer { } record Person(String name, int age) extends Developer { } record Business(String name) extends Developer { } Grade: A 🟢 Delivered in Java 17. Type patterns Declare a new name to bind when a t

## Build on latest Java, test through lowest Java

DevFeed: [Build on latest Java, test through lowest Java](<https://devfeed.tech/articles/build-on-latest-java-test-through-lowest-java-20920.md>)

Original publisher: [Read original article](<https://jakewharton.com/build-on-latest-java-test-through-lowest-java/>)

Published: 2022-05-17T00:00:00Z

Content type: tutorial

Language: en

Sources: [Jake Wharton](<https://devfeed.tech/sources/jake-wharton.md>)

Topics: [ci](<https://devfeed.tech/topics/ci.md>), [Gradle](<https://devfeed.tech/topics/gradle.md>), [Java](<https://devfeed.tech/topics/java.md>), [Testing](<https://devfeed.tech/topics/testing.md>), [toolchains](<https://devfeed.tech/topics/toolchains.md>), [Open Source](<https://devfeed.tech/topics/open-source.md>)

Tags: [ci](<https://devfeed.tech/tags/ci.md>), [gradle](<https://devfeed.tech/tags/gradle.md>), [java](<https://devfeed.tech/tags/java.md>), [open-source](<https://devfeed.tech/tags/open-source.md>), [testing](<https://devfeed.tech/tags/testing.md>), [toolchains](<https://devfeed.tech/tags/toolchains.md>), [verification](<https://devfeed.tech/tags/verification.md>)

### AI overview

This article explains how to use Gradle toolchains to compile a Java project once with the latest Java version while running tests across every supported version down to the lowest. The approach reduces CI workload and is especially useful for projects whose behavior or API usage varies by Java version.

### Source excerpt

In the past, when a new version of Java was released, I would add that version to our open source project's CI builds. strategy: matrix: java-version: - 8 - 9 ⋮ - 17 + - 18 This ensures that each project can be built and its tests pass on every major version. But this makes no sense! No user is building these projects on different versions. No user is building these projects at all. Consumers are using the pre-built .jar which we ship to Maven Central built on a single version. Testing on every version, however, is something extremely valuable. Thankfully, Gradle toolchains let us retain this while still only building once. First, CI only has to build on a single version. We choose the latest because Java has excellent cross-compilation capabilities, and we want to be using the latest tools. - uses: actions/setup-java@v2 with: distribution: 'zulu' - java-version: ${{ matrix.java-version }} + java-version: 18 Second, unchanged from before, we still target whichever Java version is the lowest supported through either the --release flag or sourceCompatibility/targetCompatibility per the Gradle docs. And finally, we set up tests to run on every supported version. // Normal test task runs on compile JDK. (8..17).each { majorVersion -> def jdkTest = tasks.register("testJdk$majorVersion", Test) { javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(majorVersion) } description = "Runs the test suite on JDK $majorVersion" group = LifecycleBasePlugin.VERIFICATION_GROUP // Copy inputs from normal Test task. def testTask = tasks.getByName("test") classpath = testTask.classpath testClassesDirs = testTask.testClassesDirs } tasks.named("check").configure { dependsOn(jdkTest) } } This setup reduces CI burden since we only compile the main and test sources once but execute the tests on every supported version from latest to lowest. Verification tasks ------------------ check - Runs all checks. test - Runs the test suite. testJdk10 - Runs the test sui

## Slope-intercept library design

DevFeed: [Slope-intercept library design](<https://devfeed.tech/articles/slope-intercept-library-design-20972.md>)

Original publisher: [Read original article](<https://jakewharton.com/slope-intercept-library-design/>)

Published: 2022-04-05T00:00:00Z

Content type: opinion

Language: en

Sources: [Jake Wharton](<https://devfeed.tech/sources/jake-wharton.md>)

Topics: [Library](<https://devfeed.tech/topics/library.md>), [Picasso](<https://devfeed.tech/topics/picasso.md>), [Android](<https://devfeed.tech/topics/android.md>), [configuration](<https://devfeed.tech/topics/configuration.md>), [Dependency injection](<https://devfeed.tech/topics/dependency-injection.md>), [Scala](<https://devfeed.tech/topics/scala.md>)

Tags: [android](<https://devfeed.tech/tags/android.md>), [configuration](<https://devfeed.tech/tags/configuration.md>), [dagger](<https://devfeed.tech/tags/dagger.md>), [dependency-injection](<https://devfeed.tech/tags/dependency-injection.md>), [http](<https://devfeed.tech/tags/http.md>), [jvm](<https://devfeed.tech/tags/jvm.md>), [library](<https://devfeed.tech/tags/library.md>), [picasso](<https://devfeed.tech/tags/picasso.md>)

### AI overview

The article proposes using slope-intercept form as a way to evaluate library design. The intercept represents the initial learning and setup cost, while the slope represents how complexity changes as requirements grow. It compares Picasso, Retrofit, and Dagger to show how libraries can trade a low initial barrier for steeper long-term complexity, or require more conceptual investment up front to remain easier to extend.

### Source excerpt

The equation y=mx+b defines a line in slope-intercept form. The line will intercept the y-axis at the value b and for each change in x its slope (the amount the line goes up or down) will change by m. Slope-intercept gives me a way to think about the design of libraries in relation to each other. The intercept is the initial cost of learning and setup for a library, and the slope is how the library's complexity changes over time. There's no real units here and the values are entirely subjective. Let's try it! Picasso Exactly 10 years ago today I introduced Picasso internally at Square. As an image loading library for Android, its primary selling point was a low intercept. It required no real configuration and only one line of code (even in a ListView adapter). Picasso.with(context).load("https://...").into(imageView); At the time this was a refreshing change from the existing libraries which required a lot of up-front and per-request configuration. The downside, however, was that as your needs grow the slope of complexity also grows faster than desired. Configuring the global instance, managing multiple instances, intercepting requests, and transforming images are all possible but more difficult than if the library was designed differently. Retrofit Retrofit is a declarative HTTP client abstraction for the JVM and Android. It requires configuration of a central object before you can use it to create instances of service interfaces. interface GitHubService { @GET("users/{user}/repos") Call<List<Repo>> listRepos(@Path("user") String user); } var retrofit = new Retrofit.Builder() .baseUrl("https://api.github.com/") .addConverter(MoshiJsonConverter.create()) .build(); var service = retrofit.create(GitHubService.class); This up-front configuration gives Retrofit a higher intercept on the y-axis. Exposure to these APIs gives you an entrypoint to discover functionality and encourages you to manage their lifetimes in an efficient way for your usage allowing the slope of com

## The state of managing state (with Compose)

DevFeed: [The state of managing state (with Compose)](<https://devfeed.tech/articles/the-state-of-managing-state-with-compose-20891.md>)

Original publisher: [Read original article](<https://code.cash.app/the-state-of-managing-state-with-compose>)

Author: Jake Wharton

Published: 2021-11-11T00:00:00Z

Content type: article

Language: en

Sources: [Jake Wharton](<https://devfeed.tech/sources/jake-wharton.md>)

Topics: [Compose](<https://devfeed.tech/topics/compose.md>), [Coroutines](<https://devfeed.tech/topics/coroutines.md>), [multiplatform](<https://devfeed.tech/topics/multiplatform.md>)

Tags: [compose](<https://devfeed.tech/tags/compose.md>), [coroutines](<https://devfeed.tech/tags/coroutines.md>), [multiplatform](<https://devfeed.tech/tags/multiplatform.md>), [rxjava](<https://devfeed.tech/tags/rxjava.md>)

### AI overview

This article describes the evolution of Cash App's Android UI architecture from RxJava-based state management toward Compose. It introduces Molecule as a way to use Compose to produce state values without performing rendering, allowing the resulting state stream to be consumed outside Compose UI.

### Source excerpt

This post was published externally on Cash App Code Blog. Read it at https://code.cash.app/the-state-of-managing-state-with-compose.

[Next page](<https://devfeed.tech/sources/jake-wharton.md?cursor=WyIyMDIxLTExLTExVDAwOjAwOjAwKzAwOjAwIiwgImUzM2FkNTg0LTA5NmQtNDY4NS04NThhLTIzMTZmMzM1NGQ2OCJd>)