Default implementations for interfaces
It all started as a crazy idea… during our Kotlin hour at Deliveroo we were talking about use cases for default implementations for interfaces. I threw in (jokingly): we could have a default implementation for all our methods in our base View
interface. I quickly tried it out and… it worked.
At first we thought: that’s crazy, right? We can’t actually do that, right? But still… there was something in there. The View
interface needs to be implement in various classes — Activity
, Fragment
, DialogFragment
etc. This gets tedious. Adding a new method means duplicating the code in all those classes. Even if it’s just calling another class which handles the actual implementation to avoid even more duplication.
But let’s backtrack a bit…
What’s a default implementation?
Kotlin interfaces are very similar to interfaces in Java 8. Sadly as an Android developer Java 8 features are only (partially) supported since the latest Android Studio 3.0 Preview.
Before Java 8 interfaces could only contain abstract methods. In Java 8 (and in Kotlin) they can also contain method implementations and — in Kotlin — properties. Interfaces cannot keep any state and properties need to either be abstract or provide accessor implementations, so they can be overridden.
Let’s see some code
So let’s take this simple interface:
interface View {
fun showToast(text: String)
}
Now for this interface we can create a default implementation:
interface DefaultView : View {
val context: Context
override fun showToast(text: String) {
Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
}
}
That’s all! You might’ve noticed that I have created a separate interface DefaultView
. The View
could also contain the implementation, but it makes it harder to test. All other View
interfaces can extend View
, but any class implementing it can implement DefaultView
directly, like this:
abstract class BaseActivity : Activity(), DefaultView {
override val context: Context
get() = this
}
The actual implementation is hidden in the interface — all we need to implement here is the getter for the Context
. Now this can also be an advantage — it’s not obvious where the implementation for View
is. In Android Studio that’s not a big problem — clicking the arrow next to the abstract method will show you all the implementations — which should now only be DefaultView
.
How does this work?
Let’s have a look at the decompiled bytecode for this.
public interface DefaultView extends View {
@NotNull
Context getContext();
void showToast(@NotNull String var1);
public static final class DefaultImpls {
public static void showToast(@NotNull DefaultView $this, String text) {
Intrinsics.checkParameterIsNotNull(text, "text");
Toast.makeText($this.getContext(), (CharSequence)text, 0).show();
}
}
}
This is what happens for the DefaultView
. There’s a new static class DefaultImpls
which contains a static showToast
method. The DefaultView
instance gets passed through. This way we can access other functions, or properties from the interface.
What about the Activity
?
public abstract class BaseActivity extends Activity implements DefaultView {
@NotNull
public Context getContext() {
return (Context)this;
}
public void showToast(@NotNull String text) {
Intrinsics.checkParameterIsNotNull(text, "text");
DefaultView.DefaultImpls.showToast(this, text);
}
}
The Activity
implements all methods from the interface and calls the static method on DefaultImpls
seen above. So that’s it! This basically is what I’d implement in the past — have a helper class that handles the actual logic, implement all methods of the interface and call the helper class. But now we don’t have to write all this code anymore.
There’s a couple of gotchas:
- Your
BaseActivity
needs to be written in Kotlin, otherwise any Java class extending it will need theDefaultView
methods implemented manually. - You need to be careful with the naming of the properties. In the example above the
context
property will clash with thegetContext()
method in theFragment
.
A default implementation probably shouldn’t be the default option. But if an interface needs to be implemented multiple times with exactly the same code it can be a good option to avoid code complication — and mistakes.