
Shared core logging API with formatter DSL, customizable formatting and tagging, caller-aware resolution, per-line levels, console/file loggers, and extensible logger interfaces.
LoKdroid is a Kotlin Multiplatform logging library with a shared core API for Android, desktop JVM, iOS, and Kotlin/JS in the browser, plus Android-specific extensions for file and Logcat-oriented logging.
LoKdroid, log functions, and builder DSL from common code.FormatterBuilder is available from shared code and formats logs on every target..kt file and line.Logcat; desktop JVM and iOS use formatted console output with emoji level markers; the browser routes each level to the matching console method.View KDoc
The repository uses flat Gradle project paths while keeping grouped folders on disk:
LoKdroid/
├── library/
│ ├── core/ -> Gradle module :core
│ └── domain/ -> Gradle module :domain
└── demoApp/
├── androidApp/ -> Gradle module :androidApp
├── desktopApp/ -> Gradle module :desktopApp
├── webApp/ -> Gradle module :webApp
├── sharedUI/ -> Gradle module :sharedUI
└── iosApp/ -> native iOS Xcode host app, not part of the Gradle build
Notes:
library/ and demoApp/ are directories for organization, not Gradle modules.:sharedUI contains the shared Compose Multiplatform screens used by the Android, desktop, iOS, and browser demo hosts.demoApp/iosApp is a native Xcode application that imports the sharedUI framework and hosts Compose UI through MainViewController.The published :core and :domain artifacts are compiled for Java 17, so consuming projects need
a JDK 17 toolchain and a matching jvmTarget / compileOptions setting.
Add LoKdroid to your project:
repositories {
mavenCentral()
}Kotlin DSL
dependencies {
implementation("io.github.nikolaykuts:lokdroid:0.2.0-alpha")
}Groovy
dependencies {
implementation 'io.github.nikolaykuts:lokdroid:0.2.0-alpha'
}For a Kotlin Multiplatform project, put the dependency in commonMain when you need the shared API there. FormatterBuilder is part of the shared API and can be used from code that targets Android, desktop JVM, and iOS. Android-specific implementations such as FileLogger, ConsoleAndFileLogger, RemoteLogger, and FileFormat are available only from androidMain.
LoKdroid works out of the box and does not require explicit initialization before logging starts. If you do nothing, it uses the default console logger, identity formatter, caller-based tag provider, and default message builder:
logI(message = "LoKdroid is ready")Call LoKdroid.initialize(...) only when you want to override those defaults.
androidApp application module from Android Studio../gradlew :desktopApp:run
demoApp/iosApp/iosApp.xcodeproj in Xcode and run the iosApp scheme../gradlew :webApp:jsBrowserDevelopmentRun and open the printed localhost address.The repository already contains a native iOS host app under demoApp/iosApp.
:sharedUI as the sharedUI framework.MainViewControllerKt.initializeLoKdroid().MainViewControllerKt.MainViewController().Minimal integration looks like this on the Swift side:
import SwiftUI
import sharedUI
@main
struct iosAppApp: App {
init() {
MainViewControllerKt.initializeLoKdroid()
}
var body: some Scene {
WindowGroup {
ComposeRootView()
}
}
}logV(message = "some message")
logD(message = "some message")
logI(message = "some message")
logW(message = "some message")
logE(message = "some message")val year: Int = currentDate.year
val month: Int = currentDate.monthValue
val dayOfWeek: java.time.DayOfWeek = currentDate.dayOfWeek
logI {
"Date:"()
"Year: $year"()
"Month: $month"()
"Day of the Week: $dayOfWeek"()
}Inside the IMessageBuilder DSL, the string extension function accepts an optional Level.
The default MultipleLineMessageBuilder uses that value to prepend an emoji marker for the corresponding line.
Shortcuts available inside the builder:
V for Level.Verbose
D for Level.Debug
I for Level.Info
W for Level.Warn
E for Level.Error
logV {
"multi log"()
"some Error"(E)
"some Info"(I)
"some Debug"(D)
"some Verbose"(V)
"some Warn"(W)
}When used with the FormatterBuilder configuration from the next section, the output can look like this:
DesktopMain [Verbose] ⬜ ---> DesktopMainKt.invoke(DesktopMain.kt:24) multi log
🟥 -> some Error
🟩 -> some Info
🟦 -> some Debug
⬜ -> some Verbose
🟨 -> some Warn some custom text
FormatterBuilder is shared and can be configured the same way on every target by passing the built IFormatter into LoKdroid.initialize.
Platform behavior for withLineReference():
MainScreen.kt:42
MainScreen.onClick(MainScreen.kt:42)
SharedUiScreensKt.onClick(SharedUiScreens.kt:77)
Before formatting the line reference, LoKdroid skips its own internal wrapper frames such as LoKdroid, LogManager, and LogFunctions*, then uses the first external frame that remains.
LoKdroid.initialize(
formatter = FormatterBuilder()
.withPointer() // adds "--->"
.space() // adds a space
.withLineReference() // Android: File.kt:123, Desktop/iOS: Class.method(File.kt:123)
.space()
.message() // MANDATORY: injects your original log message
.space()
.custom(text = "some custom text") // appends any custom text
.build()
)In the browser a stack frame points into the bundled script, for example webApp.js:26276:7. LoKdroid
translates that position back to Kotlin by reading the source map the script declares for itself, so the
reference names the original file and line the log was written on:
SharedUiScreens [Debug] 🟦 ---> SharedUiScreensKt.onClick(SharedUiScreens.kt:78) first
This requires the bundle's source map to be reachable over HTTP next to the script, which means the
webpack devtool has to emit a real .map file rather than inline modules through eval:
import org.jetbrains.kotlin.gradle.targets.js.webpack.WebpackDevtool
kotlin {
js(IR) {
browser {
commonWebpackConfig {
devtool = WebpackDevtool.NOSOURCES_SOURCE_MAP
}
}
binaries.executable()
}
}nosources-source-map is the recommended setting: LoKdroid only reads the file and line mappings, so
leaving the Kotlin sources out of the map keeps it smaller and keeps your code out of the shipped map.
Use WebpackDevtool.SOURCE_MAP instead when you also want browser developer tools to display and step
through the original .kt files, which needs the sources embedded.
Notes:
eval-source-map, whose positions no fetchable source map
describes. LoKdroid never guesses a Kotlin line in that case. Because that devtool keeps every Kotlin
module as its own script, the caller is still identified and reported against the generated file, as
in SharedUiDemoApp(LoKdroid-sharedUI.js:226), with the tag falling back to the module name.???? tag and at ???(Unknown Source) instead of a guess.ConsoleLogger writes through the platform logging system, so logs appear in Logcat with Android priorities.ConsoleLogger write formatted lines to standard output using the pattern Tag<TAB>[Level] emoji message.ConsoleLogger writes the same pattern to the browser console, routing Verbose and Debug to console.log, Info to console.info, Warn to console.warn, and Error to console.error.⬜, 🟦, 🟩, 🟨, 🟥.tagProvider derives the tag from the caller file name without the .kt extension when source metadata is available.Desktop and iOS example output:
SharedUiScreens [Debug] 🟦 ---> SharedUiScreensKt.invokeMultipleLog(SharedUiScreens.kt:293) Multiple log
LoKdroid starts with this configuration automatically, even if initialize(...) is never called:
fun initialize(
minLevel: Level = Level.Verbose,
logger: ILogger = ConsoleLogger,
formatter: IFormatter = IFormatter { message -> message },
tagProvider: () -> String = { TagProvider.getTag() },
multipleLineMessageBuilderFactory: () -> IMessageBuilder = { MultipleLineMessageBuilder() }
)Calling LoKdroid.initialize(...) later simply replaces the current configuration with your custom one.
Available implementations:
ConsoleLogger is available on Android, desktop JVM, iOS, and the browser.FileLogger is Android-only and writes messages to a file. It supports Android Studio Logcat import format.ConsoleAndFileLogger is Android-only.RemoteLogger is currently Android-only./** custom implementation */
LoKdroid.initialize(
minLevel = Level.Debug,
logger = { level: Level, tag: String, message: String -> /** your logic */ },
formatter = { message -> "return formatted message: $message" },
tagProvider = { "custom tag" },
multipleLineMessageBuilderFactory = {
object : IMessageBuilder {
override fun build(): String = "build your string"
override operator fun String.invoke(level: Level?) {
// use this block to build multiple message lines
}
}
}
)Platform visibility:
commonMain sees the shared API, including LoKdroid, log functions, ILogger, IFormatter, IMessageBuilder, ConsoleLogger, and FormatterBuilder.androidMain also sees Android-only implementations such as FileLogger, ConsoleAndFileLogger, RemoteLogger, and FileFormat.desktopMain, iosMain, and jsMain use the same shared API surface and platform-specific console behavior under the hood.Console defaults:
SharedUiScreens.SharedUiScreensKt.invoke(...).tagProvider.This project is licensed under the Apache License, Version 2.0. See the LICENSE file for more details.
LoKdroid is a Kotlin Multiplatform logging library with a shared core API for Android, desktop JVM, iOS, and Kotlin/JS in the browser, plus Android-specific extensions for file and Logcat-oriented logging.
LoKdroid, log functions, and builder DSL from common code.FormatterBuilder is available from shared code and formats logs on every target..kt file and line.Logcat; desktop JVM and iOS use formatted console output with emoji level markers; the browser routes each level to the matching console method.View KDoc
The repository uses flat Gradle project paths while keeping grouped folders on disk:
LoKdroid/
├── library/
│ ├── core/ -> Gradle module :core
│ └── domain/ -> Gradle module :domain
└── demoApp/
├── androidApp/ -> Gradle module :androidApp
├── desktopApp/ -> Gradle module :desktopApp
├── webApp/ -> Gradle module :webApp
├── sharedUI/ -> Gradle module :sharedUI
└── iosApp/ -> native iOS Xcode host app, not part of the Gradle build
Notes:
library/ and demoApp/ are directories for organization, not Gradle modules.:sharedUI contains the shared Compose Multiplatform screens used by the Android, desktop, iOS, and browser demo hosts.demoApp/iosApp is a native Xcode application that imports the sharedUI framework and hosts Compose UI through MainViewController.The published :core and :domain artifacts are compiled for Java 17, so consuming projects need
a JDK 17 toolchain and a matching jvmTarget / compileOptions setting.
Add LoKdroid to your project:
repositories {
mavenCentral()
}Kotlin DSL
dependencies {
implementation("io.github.nikolaykuts:lokdroid:0.2.0-alpha")
}Groovy
dependencies {
implementation 'io.github.nikolaykuts:lokdroid:0.2.0-alpha'
}For a Kotlin Multiplatform project, put the dependency in commonMain when you need the shared API there. FormatterBuilder is part of the shared API and can be used from code that targets Android, desktop JVM, and iOS. Android-specific implementations such as FileLogger, ConsoleAndFileLogger, RemoteLogger, and FileFormat are available only from androidMain.
LoKdroid works out of the box and does not require explicit initialization before logging starts. If you do nothing, it uses the default console logger, identity formatter, caller-based tag provider, and default message builder:
logI(message = "LoKdroid is ready")Call LoKdroid.initialize(...) only when you want to override those defaults.
androidApp application module from Android Studio../gradlew :desktopApp:run
demoApp/iosApp/iosApp.xcodeproj in Xcode and run the iosApp scheme../gradlew :webApp:jsBrowserDevelopmentRun and open the printed localhost address.The repository already contains a native iOS host app under demoApp/iosApp.
:sharedUI as the sharedUI framework.MainViewControllerKt.initializeLoKdroid().MainViewControllerKt.MainViewController().Minimal integration looks like this on the Swift side:
import SwiftUI
import sharedUI
@main
struct iosAppApp: App {
init() {
MainViewControllerKt.initializeLoKdroid()
}
var body: some Scene {
WindowGroup {
ComposeRootView()
}
}
}logV(message = "some message")
logD(message = "some message")
logI(message = "some message")
logW(message = "some message")
logE(message = "some message")val year: Int = currentDate.year
val month: Int = currentDate.monthValue
val dayOfWeek: java.time.DayOfWeek = currentDate.dayOfWeek
logI {
"Date:"()
"Year: $year"()
"Month: $month"()
"Day of the Week: $dayOfWeek"()
}Inside the IMessageBuilder DSL, the string extension function accepts an optional Level.
The default MultipleLineMessageBuilder uses that value to prepend an emoji marker for the corresponding line.
Shortcuts available inside the builder:
V for Level.Verbose
D for Level.Debug
I for Level.Info
W for Level.Warn
E for Level.Error
logV {
"multi log"()
"some Error"(E)
"some Info"(I)
"some Debug"(D)
"some Verbose"(V)
"some Warn"(W)
}When used with the FormatterBuilder configuration from the next section, the output can look like this:
DesktopMain [Verbose] ⬜ ---> DesktopMainKt.invoke(DesktopMain.kt:24) multi log
🟥 -> some Error
🟩 -> some Info
🟦 -> some Debug
⬜ -> some Verbose
🟨 -> some Warn some custom text
FormatterBuilder is shared and can be configured the same way on every target by passing the built IFormatter into LoKdroid.initialize.
Platform behavior for withLineReference():
MainScreen.kt:42
MainScreen.onClick(MainScreen.kt:42)
SharedUiScreensKt.onClick(SharedUiScreens.kt:77)
Before formatting the line reference, LoKdroid skips its own internal wrapper frames such as LoKdroid, LogManager, and LogFunctions*, then uses the first external frame that remains.
LoKdroid.initialize(
formatter = FormatterBuilder()
.withPointer() // adds "--->"
.space() // adds a space
.withLineReference() // Android: File.kt:123, Desktop/iOS: Class.method(File.kt:123)
.space()
.message() // MANDATORY: injects your original log message
.space()
.custom(text = "some custom text") // appends any custom text
.build()
)In the browser a stack frame points into the bundled script, for example webApp.js:26276:7. LoKdroid
translates that position back to Kotlin by reading the source map the script declares for itself, so the
reference names the original file and line the log was written on:
SharedUiScreens [Debug] 🟦 ---> SharedUiScreensKt.onClick(SharedUiScreens.kt:78) first
This requires the bundle's source map to be reachable over HTTP next to the script, which means the
webpack devtool has to emit a real .map file rather than inline modules through eval:
import org.jetbrains.kotlin.gradle.targets.js.webpack.WebpackDevtool
kotlin {
js(IR) {
browser {
commonWebpackConfig {
devtool = WebpackDevtool.NOSOURCES_SOURCE_MAP
}
}
binaries.executable()
}
}nosources-source-map is the recommended setting: LoKdroid only reads the file and line mappings, so
leaving the Kotlin sources out of the map keeps it smaller and keeps your code out of the shipped map.
Use WebpackDevtool.SOURCE_MAP instead when you also want browser developer tools to display and step
through the original .kt files, which needs the sources embedded.
Notes:
eval-source-map, whose positions no fetchable source map
describes. LoKdroid never guesses a Kotlin line in that case. Because that devtool keeps every Kotlin
module as its own script, the caller is still identified and reported against the generated file, as
in SharedUiDemoApp(LoKdroid-sharedUI.js:226), with the tag falling back to the module name.???? tag and at ???(Unknown Source) instead of a guess.ConsoleLogger writes through the platform logging system, so logs appear in Logcat with Android priorities.ConsoleLogger write formatted lines to standard output using the pattern Tag<TAB>[Level] emoji message.ConsoleLogger writes the same pattern to the browser console, routing Verbose and Debug to console.log, Info to console.info, Warn to console.warn, and Error to console.error.⬜, 🟦, 🟩, 🟨, 🟥.tagProvider derives the tag from the caller file name without the .kt extension when source metadata is available.Desktop and iOS example output:
SharedUiScreens [Debug] 🟦 ---> SharedUiScreensKt.invokeMultipleLog(SharedUiScreens.kt:293) Multiple log
LoKdroid starts with this configuration automatically, even if initialize(...) is never called:
fun initialize(
minLevel: Level = Level.Verbose,
logger: ILogger = ConsoleLogger,
formatter: IFormatter = IFormatter { message -> message },
tagProvider: () -> String = { TagProvider.getTag() },
multipleLineMessageBuilderFactory: () -> IMessageBuilder = { MultipleLineMessageBuilder() }
)Calling LoKdroid.initialize(...) later simply replaces the current configuration with your custom one.
Available implementations:
ConsoleLogger is available on Android, desktop JVM, iOS, and the browser.FileLogger is Android-only and writes messages to a file. It supports Android Studio Logcat import format.ConsoleAndFileLogger is Android-only.RemoteLogger is currently Android-only./** custom implementation */
LoKdroid.initialize(
minLevel = Level.Debug,
logger = { level: Level, tag: String, message: String -> /** your logic */ },
formatter = { message -> "return formatted message: $message" },
tagProvider = { "custom tag" },
multipleLineMessageBuilderFactory = {
object : IMessageBuilder {
override fun build(): String = "build your string"
override operator fun String.invoke(level: Level?) {
// use this block to build multiple message lines
}
}
}
)Platform visibility:
commonMain sees the shared API, including LoKdroid, log functions, ILogger, IFormatter, IMessageBuilder, ConsoleLogger, and FormatterBuilder.androidMain also sees Android-only implementations such as FileLogger, ConsoleAndFileLogger, RemoteLogger, and FileFormat.desktopMain, iosMain, and jsMain use the same shared API surface and platform-specific console behavior under the hood.Console defaults:
SharedUiScreens.SharedUiScreensKt.invoke(...).tagProvider.This project is licensed under the Apache License, Version 2.0. See the LICENSE file for more details.