Timber для Kotlin: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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. Советы по производи

Рекомендаций