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');
In Kotlin, the equivalent usually looks more like:
Log.d("MainActivity", "User loaded: $user")
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...")
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")
Custom tags are still possible when I actually care about them:
debugPrint.d("Auth", "Token expired")
debugPrint.e("Database", "Query failed")
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(...)
}
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
No initialization call.
No Application setup.
No configuration boilerplate.
Add the dependency and use it.
implementation("com.flagodna:debugprint:0.1.1")
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...")
you can simply write:
debugPrint.d("Loading...")
and get caller information such as:
SomeClass.someFunction:42
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
And when I already know the tag:
debugPrint.d("Database", "Loaded 120 records")
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")
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()}"
}
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
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")
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
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?")
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
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)