refactor(logging): use Timber instead of Kermit

Android-only project, so Timber (the de-facto Android logging lib) fits better than the
KMP-oriented Kermit.

- Catalog: kermit -> timber (com.jakewharton.timber:timber 5.0.1).
- core:data: Ktor Logging bridged to Timber; safeCall logs via Timber.
- app: plant Timber.DebugTree() in debug only (buildConfig enabled for BuildConfig.DEBUG).
This commit is contained in:
2026-06-10 12:23:43 +02:00
parent b7ccf2fefa
commit 6a1842ae96
6 changed files with 28 additions and 11 deletions

View File

@@ -4,6 +4,13 @@ plugins {
alias(libs.plugins.architecture.koin) alias(libs.plugins.architecture.koin)
} }
android {
// Needed for BuildConfig.DEBUG (gating the Timber DebugTree).
buildFeatures {
buildConfig = true
}
}
dependencies { dependencies {
// :app is the only place modules are assembled and the dependency graph is wired. // :app is the only place modules are assembled and the dependency graph is wired.
implementation(project(":core:data")) implementation(project(":core:data"))
@@ -15,6 +22,8 @@ dependencies {
implementation(libs.bundles.lifecycle.compose) implementation(libs.bundles.lifecycle.compose)
// Material Components — required for the Material3 XML Activity theme. // Material Components — required for the Material3 XML Activity theme.
implementation(libs.material) implementation(libs.material)
// Logging — the DebugTree is planted here; other modules log via Timber's static API.
implementation(libs.timber)
androidTestImplementation(libs.androidx.compose.ui.test.junit4) androidTestImplementation(libs.androidx.compose.ui.test.junit4)
debugImplementation(libs.androidx.compose.ui.test.manifest) debugImplementation(libs.androidx.compose.ui.test.manifest)

View File

@@ -5,6 +5,7 @@ import com.example.architecture.core.data.di.coreDataModule
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
import timber.log.Timber
/** /**
* Single Koin entry point. Feature modules append their own `*DataModule` / `*PresentationModule` * Single Koin entry point. Feature modules append their own `*DataModule` / `*PresentationModule`
@@ -13,6 +14,12 @@ import org.koin.core.context.startKoin
class ArchitectureApp : Application() { class ArchitectureApp : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
// Plant Timber only in debug; release builds get no logs (swap in a crash-reporting tree).
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
startKoin { startKoin {
androidLogger() androidLogger()
androidContext(this@ArchitectureApp) androidContext(this@ArchitectureApp)

View File

@@ -18,5 +18,5 @@ android {
dependencies { dependencies {
implementation(project(":core:domain")) implementation(project(":core:domain"))
implementation(libs.kermit) implementation(libs.timber)
} }

View File

@@ -13,10 +13,10 @@ import io.ktor.client.request.setBody
import io.ktor.client.request.url import io.ktor.client.request.url
import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.HttpResponse
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
import timber.log.Timber
import java.net.UnknownHostException import java.net.UnknownHostException
import java.nio.channels.UnresolvedAddressException import java.nio.channels.UnresolvedAddressException
import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.cancellation.CancellationException
import co.touchlab.kermit.Logger as KermitLogger
suspend inline fun <reified Response : Any> HttpClient.get( suspend inline fun <reified Response : Any> HttpClient.get(
route: String, route: String,
@@ -65,17 +65,17 @@ suspend inline fun <reified T> safeCall(
return try { return try {
responseToResult(execute()) responseToResult(execute())
} catch (e: UnresolvedAddressException) { } catch (e: UnresolvedAddressException) {
KermitLogger.withTag("HttpClient").e(e) { "No internet (unresolved address)" } Timber.tag("HttpClient").e(e, "No internet (unresolved address)")
Result.Error(DataError.Network.NO_INTERNET) Result.Error(DataError.Network.NO_INTERNET)
} catch (e: UnknownHostException) { } catch (e: UnknownHostException) {
KermitLogger.withTag("HttpClient").e(e) { "No internet (unknown host)" } Timber.tag("HttpClient").e(e, "No internet (unknown host)")
Result.Error(DataError.Network.NO_INTERNET) Result.Error(DataError.Network.NO_INTERNET)
} catch (e: SerializationException) { } catch (e: SerializationException) {
KermitLogger.withTag("HttpClient").e(e) { "Serialization failure" } Timber.tag("HttpClient").e(e, "Serialization failure")
Result.Error(DataError.Network.SERIALIZATION) Result.Error(DataError.Network.SERIALIZATION)
} catch (e: Exception) { } catch (e: Exception) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
KermitLogger.withTag("HttpClient").e(e) { "Unknown network failure" } Timber.tag("HttpClient").e(e, "Unknown network failure")
Result.Error(DataError.Network.UNKNOWN) Result.Error(DataError.Network.UNKNOWN)
} }
} }

View File

@@ -10,12 +10,13 @@ import io.ktor.http.ContentType
import io.ktor.http.contentType import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import co.touchlab.kermit.Logger as KermitLogger import timber.log.Timber
import io.ktor.client.plugins.logging.Logger as KtorLogger import io.ktor.client.plugins.logging.Logger as KtorLogger
/** /**
* Builds the app's single [HttpClient]. The [engine] is injected so tests can pass a Ktor * Builds the app's single [HttpClient]. The [engine] is injected so tests can pass a Ktor
* `MockEngine` while production passes OkHttp (see `coreDataModule`). * `MockEngine` while production passes OkHttp (see `coreDataModule`). Ktor logging is bridged to
* Timber so all logs flow through one tree (planted in the Application).
*/ */
object HttpClientFactory { object HttpClientFactory {
fun create(engine: HttpClientEngine): HttpClient { fun create(engine: HttpClientEngine): HttpClient {
@@ -30,7 +31,7 @@ object HttpClientFactory {
install(Logging) { install(Logging) {
logger = object : KtorLogger { logger = object : KtorLogger {
override fun log(message: String) { override fun log(message: String) {
KermitLogger.withTag("HttpClient").d(message) Timber.tag("HttpClient").d(message)
} }
} }
level = LogLevel.ALL level = LogLevel.ALL

View File

@@ -29,7 +29,7 @@ ktor = "3.1.3"
coil = "3.1.0" coil = "3.1.0"
# Logging # Logging
kermit = "2.0.5" timber = "5.0.1"
# Material Components (Views renderer) # Material Components (Views renderer)
material = "1.12.0" material = "1.12.0"
@@ -110,7 +110,7 @@ coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil"
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" } coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }
# --- Logging --- # --- Logging ---
kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
# --- Testing --- # --- Testing ---
junit4 = { module = "junit:junit", version.ref = "junit4" } junit4 = { module = "junit:junit", version.ref = "junit4" }