kotlin,  python,  devex

Kotlin DEVEX is not great, but Amper could fix it.

A call to embrace the CLI for Kotlin development.

Kotlin DEVEX is not great, but Amper could fix it.

I like working with kotlin. Great language, great features, super nice community, and java interop can be a superpower.

That said, Kotlin Developer Experience (DEVEX) gives me mixed feelings. On the one hand you have Jetbrains, the maker of arguably the best suite of IDEs in the market behind it. This automatically gives us, kotlin devs great support for language features directly in our editor. Debugging and refactoring Kotlin code is a breeze comparing to most other languages.

Now, the same cannot be said about other parts of the typical day-to-day workflow. For example, say you want to build a tiny Kotlin library targeting the JVM and distribute it for other devs to use. That’s a standard workflow that in the year of our lord, 2024, should be more than straight forward, right? Let’s take a look.

Getting Started

Ok. How would you go about doing that? Well, on Kotlin’s Getting Started page, Jetbrains states:

Install Kotlin

Kotlin is included in each IntelliJ IDEA and Android Studio release. Download and install one of these IDEs to start using Kotlin.

Fair enough, we’re going to use IntelliJ anyway, so yeah. Fire up your IntellijIDEA and use the New Project menu.

That works, but IMO, far from ideal. Not a big fan of clicking through a GUI just for this. What I prefer instead is using a CLI tool. Luckily, Gradle helps with that. Being the most common build tool for kotlin projects these days, I think fine to have it handle this part of the flow. This requires you to have Gradle installed globally on your system, which you can do by using sdkman. This is what it looks like to create a new kotlin library project using the Gradle CLI:

$ gradle init
Starting a Gradle Daemon (subsequent builds will be faster)

Select type of build to generate:
  1: Application
  2: Library
  3: Gradle plugin
  4: Basic (build structure only)
Enter selection (default: Application) [1..4] 2

Select implementation language:
  1: Java
  2: Kotlin
  3: Groovy
  4: Scala
  5: C++
  6: Swift
Enter selection (default: Java) [1..6] 2

Enter target Java version (min: 7, default: 21): 18

Project name (default: gradle-kotlin):

Select build script DSL:
  1: Kotlin
  2: Groovy
Enter selection (default: Kotlin) [1..2] 1

Generate build using new APIs and behavior (some features may change in the next minor release)? (default: no) [yes, no] yes


> Task :init
To learn more about Gradle by exploring our Samples at https://docs.gradle.org/8.7/samples/sample_building_kotlin_libraries.html

BUILD SUCCESSFUL in 37s
1 actionable task: 1 executed

This is what you get:

$ tree -L 3
.
├── gradle
│   ├── libs.versions.toml
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── lib
│   ├── build.gradle.kts
│   └── src
│       ├── main
│       └── test
└── settings.gradle.kts

Ok, this is kinda nice. There is a way to get started via CLI, like all the other cool kids do. Check ✅.

Adding a dependency

Now, what if you want to add a new dependency? How do you go about that?

Say for example, I need to use okhttp for creating a small HTTP client. How do I do it? First of all, we need to check which version are we going to use and what are its maven coordinates.

  1. You can find out by navigating to maven and searching for okhttp. Click through the UI and find out the version you want to use.

  2. Then you would navigate to the build.gradle.kts file in your project and add this snippet to it.

dependencies {
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
}
  1. Then, you would sync your project, by running ./gradlew or clicking through the GUI and you would be able to use the library.

Done! Added a dependency. And now you’re ready to code your small client application. This felt a bit convoluted though. If you’re a beginner you probably won’t even know what implementation stands for. What about that coordinate? You’re not gonna memorise them, are you?

Now, let’s stop right there and go the other side.

The Python way

Let’s contrast that to Python when using Poetry.

If you’re not familiar with it, Poetry is a dependency management and packaging tool for Python. It provides a standardised way to manage Python projects, including: environment and dependency management, lock files, packaging and more.

Getting Started and Adding a dependency

$ poetry new pytho_demo && cd python_demo
Created package python_demo in python_demo
$ poetry add flask
Creating virtualenv python-demo in /Users/iury.souza/projects/python-demo/.venv
Using version ^3.0.3 for flask

Updating dependencies
Resolving dependencies... (0.3s)

Package operations: 7 installs, 0 updates, 0 removals

  - Installing markupsafe (2.1.5)
  - Installing blinker (1.8.2)
  - Installing click (8.1.7)
  - Installing itsdangerous (2.2.0)
  - Installing jinja2 (3.1.4)
  - Installing werkzeug (3.0.3)
  - Installing flask (3.0.3)

Writing lock file

You’ll get this:

$ tree -L 3
.
├── README.md
├── poetry.lock
├── pyproject.toml
├── python_demo
│   └── __init__.py
└── tests
    └── __init__.py

3 directories, 5 files

Done. Much simpler, right?. You can immediately start coding. This highlights a few key differences between the two development workflows and their developer experience:

What Poetry does better

Ease of Use: Python’s Poetry offers a straightforward, CLI-driven approach to managing dependencies. In contrast, Kotlin’s reliance on manually configuring Gradle files is more involved and error-prone.

Impact on Development Workflow: Python developers can rapidly add and manage dependencies without stepping out of their development flow. On the other hand, Kotlin developers often find themselves fiddling with build scripts and build syncs.

Reproducibility and Environment Management: Poetry’s management of pyproject.toml and poetry.lock files ensures that builds are reproducible and environments are consistent across different machines and setups. With Kotlin there’s no such elegant solution. You could use the gradle lock options, but that’s not a standard workflow and doesn’t actually work the same way.

Why a CLI approach is better

Using the CLI is a better approach for a few reasons:

  • Consistency: The CLI provides a consistent interface for developers to interact with the build system. This reduces the cognitive load on developers and makes it easier to remember how to perform common tasks.
  • Automation: The CLI can be easily automated using scripts or other tools. This allows developers to perform repetitive tasks more quickly and efficiently.
  • Replicability: The CLI commands can be easily shared with other developers, making it easier to replicate workflows across different machines and environments.

What’s all the fuss about?

You might be thinking:

- Uh… I don’t see the big deal. I’m fine with the GUI. It’s just a few clicks.

Well, yeah. But consider this: How many times do you add or remove dependencies from a project? Besides, I didn’t even touch other aspects like testing, or publishing Kotlin libraries as it would make this post too long, and would hit the same points. All of these are also more convoluted than they should be and could benefit from a CLI based approach.

Besides, a lot Kotlin (especially Android) get so rely on the IDE for managing their environment and end up not getting familiar with a CLI based workflow. Missing all the benefits that come with it.

An obvious counterargument is that Kotlin has many constraints that Python and other languages doesn’t have, like the JVM, and the need to be compatible with the whole Java ecosystem, but maybe there’s some tradeoffs people would be willing to make to have a better DEVEX in at least certain use cases. I think that either the Kotlin or Gradle team could take a page from Poetry’s book and work to make the development experience more streamlined and user-friendly.

Moving Forward

I think there’s a huge opportunity here for Kotlin to improve its developer experience by providing first-class support for the command line for common workflows. Jetbrain’s Amper kinda looks like a step in the right direction; I hope we devs can tap into that system via the CLI.


A Glimpse into a Better Kotlin DEVEX

What Poetry does for Python, Amper could do for Kotlin. When you run Poetry init, it generates a pyproject.toml file. This is what a typical pyproject.toml file looks like:

[tool.poetry]
name = "audioguide-ai-api"
version = "0.0.1"
description = "The backend for the audioguide-ai project"
authors = ["Iury Souza <iurysza@gmail.com>"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.12.2"
Flask = "^3.0.3"
python-dotenv = "^1.0.1"
gunicorn = "^22.0.0"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

This is a declarative way to configure your Python project. You can either modify this file directly or use poetry commands to add or remove dependencies, etc.

Now, this is an example of Amper’s module file, which is also declarative way to configure your project:

product:
  type: lib
  platforms: [ jvm, android, iosArm64, iosSimulatorArm64, iosX64 ]

  # Shared Multiplatform dependencies:
  dependencies:
    - org.jetbrains.compose.foundation:foundation:1.5.0-rc01: exported
    - org.jetbrains.compose.material3:material3:1.5.0-rc01: exported

  # Android-only dependencies
  dependencies@android:
    - androidx.activity:activity-compose:1.7.2: exported
    - androidx.appcompat:appcompat:1.6.1: exported

  # iOS-only dependencies with a dependency on a CocoaPod
  dependencies@ios:
    - pod: 'FirebaseCore'
      version: '~> 6.6'

  settings:
    kotlin:
      serialization: json

    # Enable Compose Multiplatform framework
    compose: enabled

Imagine now, if you could run something like amper init and have a project scaffolded for you. You could then run:

$ amper add okhttp -android

And have a new dependency added to your project, and a lockfile generated. After that you could run:

$ amper install

Which would read from the lockfile, download the dependencies, and sync the project for you. Maybe you would have your credentials in the module.yml file and you could run amper publish to publish your library.

Conclusion

These are just some thoughts I’ve put together. I think that Kotlin is a great language, but there’s a lot of room for improvement in the developer experience. I think that embracing the CLI more fully could be a step in the right direction.

If you ever wrote a Node module, a Ruby gem or a Cargo package, you know how easy it is out there. I think it’s high time we have that too.


Note: I chose to compare Kotlin with Python just because I recently had to work with it and it was fresh in my mind, but the same could be said about other languages like Rust, Go, JS, Ruby and many others.

...