Napier: one Kotlin Multiplatform logger that lands in Logcat, console.log and java.util.logging
Logging library for Kotlin Multiplatform
At a glance
- What is it?
- Napier gives a Kotlin Multiplatform project a single logging call in common code and routes it to the native logger of each target. The price is a global init step, a hand written Antilog when you want crash reporting, and documentation that still points at jCenter.
- Who is it for?
- Napier fits teams that want one call site in commonMain and will accept a global base Antilog plus a per platform logger, and it fails to fit projects that need structured JSON, per module level filtering, file rotation or a documented way to route logs off device.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 108 days ago.
- What is it written in?
- Mainly Kotlin, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 21, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One call in commonMain, a different logger on every target
The problem Napier addresses is narrow and real. A Kotlin Multiplatform project has a common module and a set of platform modules, and each platform ships with a logging facility that the others do not have. Android has android.util.Log and its Logcat viewer. Darwin targets have print. JavaScript has console.log. The JVM has java.util.logging.Logger. Code in the common module cannot call any of those directly, so teams end up with an expect and actual pair, or a common wrapper of their own, for every logging call.
Napier removes the expect declaration. The documentation states that logs written in a common module are displayed in the logger viewer of each platform, and it names the exact native sink used for each target: android.util.Log for Android, print for Darwin, console.log for JavaScript, and java.util.logging.Logger for the JVM. That mapping is the whole idea. The common code calls Napier.v, Napier.d, Napier.i, Napier.w, Napier.e or Napier.wtf, and the platform implementation decides where the line lands.
The formats differ per platform rather than being normalized. On Android the documented line reads [Class name]$[Method name]: [Your log]. On Darwin it is [Date time][Symbol][Log level][Class name].[Method name] - [Your log]. On JavaScript and the JVM the docs show screenshots rather than a text format. If you are comparing lines between platforms, or writing a parser, you have to accept that the shape of a record is not stable across targets.
The project is described as a logging library for Kotlin Multiplatform, and its topics list kotlin-js, kotlin-library, kotlin-multiplatform and kotlin-native. The repository layout backs that up, with separate top level directories for android, ios, js, jvm, macOS, watchOS, a shared napier module and an mpp-sample. The author states the library is inspired by Timber, and recommends Timber itself where Kotlin Multiplatform is supported. That is the intended audience: mobile and multiplatform teams who like Timber's shape but cannot use it past one platform.
Adding the dependency to commonMain with the right coordinates
The install path is Maven Central, and the coordinates changed once. The project states that the library can be downloaded from MavenCentral or jCenter, that the Maven Central package name is io.github.aakira and that jCenter is only available up to version 1.4.1 under the older com.github.aakira name. The repository also carries a Maven Central badge for io.github.aakira/napier.
You pick a version in your build file and then declare the dependency in your commonMain source set. The docs show both a Groovy and a Kotlin DSL form. The Groovy variant looks like this, where the version property is set separately:
sourceSets {
commonMain {
dependencies {
// ...
implementation "io.github.aakira:napier:$napierVersion"
}
}
}The Kotlin DSL equivalent uses the by getting accessor:
sourceSets {
val commonMain by getting {
dependencies {
implementation("io.github.aakira:napier:$napierVersion")
}
}
}Two things to watch. First, the repository listing includes both build.gradle and settings.gradle.kts at the top level, so projects on either DSL can use the matching snippet. Second, the version placeholder in the project docs is written as def napierVersion = "[latest version]", which is a placeholder rather than a resolvable value, so you must substitute a real version. The listed releases are 2.7.1, 2.7.0 and 2.6.1, so the coordinate and the version are two separate things you have to get right.
The documentation also says you can download this from 1.4.1 on Maven Central, which is the earliest version it claims for that repository. If you are pinning an old version for a legacy branch, check the coordinates match the era, because the com.github.aakira group id belongs to the jCenter era that stops at 1.4.1.
Napier.base must run before any log call, and the logger is global
Napier is not usable out of the box. The documentation is explicit: you must initialize Napier in your module. The mechanism is an Antilog, and the base is installed with Napier.base. On Android the initialization is a single call:
Napier.base(DebugAntilog())On iOS the same thing is wrapped in a function in the Kotlin Multiplatform project, and the Swift side calls it through the generated proxy:
fun debugBuild() {
Napier.base(DebugAntilog())
}NapierProxyKt.debugBuild()The call is global and process wide. The documentation does not describe a per instance logger, a scoped logger, or a builder that you pass around, so switching log destinations happens by replacing the base. To shut logging down there is a single call, Napier.takeLogarithm(), which the docs list under clear antilog.
The practical consequence is that initialization becomes a wiring problem rather than a library call. On Android it has to run before the first Napier.v in your process, which usually means the Application class. On iOS it has to run from the AppDelegate, and the Crashlytics sample does exactly that, with a DEBUG branch calling NapierProxyKt.debugBuild() and a release branch calling a different function. The same shape appears in the shared scope sample, where a CoroutineExceptionHandler prints directly with println rather than going through Napier.
If you need different behaviour per screen or per feature, this global design will get in your way, because there is no documented way to scope an Antilog to a subtree of your code.
The [async] suffix is the only coroutine aware feature documented
Napier is not a coroutine library, but it does carry one piece of concurrency awareness. The Darwin format description says an [async] label is added at the end when the call comes from a suspend function. The initialization table gives the same fact in a different shape, listing an argument named coroutinesSuffix of type Boolean with the description that it adds the [async] label at the end if it is called from suspend functions.
The base call therefore takes an option, and the label is opt out through a Boolean. That is the extent of it. The docs do not document structured concurrency primitives, log buffering, or any interception of the coroutine context beyond that suffix.
The same section shows the recommended way to run work on a background thread on iOS, using Kotlin.coroutines as native-mt. You define a scope whose context is a dispatcher plus a Job plus a CoroutineExceptionHandler:
internal val mainScope = SharedScope(Dispatchers.Main)
internal val backgroundScope = SharedScope(Dispatchers.Default)
internal class SharedScope(private val context: CoroutineContext) : CoroutineScope {
private val job = Job()
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
println("[Coroutine Exception] $throwable")
}
override val coroutineContext: CoroutineContext
get() = context + job + exceptionHandler
}and then launch into it:
backgroundScope.launch {
suspendFunction()
}Note what the sample itself does with failures: the exception handler prints with println, not with Napier. That is a small sign of where the library sits. It formats and routes messages to platform loggers. It is not a crash reporting pipeline, and the coroutine sample keeps its error path outside of it.
One other detail worth planning around: the sample is described as using Kotlin.coroutines as native-mt, so on Apple targets the coroutine support is expected to be supplied in the multi threaded native form rather than the default one. That is a decision your project has to make, and the project frames it as a prerequisite rather than something Napier configures for you.
A custom Antilog is the only way to reach Crashlytics
The feature that makes Napier more than a pretty console wrapper is the ability to inject a custom Antilog, and the documented use case is crash reporting. The docs state that you can inject a custom Antilog and that you should change the Antilog in debug builds or release builds.
The Android sample selects between two implementations based on BuildConfig.DEBUG, and it toggles Crashlytics collection to match, because collecting crash data in a debug build makes local testing harder:
if (BuildConfig.DEBUG) {
// Debug build
// disable firebase crashlytics
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(false)
// init napier
Napier.base(DebugAntilog())
} else {
// Others(Release build)
// enable firebase crashlytics
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(true)
// init napier
Napier.base(CrashlyticsAntilog(this))
}On iOS the same split is done from the AppDelegate with preprocessor branches, and the release branch hands Crashlytics two closures, one for adding a log and one for sending a throwable:
#if DEBUG
// Debug build
// init napier
NapierProxyKt.debugBuild()
#else
// Others(Release build)
// init firebase crashlytics
FirebaseApp.configure()
// init napier
NapierProxyKt.releaseBuild(antilog: CrashlyticsAntilog(
crashlyticsAddLog: { priority, tag, message in
Crashlytics.crashlytics().log("\(String(describing: tag)): \(String(describing: message))")
},
crashlyticsSendLog: { throwable in
Crashlytics.crashlytics().record(error: throwable)
}))
#endifThe sample projects use Firebase Crashlytics and need authentication files at android/google-services.json and ios/Napier/GoogleService-Info.plist.
This design keeps the dependency direction clean. Napier owns the log call, the Antilog owns the destination, and Crashlytics never appears in your common code. The cost is that the Antilog interface becomes your integration surface, and the docs do not spell out that interface beyond these samples, so you are reading sample source rather than an API contract when you write your own destination.
Log levels are fixed to six calls, and message building is lazy
The level set is small and closed. The docs list six levels with the call that produces each one: VERBOSE is Napier.v(), DEBUG is Napier.d(), INFO is Napier.i(), WARNING is Napier.w(), ERROR is Napier.e() and ASSERT is Napier.wtf(). There is no trace level and no separate query or benchmark level, which is normal for a logger built on top of platform facilities that do not have one.
The message argument comes in two forms. A plain string is the common case, and a lambda form defers building the message until the call site runs, which matters when the message is expensive to compute:
// verbose log
Napier.v("Hello napier")
Napier.v { "Hello napier" }
// you can set a tag for each log
Napier.d("optional tag", tag = "your tag")
Napier.d(tag = "your tag") { "optional tag" }The same two forms exist for errors, where you can attach a throwable:
try {
...
} catch (e: Exception) {
// you can set the throwable
Napier.e("Napier Error", e)
Napier.e(e) { "Napier Error" }
}There is also a top level function that mirrors the object API:
// you can also use top-level function
log { "top-level" }
log(tag = "your tag") { "top-level" }What the documentation does not include is worth stating plainly. There is no documented per level toggle, no configuration object, and no documented way to route by level other than by choosing which call to make. A tag is accepted as a parameter, but the docs do not show filtering on it. If you need verbose logs off in release while keeping errors, the documented mechanism is to install a different Antilog for release, which means writing a destination that drops records.
The download docs still point at jCenter and a placeholder version
Two lines in the download section date this documentation. The first says the library can be downloaded from MavenCentral or jCenter, and the jCenter instruction is qualified with you can download this until 1.4.1. jCenter was read only and then shut down, so a reader following the Groovy snippet that calls jCenter() will not resolve the dependency. The Maven Central snippet is the one that works, and the group name there is io.github.aakira.
The second dated line is the version declaration, which the docs write as a placeholder string rather than a number. A reader has to look up the version elsewhere, and the releases listed for this project run from 2.6.1 through 2.7.1. The preview section also contains a broken image reference in the preview section, where the preview links resolve to a path fragment rather than a usable asset path.
Neither of these is a design problem, and the library itself is straightforward. But they suggest the documentation predates current Kotlin Multiplatform module naming and source set idioms. Expect to spend time reconciling the sample build files with the current convention, where the ios, macOS and watchOS directories follow the modern target layout rather than the shared mpp-sample arrangement shown in the repository listing.
The maintenance picture is worth stating as a date rather than an adjective. The repository is not archived, and the last push was on 2026-07-07. The three most recent tagged releases are 2.7.1 on 2024-01-04, 2.7.0 on 2023-12-30 and 2.6.1 on 2022-05-18. Release activity and push activity are not the same thing here, and a team that needs frequent releases of a logging layer should weigh the release history against the push date before committing to a pinned version. The licence is Apache-2.0, with the standard notice carrying a 2019 copyright for A.Akira, and the library is published as a normal Maven artifact rather than anything with copyleft obligations.
Editorial conclusion
Napier fits teams that want one call site in commonMain and will accept a global base Antilog plus a per platform logger, and it fails to fit projects that need structured JSON, per module level filtering, file rotation or a documented way to route logs off device. Before adopting, check the artifact coordinates against your own repository setup, confirm that io.github.aakira:napier resolves from Maven Central for the version you plan to pin, and read the Antilog interface to see whether your crash reporter can be plugged in the way the Crashlytics sample does.
Frequently asked questions
How do I add Napier to a Kotlin Multiplatform project?
Declare the dependency in your commonMain source set using the Maven Central coordinates io.github.aakira:napier, with a version you choose, as shown in the Groovy and Kotlin DSL examples. The jCenter repository and the older com.github.aakira group id only work up to version 1.4.1.
Why do my Napier logs not appear until I call Napier.base?
Napier has to be initialized in your module before any log call, and the documentation says so directly. Install a DebugAntilog with Napier.base(DebugAntilog()) at application start, on Android in your Application class and on iOS from the AppDelegate through the generated proxy.
Does Napier support Crashlytics on Android and iOS?
Yes, through a custom Antilog rather than a built in integration. The docs provide a CrashlyticsAntilog that you install with Napier.base in release builds, and shows the matching Swift branch for iOS where CrashlyticsAntilog takes closures for adding a log and recording a throwable.
How do I turn Napier logging off or clear the current logger?
Call Napier.takeLogarithm(), which the docs list as the way to clear the antilog. To change destination instead, install a different Antilog with Napier.base, which is how the debug and release branches of the Crashlytics sample differ.
What is the difference between Napier and Timber?
The author describes Napier as inspired by Timber. Napier keeps the same call shape in a common module and routes each record to the native logger of the current target, so Logcat on Android, print on Darwin, console.log in JavaScript and java.util.logging.Logger on the JVM.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/aakira-napier)Was this useful?