
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 —
- Data models (
TimeEntry.kt) — We had just one goal, to save time entires. - 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 - Repository & other
expectfunctions (TimerRepository.kt) — Other files mainly contains theexpectfunctions 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:
- A common
DefaultTimeLogRepositoryImplclass lived incommonMain. - We defined
expectfunctions 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
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



