Blog Infos
Author
Published
Topics
, , , ,
Published

Kotlin Multiplatform Image

I’ve always wanted to develop desktop apps, but never really had time to explore due to limitations in language or framework. Many will say that a coder should be able to code in any language, but trust me, there’s a learning curve to it to be able to know the best practices of that framework. I’ve always been in Android, but with Kotlin Multiplatform, I think I’ve just found the right tool to make my dreams come true. Well, I still have a lot to explore, but this is what I’ve tried during my weekend to get myself into trying this new framework.

For those who directly want to skip to the code. Here’s the GitHub link for the repository.

Link: https://github.com/JyotimoyKashyap/trackie

The Problem

Lately at work, I’ve felt the need to better track how I spend my time — specifically, how many hours I put into different tasks. I’ve always tried to track it in some way, but none of the methods really gave me data that felt accurate or useful.

Here are a few things I’ve tried:

  • Logging work directly on the ticket using a plugin as soon as I complete a task
  • Keeping manual notes with timestamps of when I started and stopped work
  • Creating a “work calendar” where I log tasks as calendar events

Of all these, I liked the calendar method the most — it gave me a good overview — but the experience didn’t feel smooth or intuitive. That’s when I started exploring apps designed for time tracking. Unfortunately, most of them were either paid or didn’t quite match the way I wanted to work.

I know these reasons might sound a bit vague, but as a developer, I really want a tool that feels good to use and is fully customizable to my needs. That’s something I could only achieve by building it myself.

Idea 💡

Based on the problem I described earlier, I wanted to build a time-tracking app that works seamlessly across macOS, Windows, and the Web. (We can discuss iOS and Android support later.)

Since I didn’t want this to be just a personal tool, my goal was to make it usable for others as well — which meant making it available on as many platforms as possible.

I had limited experience with Kotlin Multiplatform, so I had to do some research. In that process, Gemini 2.5 Pro turned out to be incredibly helpful. It’s gotten impressively good and really helped me navigate the initial technical decisions.

Thinking About the MVP

I’ve been trying to launch an Android app for a while now, but I kept approaching it with the mindset of including all the features. I think there’s a name for that — feature creep — and unsurprisingly, it’s been delaying the launch again and again.

Recently, I realized that to keep my motivation up (and chase those dopamine hits), I need to ship something fast and simple. That’s when I shifted my mindset toward building just the Minimum Viable Product (MVP)— only the essential things a user would care about.

From a functionality point of view, I narrowed it down to these core user flows:

  • A user should be able to start a timer with an optional comment to log their work or task.
  • When the user stops the timer, the work entry gets saved.
  • The user should be able to view their past logs with the comment, start time, and time spent.

That felt like a solid bare minimum to get started with — although I later realized I even forgot to add a delete functionality for logged work 😅.

Setting Up the Development Environment

During the whole process, I used two main tools to complete the MVP:

  • Gemini 2.5 Pro
  • IntelliJ IDEA

Google is currently offering a free Gemini 2.5 Pro subscription for a year, so I decided to test it out. I also have access to IntelliJ IDEA Ultimate, thanks to their generous free subscription for students.

To get started, I just had to install the Kotlin Multiplatform plugin — and then I was good to go.

Development Process

In this blog, I won’t provide a detailed guide around the code since it mostly consists of basic business logic and some platform-specific parts. Instead, I’ll focus on the key architectural and low-level design decisions I made during development.

As someone new to Kotlin Multiplatform, I spent time understanding the philosophy behind it — and that really gave me a fresh perspective on how to approach cross-platform development.

Design Pattern

We followed a pretty basic MVVM (Model — View — ViewModel) pattern for designing the layers of this application. This is generally being followed in most android applications hence it was a no brainer.

I tried to keep most of the business logic in commonMain , hence it contained all the platform-agnostic code. Hence, it mainly has —

  1. Data models ( TimeEntry.kt ) — We had just one goal, to save time entires.
  2. ViewModel ( TimerViewModel.kt ) — Since we had only one screen, hence only one viewmodel was being used, although, I believe in later parts of this blog, people will realise two viewmodels could have been used
  3. Repository & other expect functions ( TimerRepository.kt ) — Other files mainly contains the expect functions for which platform-dependent code has to be written.

Platform specific code ( desktopMain & wasmJsMain ) contained glue code and actual implementations of those expect functions.

Evolving the Data Layer

Our first major architectural challenge was data storage. How do you write a repository in commonMain when file access is different on every platform?

Our first attempt was to use the expect/actual pattern on the entire TimeLogRepository class. This worked, but it was clunky and required re-implementing every function on each platform.

The breakthrough was realizing we didn’t need to make the whole class platform-specific — only the I/O functions. We settled on a much cleaner approach:

  1. A common DefaultTimeLogRepositoryImpl class lived in commonMain.
  2. We defined expect functions for the only parts that needed to be different:

 

// In commonMain
expect fun readJsonFromFile(): String?
expect fun writeJsonToFile(jsonString: String)

 

3. We provided the actual implementations in the platform-specific modules (desktopMain using java.io.File and jsMain using browser localStorage).

This was a pivotal moment that dramatically improved our architecture, making it cleaner, more maintainable, and perfectly aligned with the multiplatform philosophy.

Why Logging in an MVP?

First of all, you might be wondering — why am I focusing on logging in an MVP?

Well, there are try-catch blocks around the code, so errors must be logged, right? That’s exactly where I hit a hurdle. Initially, I considered logging in the platform-specific modules, but then I decided to move the logging into commonMaininstead.

By doing this, any errors thrown become accessible in commonMain, which allows me to handle them more consistently across platforms. It also helps in extracting exception messages and displaying appropriate UI based on the type of failure.

There are actually some neat ways to handle this using a sealed class and a WorkResult pattern. Below is an example of how I implemented it in WorkResult.kt.

sealed class WorkResult<out R> {
    data class Success<out T>(val data: T) : WorkResult<T>()
    data class Error(val exception: Exception) : WorkResult<Nothing>()
    data object Loading : WorkResult<Nothing>()
}

But for an MVP, shipping the product was the first and foremost priority. I wanted to launch something quickly — refinements could always come later.

So, all errors were caught and logged at the repository layer. This approach also sets things up nicely for integrating WorkResult later on.

However, the main challenge was that logging works differently across platforms. After some digging, I figured out the appropriate logging mechanisms for both desktop and web targets. To handle this cleanly, I wrote expect functions for the following:

expect object Logger {
    fun log(message: String)
    fun warn(message: String, throwable: Throwable? = null)
    fun error(message: String, throwable: Throwable? = null)
}

The actual implementations were written separately for desktopMain and wasmJsMain. While I’m still figuring out a few things related to wasmJsMain and its implementation, the desktopMain side is already sorted.

Going Live: The Adventure of GitHub Pages Deployment

With the app up and running, the final frontier was publishing the web version. We chose GitHub Pages and set up a GitHub Actions workflow to automate the deployment process. That step turned out to be an adventure in itself.

Challenge #1: The Blank Page of Doom

Our app deployed — but all we saw was a blank white page.

The Problem: The index.html file was missing a crucial <base href> tag. GitHub Pages hosts sites in a sub-folder (e.g., user.github.io/repo-name/), so the app couldn’t locate its JavaScript files. This issue also affected local development.

The Fix: We created two versions of the index.html file — one for local development, and a index.prod.html that included <base href="/trackie/" /> for production. Then, we added a step in our GitHub Actions workflow to replace the default file with the production version before building.

Challenge #2: “Nothing to Commit”

We’d push changes, but the deployment action kept reporting that nothing had changed.

The Problem: The action was either using a cached build or didn’t detect any differences in the output files.

The Fix: We added the clean task to our Gradle command in the workflow:
./gradlew clean :composeApp:wasmJsBrowserDistribution.
This forced a fresh build on every run, ensuring the deployment action always had updated files to publish.

After several rounds of troubleshooting, we eventually landed on a final, reliable workflow using GitHub’s modern artifact-based deployment, which effectively resolved all our caching issues.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

When sharing isn’t caring: Using platform-specific code in Kotlin Multiplatform

Sharing code across platforms is a wonderful superpower. But sometimes, sharing 100% of your codebase isn’t the goal. Maybe you’re migrating existing apps to multiplatform, maybe you have platform-specific libraries or APIs you want to…
Watch Video

When sharing isn’t caring: Using platform-specific code in Kotlin Multiplatform

Russell Wolf
Kotlin Multiplatform Developer

When sharing isn’t caring: Using platform-specific code in Kotlin Multiplatform

Russell Wolf
Kotlin Multiplatform ...

When sharing isn’t caring: Using platform-specific code in Kotlin Multiplatform

Russell Wolf
Kotlin Multiplatform Deve ...

Jobs

Final Thoughts

Building Trackie was more than just creating a utility — it was a deep dive into the practical realities of modern multiplatform development. It reinforced the value of having a solid architectural foundation and proved that, with today’s tools, even a solo developer can efficiently build and deploy an application across multiple platforms — all from a single codebase.

Vibe Coding

Some might be wondering — what was actually “vibe coded” here?

Well, most of the files were generated by Gemini 2.5 Pro, but behind the scenes, I wore multiple hats — Product Manager, UI/UX Designer, and Developer. I guided the AI with clear instructions, corrected its course when it drifted, and made decisions that aligned with my vision.

Thanks to my experience with Android and its architecture, I was able to vibe code a clean, modular project that doesn’t feel like a monolith. I genuinely believe this foundation is scalable and ready for future features.

So no, I don’t think AI will replace us — but it’s absolutely worth embracing. When used well, it becomes a powerful tool in our arsenal to build and ship things faster than ever before.

Below is the GitHub link for my project:

Link: https://github.com/JyotimoyKashyap/trackie

Take a look: https://jyotimoykashyap.github.io/trackie/

P.S. Well, there’s always a chance I might forget to start the timer in the first place — looks like I’ll still have to keep a few things in check 😅. Just a little realization I had after building the whole thing.

This article was previously published on proandroiddev.com

Menu