Create boundaries over libraries before it is too late

Read on:

Medium

Recently, I’ve been working on migrating the Quotes project to a multiplatform. During the migration process, I faced a problem with Timber logging library, which happens to be a favorite among Android developers. Unfortunately it doesn’t support multiple platforms yet, so, I needed to find another logging library that would work for all platforms. The tricky part was that the project heavily depended on Timber, and library functions were spread throughout the codebase. At first, I thought I could simply replace Timber functions with the new library’s functions using a search and replace method in Android Studio. But then I realized that if the new library stopped being maintained, I would have to go through the entire code again to switch libraries. That’s when I understood the importance of wrapping or creating our own classes around libraries even if that library is written perfectly. By doing this, we can easily switch libraries in the future without making extensive changes to the codebase. This approach will keep the project clean and easier to maintain.

As Uncle Bob said, “Keep frameworks behind architectural boundaries”.

So these are the steps I did to create a Logger wrapper around the Timber class:

1. First step — Creating Logger interface

interface Logger {
//Should be called on Application start
fun initialize()

fun e(message: String?)
fun d(message: String?)
fun i(message: String?)
}

2. Second step — Creating Timber implementation of Logger class

class TimberLogger : Logger {
override fun initialize() {
Timber.plant(Timber.DebugTree())
}

override fun e(message: String?) {
Timber.e(message)
}

override fun d(message: String?) {
Timber.d(message)
}

override fun i(message: String?) {
Timber.i(message)
}
}

3. Third step — Delegating logging to Timber implementation

We can use kotlin delegation power and with just one line code we’ll delagate logging function to Timber.

object AppLogger : Logger by TimberLogger()

From now on, whenever we require logging, we will use the AppLogger for logging, which has the same functions as the Logger interface. If we want to switch from Timber to another logging library later on, we can easily do that. We just need to create a new Logger implementation for the chosen library and replace TimberLogger with the new logger, like AnotherLogger. This way, we can smoothly integrate different logging libraries in the future while keeping our logging capabilities.

//AnotherLogger has to implement Logger interface methods, 
//and call another logger library methods
object AppLogger : Logger by AnotherLogger()

4. Find and replace all Timber package imports and function calls

Using find and replace all (shortcut: ⌘ + Shift + R) in Android studio we’ll change Timber function calls with AppLogger function calls.

Replacing imports:

Replacing imports

Replacing function calls (You can use this regex “Timber\.[d]\(“ for replacing debug function calls for example).

Note: This will replace timber calls and package imports in all project. Make sure at the end you import timber package and implement timber methods in TimberLogger class correctly.

While working on the implementation, I faced an issue with the auto tag naming feature of Timber. By default, Timber generates a tag based on the class where it is called. Therefore, when logging from our wrapper class, it would use the tag name “TimberLogger,” which was not ideal.

To solve this problem, I looked at the Timber source code and found that it determines tag names using the stack trace. I needed to add the names of our wrapper classes to the fqcnIgnore list. This would ensure that the correct class names were used for tag generation, resulting in more meaningful logging.

How Timber generates tag using stacktrace

I implemented a potential solution in the form of a pull request. This solution allows you to add your wrapper class to the ignore list. By doing so, the Timber library will exclude your wrapper class from the automatic tag generation process. This modification ensures that the generated tag reflects the appropriate class name when logging from your wrapper class.

https://github.com/JakeWharton/timber/pull/485.

Until it is merged or accepted, I just copied function from Timber source code, and added my wrapper classes to the list.

[Read on Medium]