Abstracting Logging in Multi-Module Android App

Oğuzhan Aslan
3 min readAug 9, 2023
Photo by Vidar Smits on Unsplash

In modern Android development, most projects are multi-module. Basically, developers build smaller pieces and then combine them to achieve the requirements of the project. Moreover, as suggested in the Android documentation, there are multiple types of modules that can be used in a project, such as feature modules, data modules, common modules, etc.

By nature of this structure, different modules have different dependencies and requirements. Some modules tend to be more dependent on the Android framework, while others are more independent. And, We want our modules to be as independent as possible in order to maximize their reusability. For example, domain modules are generally where we put our business logic, and they are preferred to be as pure as possible. I personally prefer my domain modules to be pure and independent, so that I can use them in different projects without any modification.

I probably don’t need to mention that logging is a very important part of the development process. It is a very useful tool for debugging and monitoring the application. But when we use a multi-module structure while also trying to keep our modules independent, we face some challenges. One of them is logging. Of course we can use our old friend `System.out.println()` but it is not a good practice. We can also use the Android Log class, but it is not a good practice.

Using the Android Log class in our domain modules will make them dependent on the Android framework. Other than that, not all projects use Logcat directly for logging; some may use other tools like Timber, Crashlytics, etc. So, using the direct project logging tool in our domain modules reduces the reusability of the modules.

So, what can we do? Simple: we can use the power of abstraction. We can define an interface for our project and then delegate all the details of logging to the project. In this way, our domain modules will be independent and pure. And, we can use the same domain modules in different projects without any modification. Let’s see how we can do that.

First we want to define an interface for our logging similar to Android Log class. We can define it in our domain module.

Then we can implement this interface in our app module.

class LogcatLogger: Logger {
override fun d(tag: String, msg: String) {
Log.d(tag, msg)
}

override fun e(tag: String, msg: String, throwable: Throwable?) {
Log.e(tag, msg, throwable)
}

override fun i(tag: String, msg: String) {
Log.i(tag, msg)
}

override fun w(tag: String, msg: String) {
Log.w(tag, msg)
}
}

But, we do not need to implement this by using logcat. We can use any logging tool we want. For example, we can use Timber.

class AppLogger : Logger {
override fun d(tag: String, msg: String) {
Timber.tag(tag).d(msg)
}

override fun e(tag: String, msg: String, throwable: Throwable?) {
Timber.tag(tag).e(throwable, msg)
}

override fun i(tag: String, msg: String) {
Timber.tag(tag).i(msg)
}

override fun w(tag: String, msg: String) {
Timber.tag(tag).w(msg)
}
}

Then we can use this interface in our domain module.

class UseCase(private val logger: Logger) {
fun doSomething() {
logger.d("UseCase", "doSomething")
}
}

And, we can provide the implementation of this interface in our app module by using Hilt.

@Module
@InstallIn(SingletonComponent::class)
object AppModule {

@Provides
fun provideLogger(): Logger {
return AppLogger()
}
}

And now, we have a more independent domain module. We can use this domain module in different projects without any modifications for logging. We can use any logging tool we want in our project. We can use Logcat, we can use Timber, we can use Crashlytics, etc. We can even use different logging tools in different modules.

Let me know what you think about this approach. Thanks for reading.

LinkedIn

Love you all.

Stay tune for upcoming blogs.

Take care.

--

--