Kotlin Interfaces — Complete Guide with Examples
In this tutorial, you will learn about Kotlin Interfaces. We cover key concepts, practical examples, and best practices to help you master this topic.
Kotlin interfaces can declare abstract and default methods, abstract properties, and support multiple inheritance with Conflict Resolution through override and super with angle bracket syntax.
What You'll Learn
- Declare interfaces with abstract and default implementations
- Implement multiple interfaces in a single class
- Resolve method conflicts between interfaces
- Define abstract properties in interfaces
- Use functional (SAM) interfaces for single-method contracts
- Apply interface delegation with the by keyword
- Distinguish between interfaces and abstract classes
Why It Matters
Interfaces define contracts that classes can implement. Kotlin interfaces are more powerful than Java's because they can contain property declarations and default method implementations. Multiple interface inheritance allows a class to conform to multiple contracts without the diamond problem ambiguities. Interface delegation reduces boilerplate by automatically generating forwarding methods. Understanding interfaces is key to designing loosely coupled, testable systems.
Real-World Use
DodaTech's Android app uses interfaces for Repository patterns, where data sources implement Readable and Writable interfaces. The logger system defines a Loggable interface with default formatting. Multiple components (FileLogger, ConsoleLogger, CrashReporter) each implement Logger with specific behavior.
Learning Path
flowchart LR A[Inheritance] --> B[Interfaces\nYou are here] B --> C[Data Classes] style B fill:#f90,color:#fff
Interface Declaration
Interfaces are declared with the interface keyword. Methods can be abstract or have default implementations.
interface Drawable {
fun draw()
fun description(): String {
return "A drawable object"
}
}
interface Resizable {
fun resize(factor: Double)
val isResizable: Boolean
}
class Circle(val radius: Double) : Drawable, Resizable {
override fun draw() {
println("Drawing circle with radius $radius")
}
override fun resize(factor: Double) {
// Resize logic
}
override val isResizable: Boolean get() = true
override fun description(): String {
return "Circle with radius $radius"
}
}
fun main() {
val circle = Circle(5.0)
circle.draw() // Output: Drawing circle with radius 5.0
println(circle.description()) // Output: Circle with radius 5.0
println("Resizable: ${circle.isResizable}") // Output: Resizable: true
}
Output: Circle implements both Drawable and Resizable interfaces, providing concrete implementations for abstract members and optionally overriding default methods.
Properties in Interfaces
Interfaces can declare abstract properties. Implementing classes must override them.
interface Configurable {
val name: String
val timeout: Int
val retries: Int
get() = 3 // Default implementation
val fullDescription: String
get() = "$name (timeout=${timeout}s, retries=$retries)"
}
class DatabaseConfig(override val name: String) : Configurable {
override val timeout: Int = 30
// retries uses the default from the interface
}
class NetworkConfig(override val name: String) : Configurable {
override val timeout: Int = 10
override val retries: Int = 5 // Override the default
}
fun main() {
val dbConfig = DatabaseConfig("PostgreSQL")
println(dbConfig.fullDescription) // Output: PostgreSQL (timeout=30s, retries=3)
val netConfig = NetworkConfig("API Service")
println(netConfig.fullDescription) // Output: API Service (timeout=10s, retries=5)
}
Output: Interface properties can have default getters. Implementing classes override abstract properties but can use or override default implementations.
Interface properties cannot store state (no backing fields). All properties are either abstract or computed.
Multiple Interface Inheritance
A class can implement multiple interfaces. The compiler handles conflicts when two interfaces define the same method.
interface A {
fun method() = println("A's method")
fun another()
}
interface B {
fun method() = println("B's method")
fun extra()
}
class Impl : A, B {
// Must resolve conflict between A.method() and B.method()
override fun method() {
super<A>.method()
super<B>.method()
println("Impl's additional logic")
}
override fun another() = println("Implementing another")
override fun extra() = println("Implementing extra")
}
fun main() {
val impl = Impl()
impl.method()
impl.another()
impl.extra()
}
Output:
A's method
B's method
Impl's additional logic
Implementing another
Implementing extra
When two interfaces define the same method, the implementing class must override it. Inside the override, use super
SAM (Functional) Interfaces
A Single Abstract Method interface is a functional interface with one abstract method. Use fun interface keyword for SAM conversion.
fun interface ClickListener {
fun onClick(view: String)
}
// SAM conversion: lambda instead of anonymous class
fun interface StringTransformer {
fun transform(input: String): String
}
fun main() {
// Traditional anonymous object
val listener1 = object : ClickListener {
override fun onClick(view: String) {
println("Clicked on $view")
}
}
// SAM conversion with lambda
val listener2 = ClickListener { view ->
println("Lambda handling click on $view")
}
listener1.onClick("Button") // Output: Clicked on Button
listener2.onClick("Button") // Output: Lambda handling click on Button
// Another SAM example
val toUpper = StringTransformer { it.uppercase() }
val reverse = StringTransformer { it.reversed() }
println(toUpper.transform("hello")) // Output: HELLO
println(reverse.transform("hello")) // Output: olleh
// Using with collection
val numbers = listOf(1, 2, 3, 4, 5)
val predicate = fun interface Predicate {
fun test(x: Int): Boolean
}
val isEven = Predicate { it % 2 == 0 }
println(numbers.filter { isEven.test(it) }) // Output: [2, 4]
}
Output: SAM interfaces allow lambda syntax where anonymous classes were required in Java. This reduces boilerplate significantly.
Interface Delegation
The by keyword enables you to delegate interface implementation to another object.
interface Loggable {
fun log(message: String)
fun logError(message: String) {
log("ERROR: $message")
}
}
class ConsoleLogger : Loggable {
override fun log(message: String) {
println("[Console] $message")
}
}
class FileLogger : Loggable {
override fun log(message: String) {
println("[File] $message")
}
}
// Delegation: CustomLogger delegates Loggable to ConsoleLogger
class CustomLogger(logger: Loggable = ConsoleLogger()) : Loggable by logger {
override fun log(message: String) {
// Add timestamp before delegating
println("[${System.currentTimeMillis()}] Custom: $message")
// Call the delegate's implementation
}
}
fun main() {
val console = ConsoleLogger()
console.log("Hello") // Output: [Console] Hello
val custom = CustomLogger()
custom.log("Delegated") // Output with timestamp and "Custom: Delegated"
// Delegation to FileLogger
val fileLogger = FileLogger()
val customFile = CustomLogger(fileLogger)
customFile.logError("File not found") // Output with timestamp and "Custom: ERROR: File not found"
}
Output: The CustomLogger class implements Loggable by delegating all methods to the provided logger instance. Override specific methods to add behavior before or after delegation.
Interface versus Abstract Class
| Feature | Interface | Abstract Class |
|---|---|---|
| Constructor | No | Yes |
| State (backing fields) | No | Yes |
| Multiple inheritance | Yes | No |
| Access modifiers | No private members | All modifiers |
| Default methods | Yes | Yes |
| Instantiation | No | No |
Use interfaces when you define a contract (what something can do). Use abstract classes when you define a partial implementation with shared state.
Common Mistakes
Trying to store state in interfaces: Interface properties cannot have backing fields. All properties must be abstract or computed. Use an abstract class if you need mutable state.
Using interface where abstract class is appropriate: If multiple related classes share implementation code and state, use an abstract class. Interfaces are for contracts, not implementation sharing.
Not handling multiple inheritance conflicts: When two interfaces define the same method, the implementing class must override it. The compiler does not choose a default.
Forgetting fun interface for SAM conversion: Regular interfaces with one abstract method do not automatically support SAM conversion. Add fun before interface to enable lambda syntax.
Overusing interface delegation: Delegation adds indirection. Use it when the delegation relationship is natural (e.g., logger wrapping another logger).
Creating interfaces with too many methods: Interfaces should be focused (Interface Segregation Principle). Split large interfaces into smaller, role-specific ones.
Practice Questions
- Can an interface have default method implementations?
Answer: Yes. Interface methods can have default implementations. Implementing classes can use the default or override it.
- How do you resolve a conflict when two interfaces define the same method?
Answer: Override the conflicting method in the implementing class. Use super
- What is SAM conversion and when does it apply?
Answer: SAM conversion allows using a lambda instead of an anonymous class for interfaces with a single abstract method. The interface must be declared with fun interface.
- What is interface delegation with the by keyword?
Answer: The by keyword delegates all interface methods to a provided object. The class can override specific methods while delegating others.
- Challenge: Create a filtering system with interfaces. Define Filter, Sortable, and Paginable interfaces. Implement a DataProcessor class that uses interface delegation for filtering and sorting, and provides its own pagination logic.
Answer:
interface Filter<T> {
fun apply(items: List<T>): List<T>
}
interface Sortable<T> {
fun sort(items: List<T>): List<T>
}
class NameFilter(private val query: String) : Filter<String> {
override fun apply(items: List<String>): List<String> {
return items.filter { it.contains(query, ignoreCase = true) }
}
}
class AlphabeticalSort : Sortable<String> {
override fun sort(items: List<String>): List<String> {
return items.sorted()
}
}
class DataProcessor<T>(
private val filter: Filter<T>,
private val sorter: Sortable<T>,
private val pageSize: Int = 10
) : Filter<T> by filter, Sortable<T> by sorter {
fun process(items: List<T>, page: Int = 1): List<T> {
val filtered = filter.apply(items)
val sorted = sorter.sort(filtered)
val start = (page - 1) * pageSize
return sorted.drop(start).take(pageSize)
}
}
fun main() {
val items = listOf("Banana", "Apple", "Cherry", "apricot", "Blueberry")
val processor = DataProcessor(NameFilter("a"), AlphabeticalSort(), 3)
val result = processor.process(items)
println(result) // Output: [Apple, apricot, Banana]
}
Mini Project
Build a plugin system using interfaces. Requirements:
- Plugin interface with name(), init(), execute(), and cleanup() methods
- Default implementations for init() and cleanup()
- PluginMananger interface with register, unregister, and executeAll
- Use interface delegation for PluginManager
- Each plugin implements Plugin with custom execute logic
- Use SAM interfaces for simple single-method plugins
This project demonstrates interface design, default methods, delegation, and SAM conversion in a practical plugin architecture.
FAQ
What's Next
After learning interfaces, explore data classes for concise model objects. You can also learn about objects for singletons and anonymous classes.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro