DEV Community

Cover image for I Missed Flutter's debugPrint After Moving to Kotlin
Cahyanudien Aziz Saputra
Cahyanudien Aziz Saputra

Posted on

I Missed Flutter's debugPrint After Moving to Kotlin

For more than three years, Flutter was my main environment for building applications.

Recently, I started working on several new projects with Kotlin and Jetpack Compose.

There was no dramatic problem.

Kotlin was fine. Android's tooling was fine. And Android already had a perfectly capable logging API.

But there was one small thing that kept bothering me.

Logging felt unnecessarily repetitive.

In Flutter, I could simply write:

debugPrint('User loaded: $user');
Enter fullscreen mode Exit fullscreen mode

In Kotlin, the equivalent usually looks more like:

Log.d("MainActivity", "User loaded: $user")
Enter fullscreen mode Exit fullscreen mode

Of course, this is not a serious problem.

It's a few extra characters.

But when you write something dozens or hundreds of times during development, these tiny decisions start becoming part of your workflow.

I kept thinking:

I don't need another logging framework. I just want the convenience I was used to.

So I built one.

GitHub: https://github.com/cas8398/debugprint

Maven Central: https://central.sonatype.com/artifact/com.flagodna/debugprint

Not another logging framework

The first mistake would have been trying to build a "better Android logging framework."

I wasn't interested in that.

Android already has Logcat, and there are plenty of mature logging libraries for applications that need structured logging, persistence, remote logging, crash reporting, or sophisticated filtering.

That wasn't the problem I was trying to solve.

I wanted something much smaller:

debugPrint.d("Loading user...")
Enter fullscreen mode Exit fullscreen mode

That's it.

A tiny convenience layer for development-time logging.

That became debugprint.

The basic idea

The API is intentionally close to the mental model I had from Flutter.

debugPrint.d("Hello")
debugPrint.i("User signed in")
debugPrint.w("Cache miss")
debugPrint.e("Request failed")
debugPrint.v("Verbose information")
Enter fullscreen mode Exit fullscreen mode

Custom tags are still possible when I actually care about them:

debugPrint.d("Auth", "Token expired")
debugPrint.e("Database", "Query failed")
Enter fullscreen mode Exit fullscreen mode

The library is not trying to hide Android's logging system.

It simply removes some of the repetitive parts that I don't always need while developing.

Debug means debug

One thing I wanted from the beginning was for the library to behave like its name suggests.

If I'm using it for development debugging, I don't want to manually write this everywhere:

if (BuildConfig.DEBUG) {
    Log.d(...)
}
Enter fullscreen mode Exit fullscreen mode

So debugprint automatically enables logging for debug builds and disables it for release builds.

The intended workflow is simply:

Debug build   → logging enabled
Release build → logging disabled
Enter fullscreen mode Exit fullscreen mode

No initialization call.

No Application setup.

No configuration boilerplate.

Add the dependency and use it.

implementation("com.flagodna:debugprint:0.1.1")
Enter fullscreen mode Exit fullscreen mode

The package is available on Maven Central:

https://central.sonatype.com/artifact/com.flagodna/debugprint

Automatic caller information

Another thing I found repetitive was tags.

When debugging something locally, I often don't really care about creating a meaningful logging category for every single message.

I mostly care about:

Where did this message come from?

So debugprint can derive the tag from the caller.

Instead of:

Log.d("SomeClass", "Loading...")
Enter fullscreen mode Exit fullscreen mode

you can simply write:

debugPrint.d("Loading...")
Enter fullscreen mode Exit fullscreen mode

and get caller information such as:

SomeClass.someFunction:42
Enter fullscreen mode Exit fullscreen mode

There is an obvious trade-off here.

Finding the caller requires inspecting the stack, and stack walking is not free.

So automatic tagging can be disabled when necessary:

DebugPrint.autoTag = false
Enter fullscreen mode Exit fullscreen mode

And when I already know the tag:

debugPrint.d("Database", "Loaded 120 records")
Enter fullscreen mode Exit fullscreen mode

there is no reason to pay for automatic caller detection.

Lazy messages

This was probably one of the more important design decisions.

Consider:

debugPrint.d("User: $user")
Enter fullscreen mode Exit fullscreen mode

The message has to be constructed before the logging function receives it.

That may be completely fine for a small string.

But sometimes the value you're formatting is expensive.

So debugprint also accepts a lambda:

debugPrint.d {
    "User: ${expensiveOperation()}"
}
Enter fullscreen mode Exit fullscreen mode

When debug logging is disabled, the lambda is not evaluated.

That means the work required to construct the message can be skipped entirely.

The API stays simple, but it gives me a way to avoid unnecessary work when logging is turned off.

A small sink abstraction

At first, everything simply went to Android's Logcat.

But I didn't want the implementation to be permanently tied to one output.

So the library has a small sink abstraction.

Conceptually:

debugPrint
    ↓
  sink
    ↓
 Logcat
Enter fullscreen mode Exit fullscreen mode

The sink can be replaced with a custom implementation.

That could be useful for something like writing development logs to a file or forwarding them somewhere else.

The important part for me is that the core API doesn't have to change.

debugPrint.d("hello")
Enter fullscreen mode Exit fullscreen mode

can remain the same even if the destination changes.

Logging should not break the app

There is one more rule I wanted to keep:

A logging failure should never become an application failure.

If a custom sink throws an exception, debugprint treats that as a logging problem rather than an application problem.

In other words:

logging fails
     ↓
application continues
Enter fullscreen mode Exit fullscreen mode

A debug utility should not be capable of taking down the application it is helping me inspect.

What I deliberately did not build

There are many things I could have added.

Timestamps.

Colors.

JSON formatting.

Log persistence.

Remote logging.

Crash reporting.

Network transport.

Analytics.

A configuration dashboard.

And probably another hundred "useful" features.

I intentionally left them out.

The point of debugprint is not to become a complete logging ecosystem.

It is supposed to stay small.

If I need a production logging system, I should use a production logging system.

If I just want to write:

debugPrint.d("Why is this null?")
Enter fullscreen mode Exit fullscreen mode

I want that to be easy.

Why make it a public library?

Honestly, the original motivation was personal.

I was working on several Kotlin projects and wanted the same small convenience in all of them.

So initially, this could have remained a private utility inside one project.

Instead, I decided to turn it into my first published Kotlin/Android library and put it on Maven Central.

That made the project slightly more interesting.

Once something becomes a reusable library, even a tiny one, you have to think about API design differently.

You start asking:

  • Is this API actually understandable?
  • Is the default behavior sensible?
  • What should happen in release builds?
  • What are the performance trade-offs?
  • How much configuration is enough?
  • What should never be part of the public API?
  • Can I keep the dependency footprint small?

A utility that took a short time to write becomes a small exercise in software design.

Is this something everyone needs?

No.

And I think that's worth saying explicitly.

Android already provides logging.

Logcat is not missing.

There are already many mature logging libraries.

debugprint is not trying to replace them.

This library exists because I spent more than three years using one development workflow, moved to another ecosystem, and realized that I missed one tiny piece of that workflow.

That's a very small reason to create a library.

But sometimes small reasons are enough.

For me, the interesting part wasn't inventing a new logging technology.

It was noticing how much developer experience is made up of tiny conveniences that become invisible once you use them every day.

After years of writing Flutter code, debugPrint() had become one of those conveniences.

So I brought the idea with me.

Flutter
  ↓
debugPrint()
  ↓
years of muscle memory
  ↓
Kotlin
  ↓
"Why am I writing this tag again?"
  ↓
debugprint
Enter fullscreen mode Exit fullscreen mode

My first Kotlin/Android library probably isn't going to change how people log on Android.

It wasn't supposed to.

I just wanted my new Kotlin projects to feel a little more familiar.

Links

GitHub repository:
https://github.com/cas8398/debugprint

Maven Central:
https://central.sonatype.com/artifact/com.flagodna/debugprint

Top comments (0)