Logging in Kotlin with Companion Object

Trevor Hackman
3 min readOct 20, 2021

--

In Java, we have the static keyword.

In Kotlin, we have companion objects. While not exactly the same, variables and functions defined within a companion object are practically equivalent to static variables and static functions in Java.

But there is one difference I’d like to talk about. There is something clever, a bit magical even, that you can do with companion objects. They can extend classes and implement interfaces!

open class Tagger
interface Factory
class Foo {
companion object: Tagger(), Factory
}

This makes a sort of static inheritance possible. Our class Foo will be recognized as a Tagger and as a Factory. For example:

 val fooFactory: Factory = Foo // This works

Additionally, since our companion object extends Tagger, it will via inheritance gain the fields and functions of a Tagger. And then the kicker is that the outer class Foo will gain static access to those.

Now let’s explore how we can make use of companion object inheritance with a use case I’ve found, logging in Android.

Logging in Android

Logging with Android’s Log API consists of a log level, a tag, and a message. It may look something like this:

class Foo {

init {
Log.v(TAG, "Foo created") // Verbose-level log
}

fun foo() {
Log.d(TAG, "foo() called") // Debug-level log
}

companion object {
private const val TAG = "Foo"
}
}

In this convention, in every class a private val TAG is defined in the companion object equal to the class name to use as the tag in all logs. TAG is repetitively used in every log and depending on if the class already had a companion object or not, 3 or 1 lines were added for TAG. It’s annoying, it’s boilerplate. Let’s get rid of it with companion object inheritance.

class Foo {

init {
log.v("Foo created") // Verbose-level log
}

fun foo() {
log.d("Foo called") // Debug-level log
}

companion object: Tagger("Foo")
}

No more TAG in every log statement, and we only had to increase the line count by one if a companion object didn’t already exist, zero increase if it already did. And we still have the different log levels selectable in the same form.

If you need to log with some other API or are interested in this outside of Android, you can of course substitute or add to the code above.

Using Delegation

There’s a reason for the Tag interface. What if you want to use this with an object that already has a parent? Multiple inheritance is not allowed in Java or Kotlin, but we can get around that with delegation.

object FooController: Controller(), Tag by Tagger("FooController") {
...
}

Here FooController is already an object, so it can’t have a companion object, and it already extends the class Controller. But we can give it the exact same logging abilities through delegation i.e.Tag by Tagger("FooFactory").

Using Reflection

If you like reflection and aren’t scared of obfuscation messing up your class names and as a result your log tags, then you can use reflection to shorten the use-site further.

override val tag = javaClass.enclosingClass?.simpleName ?: javaClass.simpleName

Note that javaClass.enclosingClass?.simpleName is preferred over javaClass.simpleName so that if this is used on a companion object the name of the enclosing class is grabbed rather than the name of the companion object’s generated class. Yes, under the hood a companion object generates a class, a static nested class to be specific. This is the key difference between Kotlin’s companion object and Java’s static.

--

--

Trevor Hackman
Trevor Hackman

Written by Trevor Hackman

Hi, I am an Android developer.

No responses yet