Timber для Kotlin: полное руководство с примерами
1. Что это и зачем нужна
Timber — это легковесная библиотека для логирования в Android-приложениях, написанная на Kotlin. Она была создана Джейком Уортоном (Jake Wharton) — одним из ведущих Android-разработчиков, известным по таким проектам, как Retrofit, OkHttp и Dagger. Основная цель Timber — избавить разработчика от шаблонного кода при логировании и предоставить гибкий механизм для управления логами на разных этапах разработки.
Зачем нужна Timber, если есть стандартный android.util.Log? Во-первых, Timber автоматически добавляет имя класса и метода в каждый лог, что делает трассировку значительно удобнее. Вам больше не нужно писать Log.d(TAG, "message") — достаточно вызвать Timber.d("message"). Во-вторых, Timber предоставляет систему "деревьев" (Trees), которая позволяет разделять логи по разным каналам: например, в debug-сборке выводить всё в Logcat, а в release-сборке отправлять только ошибки в Crashlytics или другой сервис аналитики.
Третье важное преимущество — Timber автоматически отключает все логи в release-сборке, если вы не настроили специальное дерево для продакшена. Это не только повышает производительность, но и предотвращает утечку чувствительных данных через логи. Библиотека настолько популярна, что используется в тысячах коммерческих проектов и рекомендуется Google в официальных руководствах по Android-разработке.
2. Установка
Timber распространяется через Maven Central. Для установки добавьте зависимость в файл build.gradle (уровень модуля):
dependencies {
implementation 'com.jakewharton.timber:timber:5.0.1'
}
Если вы используете Kotlin DSL (build.gradle.kts):
dependencies {
implementation("com.jakewharton.timber:timber:5.0.1")
}
После синхронизации Gradle библиотека готова к использованию. Обратите внимание: Timber не требует дополнительных разрешений в AndroidManifest.xml.
3. Быстрый старт — минимальный рабочий пример
Для начала работы необходимо "посадить дерево" (plant a tree) — зарегистрировать обработчик логов. Самый простой способ — использовать встроенное дерево Timber.DebugTree, которое выводит логи в Logcat с уровнем DEBUG и выше:
import timber.log.Timber
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
}
}
Теперь в любом месте приложения можно писать логи:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Timber.d("Activity created")
Timber.i("User opened main screen")
Timber.w("Something suspicious happened")
Timber.e("Error occurred: %s", "Network timeout")
}
}
В Logcat вы увидите примерно следующее:
D/MainActivity: Activity created
I/MainActivity: User opened main screen
W/MainActivity: Something suspicious happened
E/MainActivity: Error occurred: Network timeout
Обратите внимание: Timber автоматически определил имя класса (MainActivity) и добавил его в тег. Вам не нужно вручную создавать константу TAG.
4. Основные методы/функции/классы
4.1. Timber.d() — Debug-логирование
Сигнатура: fun d(message: String, vararg args: Any?)
Что делает: Выводит отладочное сообщение с уровнем DEBUG. Используется для временных логов в процессе разработки. В release-сборке по умолчанию не выводится.
Timber.d("User clicked button with id: %d", buttonId)
Timber.d("Current state: $state")
4.2. Timber.i() — Информационное логирование
Сигнатура: fun i(message: String, vararg args: Any?)
Что делает: Выводит информационное сообщение с уровнем INFO. Подходит для логирования ключевых событий в приложении (например, загрузка данных, переходы между экранами).
Timber.i("Data loaded successfully: %d items", items.size)
Timber.i("User navigated to profile screen")
4.3. Timber.w() — Предупреждения
Сигнатура: fun w(message: String, vararg args: Any?)
Что делает: Выводит предупреждение с уровнем WARN. Используется для ситуаций, которые не являются ошибками, но требуют внимания (например, устаревшие API, неожиданные значения).
Timber.w("Deprecated method called: %s", methodName)
Timber.w("Cache miss for key: $key")
4.4. Timber.e() — Ошибки
Сигнатура: fun e(message: String, vararg args: Any?) и fun e(throwable: Throwable?, message: String, vararg args: Any?)
Что делает: Выводит сообщение об ошибке с уровнем ERROR. Первая версия принимает только строку, вторая — дополнительно объект Throwable для логирования стека вызовов.
Timber.e("Failed to parse JSON")
Timber.e(exception, "Network error while fetching user data")
4.5. Timber.wtf() — Фатальные ошибки
Сигнатура: fun wtf(message: String, vararg args: Any?) и fun wtf(throwable: Throwable?, message: String, vararg args: Any?)
Что делает: Выводит сообщение с уровнем ASSERT (What a Terrible Failure). Используется для критических ошибок, после которых приложение не может продолжать работу.
Timber.wtf("Critical system failure: null pointer in core module")
Timber.wtf(exception, "Unrecoverable error in database layer")
4.6. Timber.tag() — Кастомный тег
Сигнатура: fun tag(tag: String): Timber.Tree
Что делает: Позволяет временно переопределить тег для следующего лог-сообщения. Возвращает объект дерева, к которому можно применить любой метод логирования.
Timber.tag("NetworkLayer").d("Request sent to: %s", url)
Timber.tag("Auth").i("User authenticated successfully")
4.7. Timber.plant() — Регистрация дерева
Сигнатура: fun plant(tree: Timber.Tree)
Что делает: Регистрирует новое дерево для обработки логов. Можно зарегистрировать несколько деревьев — логи будут отправляться во все зарегистрированные деревья.
Timber.plant(Timber.DebugTree())
Timber.plant(CrashReportingTree())
4.8. Timber.uproot() — Удаление дерева
Сигнатура: fun uproot(tree: Timber.Tree)
Что делает: Удаляет ранее зарегистрированное дерево. Полезно, если нужно временно отключить определенный канал логирования.
val debugTree = Timber.DebugTree()
Timber.plant(debugTree)
// ... некоторое время логируем ...
Timber.uproot(debugTree)
4.9. Timber.asTree() — Получение текущего дерева
Сигнатура: fun asTree(): Timber.Tree
Что делает: Возвращает текущее активное дерево. Полезно, если нужно передать логгер в другой класс или сохранить ссылку на него.
val logger = Timber.asTree()
logger.d("Logging through saved reference")
4.10. Timber.Tree — Базовый класс для создания кастомных деревьев
Сигнатура: abstract class Tree
Что делает: Абстрактный класс, который нужно наследовать для создания собственного обработчика логов. Основной метод для переопределения — log(priority: Int, tag: String?, message: String, t: Throwable?).
class FileLoggingTree : Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
val logEntry = "[${getPriorityString(priority)}] $tag: $message"
writeToFile(logEntry)
}
private fun getPriorityString(priority: Int): String = when (priority) {
Log.VERBOSE -> "VERBOSE"
Log.DEBUG -> "DEBUG"
Log.INFO -> "INFO"
Log.WARN -> "WARN"
Log.ERROR -> "ERROR"
Log.ASSERT -> "ASSERT"
else -> "UNKNOWN"
}
}
5. Реальный практический пример (100-200 строк кода)
Рассмотрим полноценный пример использования Timber в Android-приложении для загрузки и отображения списка пользователей. Мы создадим кастомное дерево для отправки ошибок в Firebase Crashlytics, настроим логирование в зависимости от сборки и реализуем ViewModel с логированием ключевых событий.
// 1. Кастомное дерево для Crashlytics
class CrashlyticsTree : Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
if (priority == Log.ERROR || priority == Log.ASSERT) {
if (t != null) {
FirebaseCrashlytics.getInstance().recordException(t)
} else {
FirebaseCrashlytics.getInstance().log(message)
}
}
}
}
// 2. Application класс с настройкой Timber
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
// В debug-сборке: логи в Logcat + Crashlytics для ошибок
Timber.plant(Timber.DebugTree())
Timber.plant(CrashlyticsTree())
} else {
// В release-сборке: только Crashlytics для ошибок
Timber.plant(CrashlyticsTree())
}
Timber.i("Application started, build type: ${BuildConfig.BUILD_TYPE}")
}
}
// 3. Data класс для пользователя
data class User(
val id: Long,
val name: String,
val email: String
)
// 4. Репозиторий с логированием
class UserRepository {
suspend fun fetchUsers(): List {
Timber.tag("Repository").d("Starting to fetch users")
return try {
val response = apiService.getUsers()
Timber.tag("Repository").i("Successfully fetched %d users", response.size)
response
} catch (e: Exception) {
Timber.tag("Repository").e(e, "Failed to fetch users")
throw e
}
}
}
// 5. ViewModel с детальным логированием
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
private val _users = MutableLiveData>()
val users: LiveData> = _users
private val _isLoading = MutableLiveData()
val isLoading: LiveData = _isLoading
private val _error = MutableLiveData()
val error: LiveData = _error
fun loadUsers() {
Timber.i("loadUsers called")
_isLoading.value = true
viewModelScope.launch {
try {
val result = repository.fetchUsers()
_users.value = result
Timber.d("Users loaded: %d items", result.size)
} catch (e: Exception) {
Timber.e(e, "Error loading users")
_error.value = "Failed to load users: ${e.localizedMessage}"
} finally {
_isLoading.value = false
Timber.d("loadUsers completed")
}
}
}
fun refreshUsers() {
Timber.w("Manual refresh triggered")
loadUsers()
}
override fun onCleared() {
super.onCleared()
Timber.d("UserViewModel cleared")
}
}
// 6. Activity с UI и логированием жизненного цикла
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: UserViewModel
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
Timber.d("MainActivity onCreate")
viewModel = ViewModelProvider(this)[UserViewModel::class.java]
observeViewModel()
setupListeners()
viewModel.loadUsers()
}
private fun observeViewModel() {
viewModel.users.observe(this) { users ->
Timber.d("Observing users update: %d items", users.size)
binding.recyclerView.adapter = UserAdapter(users)
}
viewModel.isLoading.observe(this) { loading ->
Timber.d("Loading state changed to: $loading")
binding.progressBar.isVisible = loading
}
viewModel.error.observe(this) { errorMessage ->
Timber.w("Error observed: $errorMessage")
Snackbar.make(binding.root, errorMessage, Snackbar.LENGTH_LONG).show()
}
}
private fun setupListeners() {
binding.refreshButton.setOnClickListener {
Timber.tag("UI").i("Refresh button clicked")
viewModel.refreshUsers()
}
}
override fun onResume() {
super.onResume()
Timber.d("MainActivity onResume")
}
override fun onPause() {
super.onPause()
Timber.d("MainActivity onPause")
}
override fun onDestroy() {
super.onDestroy()
Timber.d("MainActivity onDestroy")
}
}
// 7. Пример использования Timber.tag() для модульного логирования
class NetworkInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
Timber.tag("HTTP").d("--> %s %s", request.method, request.url)
val response = chain.proceed(request)
Timber.tag("HTTP").d("<-- %d %s", response.code, response.message)
return response
}
}
6. Советы по производи
Также в библиотеке
WireMock.Net для C# (.NET): полное руководство с примерами
Интеграция Prometheus метрик в PHP: библиотеки и примеры
Swagger/OpenAPI генерация в Kotlin: полный гайд по инструментам
GraphQL Java: мощный инструмент для создания современных API на Java
Работа с изображениями в Go: resize и crop с библиотекой imaging