Popular Searches
Popular Course Categories
Popular Courses

Working with Interfaces and Abstract Classes

Working with Interfaces and Abstract Classes

Object-Oriented Kotlin and Advanced Language Features

Working with Interfaces and Abstract Classes in Android App Development

Course: Android App Development

Topic: Working with Interfaces and Abstract Classes

Course Link: Android App Development Training

Demo Link: Register for a Free Course Demo

Website: JustAcademy


1. Introduction

Interfaces and abstract classes are important object-oriented programming concepts used in Android development. They help developers create reusable, flexible, maintainable, and testable applications. Both concepts define common behavior, but they serve different purposes.

An interface describes a contract that a class agrees to implement. An abstract class provides a common base that can contain both implemented behavior and declarations that subclasses must complete.

In Android applications, interfaces are commonly used for click listeners, callbacks, repository contracts, navigation events, and communication between components. Abstract classes are useful when multiple related classes share common state or implementation.

2. Learning Objectives

  • Understand interfaces and abstract classes in Kotlin.
  • Learn how to declare and implement interfaces.
  • Understand abstract properties, methods, and classes.
  • Compare interfaces with abstract classes.
  • Apply these concepts to Android event handling and application architecture.
  • Understand inheritance, polymorphism, and multiple interface implementation.
  • Identify common errors and follow best practices.

3. Understanding Interfaces

An interface defines a set of properties and functions that implementing classes must provide where required. It specifies what an object can do without necessarily defining how every operation is performed.

In Kotlin, the interface keyword is used to declare an interface.

Example: Declaring an Interface

interface NotificationService {
    fun sendNotification(message: String)
}

In this example, NotificationService defines a contract. Any concrete class implementing this interface must provide an implementation of sendNotification().

Implementing the Interface

class EmailNotificationService : NotificationService {
    override fun sendNotification(message: String) {
        println("Sending email: $message")
    }
}

class SmsNotificationService : NotificationService {
    override fun sendNotification(message: String) {
        println("Sending SMS: $message")
    }
}

fun main() {
    val emailService: NotificationService =
        EmailNotificationService()

    val smsService: NotificationService =
        SmsNotificationService()

    emailService.sendNotification("Welcome!")
    smsService.sendNotification("Your code is 1234")
}

Both classes implement the same contract but perform the operation differently. This is an example of polymorphism.

4. Important Features of Interfaces in Kotlin

  • Contract-based design: Interfaces define operations that implementing classes should support.
  • Multiple implementation: A class can implement multiple interfaces.
  • Abstract members: Interface functions and properties can be declared without implementations.
  • Default implementations: Interfaces can contain functions with method bodies.
  • Loose coupling: Code can depend on an interface rather than a specific implementation.
  • Testability: Interfaces make it easier to replace real services with fake or mock implementations during testing.

5. Interface Properties and Default Methods

Kotlin interfaces can declare properties and provide default method implementations. An interface property does not store its own backing field; implementing classes must provide the required property behavior.

interface AppUser {
    val username: String

    fun displayWelcome() {
        println("Welcome, $username")
    }
}

class AndroidUser(
    override val username: String
) : AppUser

fun main() {
    val user = AndroidUser("Aarav")
    user.displayWelcome()
}

The displayWelcome() function already has an implementation in the interface. The AndroidUser class only needs to provide the required username property.

6. Understanding Abstract Classes

An abstract class is a class that cannot be instantiated directly. It is designed to serve as a base class for related subclasses. It can contain abstract members without implementations as well as concrete members with working implementations.

In Kotlin, the abstract keyword is used to define an abstract class or member.

Example: Abstract Class

abstract class BaseScreen {
    abstract fun loadContent()

    fun showLoadingMessage() {
        println("Loading screen...")
    }
}

class HomeScreen : BaseScreen() {
    override fun loadContent() {
        println("Loading home screen content")
    }
}

fun main() {
    val screen = HomeScreen()
    screen.showLoadingMessage()
    screen.loadContent()
}

The BaseScreen class defines a common operation and a required operation. The subclass supplies the implementation of loadContent().

7. Abstract Properties and Methods

Abstract members declare behavior or data that a concrete subclass must implement. They do not provide a complete implementation in the abstract declaration.

abstract class PaymentMethod {
    abstract val methodName: String

    abstract fun pay(amount: Double)

    fun printReceipt(amount: Double) {
        println("$methodName payment: $amount")
    }
}

class CardPayment : PaymentMethod() {
    override val methodName = "Card"

    override fun pay(amount: Double) {
        println("Processing card payment of $amount")
    }
}

The subclass provides the payment method name and implements the payment operation. It also inherits the common receipt function.

8. Interface vs Abstract Class

Feature Interface Abstract Class
Purpose Defines a contract or capability. Provides a shared base for related classes.
Keyword interface abstract class
Instantiation Cannot be instantiated directly. Cannot be instantiated directly.
Implementation Can contain abstract declarations and default implementations. Can contain abstract and concrete members.
State Cannot hold ordinary per-instance backing-field state for interface properties. Can hold instance properties and backing fields.
Inheritance A class can implement multiple interfaces. A class can directly inherit from only one class.
Constructor Does not have a class-style constructor. Can have a primary or secondary constructor.
Best use Shared capabilities and interchangeable implementations. Shared state and implementation among related classes.

9. Implementing Multiple Interfaces

Kotlin allows a class to implement more than one interface. This is useful when an Android component needs to support several independent capabilities.

interface Clickable {
    fun onClick()
}

interface Shareable {
    fun share()
}

class ArticleCard : Clickable, Shareable {
    override fun onClick() {
        println("Article opened")
    }

    override fun share() {
        println("Article shared")
    }
}

The ArticleCard class implements both interfaces and provides the required methods. This avoids the need to inherit from multiple base classes.

Handling Conflicting Default Methods

If two interfaces provide implementations of a method with the same signature, the implementing class may need to override that method and explicitly choose which implementation to call.

interface FirstLogger {
    fun log() {
        println("First logger")
    }
}

interface SecondLogger {
    fun log() {
        println("Second logger")
    }
}

class AppLogger : FirstLogger, SecondLogger {
    override fun log() {
        super<FirstLogger>.log()
        super<SecondLogger>.log()
        println("Application logger")
    }
}

10. Using Interfaces in Android Click Events

Interfaces are frequently used to handle user interactions. A click listener defines what should happen when a user taps a button or another interactive component.

interface OnActionListener {
    fun onActionSelected(action: String)
}

class ActionHandler : OnActionListener {
    override fun onActionSelected(action: String) {
        println("Selected action: $action")
    }
}

fun main() {
    val handler: OnActionListener = ActionHandler()
    handler.onActionSelected("Open Profile")
}

This example demonstrates the interface pattern independently of the Android UI framework. In a real Android screen, the same contract can connect a UI event to an activity, fragment, or ViewModel-facing component, depending on the architecture.

Android Button Example

// Inside an Activity's onCreate() method
val saveButton = findViewById<Button>(R.id.saveButton)

saveButton.setOnClickListener {
    // Handle the user's click
    saveUserData()
}

Android's setOnClickListener() accepts a listener contract. Kotlin lets developers use a lambda for this single-method interface, making event-handling code concise.

Ensure the required Android imports and the saveButton layout ID exist in the project before running this example.

11. Using Abstract Classes for Shared Screen Behavior

Abstract classes can provide shared behavior for a group of related components. For example, several screen controllers may share a method for displaying loading and error messages while implementing their own content-loading logic.

abstract class BaseRepository {
    fun logRequest(operation: String) {
        println("Repository operation: $operation")
    }

    abstract fun fetchData(): String
}

class ProfileRepository : BaseRepository() {
    override fun fetchData(): String {
        logRequest("fetch profile")
        return "Profile data"
    }
}

class ProductRepository : BaseRepository() {
    override fun fetchData(): String {
        logRequest("fetch products")
        return "Product data"
    }
}

Both repositories reuse the logging behavior but implement data retrieval differently. In production Android apps, repositories may retrieve data from a database, network API, or local cache.

12. Interfaces in Android Architecture

Interfaces help separate the definition of a service from its implementation. This is useful in layered applications where the user interface should not depend directly on a specific data source.

  • Presentation layer: Handles screens, UI state, and user interactions.
  • Domain layer: Defines business rules and use cases where a separate domain layer is appropriate.
  • Data layer: Implements repositories and manages data sources such as APIs and databases.
  • Interface contract: Defines operations that the data or service implementation must support.

Repository Interface Example

data class User(
    val id: Int,
    val name: String
)

interface UserRepository {
    fun getUser(userId: Int): User?
}

class FakeUserRepository : UserRepository {
    private val users = listOf(
        User(1, "Aarav"),
        User(2, "Diya")
    )

    override fun getUser(userId: Int): User? {
        return users.find { it.id == userId }
    }
}

fun main() {
    val repository: UserRepository = FakeUserRepository()
    println(repository.getUser(1))
}

The application code depends on UserRepository, not on the concrete fake repository. A production implementation could later retrieve users from a database or remote API without changing every consumer of the contract.

13. Abstract Classes and Interfaces with Polymorphism

Polymorphism allows code to work with a common type while different objects provide their own behavior. Interfaces and abstract classes both support polymorphism.

interface Exportable {
    fun export(): String
}

class PdfExporter : Exportable {
    override fun export(): String = "Exported PDF"
}

class CsvExporter : Exportable {
    override fun export(): String = "Exported CSV"
}

fun exportReport(exporter: Exportable) {
    println(exporter.export())
}

fun main() {
    exportReport(PdfExporter())
    exportReport(CsvExporter())
}

The exportReport() function accepts any object implementing Exportable. It does not need separate functions for every export format.

14. Choosing Between an Interface and an Abstract Class

  • Choose an interface when you need to define a capability that unrelated classes can implement.
  • Choose an abstract class when related subclasses should share state, constructor initialization, or common implementation.
  • Use an interface for repository contracts when multiple implementations are possible.
  • Use an abstract class for a family of components that genuinely share a common base implementation.
  • Avoid creating an abstract class solely to hold unrelated utility functions; a regular class or top-level function may be simpler.
  • Prefer composition and small interfaces when they produce a clearer design than a deep inheritance hierarchy.

15. Common Errors and Troubleshooting

Error or Problem Possible Cause Solution
Class does not implement an abstract method A required interface or abstract-class member is missing. Implement every required member or keep the subclass abstract.
Cannot create an instance of an abstract class The code attempts to instantiate an abstract class directly. Create an instance of a concrete subclass.
Overrides nothing The function name, parameters, or return type does not match the declaration. Compare the method signature and add the correct override.
Conflicting interface implementations Multiple interfaces define the same default method. Override the method and explicitly resolve the conflict.
Unresolved reference A type or method name is misspelled, missing, or outside the available scope. Check spelling, imports, and declarations.
Too many responsibilities in a base class The abstract class combines unrelated features. Split behavior into focused interfaces or helper classes.
Android click handler does not run The wrong view ID is used or the listener is not attached. Verify the layout, view lookup, and listener registration.

16. Best Practices

  • Give interfaces meaningful names based on a capability or contract, such as Printable, Downloadable, or UserRepository.
  • Keep interfaces focused and avoid requiring implementations to provide unrelated operations.
  • Use Kotlin's override keyword for interface and abstract member implementations.
  • Use abstract classes only when there is a meaningful shared base or common state.
  • Depend on abstractions when it improves flexibility, testing, or separation of concerns.
  • Prefer constructor injection for passing dependencies into classes where appropriate.
  • Use lifecycle-aware Android architecture patterns and avoid retaining activity or fragment references longer than necessary.
  • Write unit tests for each implementation and tests for the behavior expected by the interface contract.
  • Document important contracts, including error conditions and whether operations are synchronous or asynchronous.
  • Do not use inheritance when composition would be simpler and easier to maintain.

17. Practical Exercise

Exercise: Build a simple notification system using interfaces and an abstract class.

  1. Create an interface named NotificationSender with a function named send().
  2. Create two implementations: EmailSender and PushSender.
  3. Create an abstract class named BaseNotification with a shared property for the message and a concrete function for validating that the message is not blank.
  4. Create a concrete notification class that inherits from BaseNotification and uses a NotificationSender.
  5. Write a test that verifies both sender implementations can be used through the same interface.
  6. Extend the example to display a success or failure result in an Android screen.

Sample Solution

interface NotificationSender {
    fun send(message: String): Boolean
}

class EmailSender : NotificationSender {
    override fun send(message: String): Boolean {
        println("Email sent: $message")
        return true
    }
}

class PushSender : NotificationSender {
    override fun send(message: String): Boolean {
        println("Push notification sent: $message")
        return true
    }
}

abstract class BaseNotification(
    protected val message: String
) {
    fun isValid(): Boolean = message.isNotBlank()

    abstract fun deliver(sender: NotificationSender): Boolean
}

class AppNotification(
    message: String
) : BaseNotification(message) {
    override fun deliver(sender: NotificationSender): Boolean {
        if (!isValid()) return false
        return sender.send(message)
    }
}

fun main() {
    val notification = AppNotification("Welcome to the app!")

    notification.deliver(EmailSender())
    notification.deliver(PushSender())
}

This design separates notification content from the mechanism used to deliver it. The abstract class provides shared validation, while the interface allows different delivery implementations.

18. Interview Questions and Answers

Q1. What is an interface in Kotlin?

An interface defines a contract of properties and functions. A class implements the interface and supplies implementations for its required members.

Q2. What is an abstract class?

An abstract class is a class that cannot be instantiated directly. It can contain abstract members as well as concrete methods and properties shared by subclasses.

Q3. Can a Kotlin class implement multiple interfaces?

Yes. A class can implement multiple interfaces, allowing it to support several capabilities without inheriting from multiple classes.

Q4. Can an interface contain a method implementation?

Yes. Kotlin interfaces can contain default method implementations as well as declarations without bodies.

Q5. Can an abstract class have a constructor?

Yes. An abstract class can have a primary constructor and secondary constructors. Its subclasses initialize the base class through the inheritance declaration.

Q6. What is the main difference between an interface and an abstract class?

An interface primarily defines a contract or capability, while an abstract class can also provide shared instance state, constructors, and common implementation.

Q7. Why are interfaces useful in Android development?

They support event listeners, callbacks, interchangeable service implementations, repository contracts, and testable application architecture.

Q8. What does the override keyword mean?

It indicates that a class is implementing or replacing a member declared by a parent class or interface.

Q9. What happens if a concrete class does not implement a required abstract member?

The compiler reports an error. The class must implement the member or itself be declared abstract.

Q10. How do interfaces improve unit testing?

They allow a dependency to be replaced with a fake or mock implementation, so tests can focus on the class being tested without requiring a real network or database.

19. Quick Revision Checklist

  • Understand the purpose of interfaces and abstract classes.
  • Know the Kotlin keywords interface, abstract, and override.
  • Implement required interface methods and abstract members.
  • Know how to implement multiple interfaces.
  • Resolve conflicting default method implementations.
  • Use interfaces to separate contracts from implementations.
  • Use abstract classes for meaningful shared state and behavior.
  • Apply these concepts to Android listeners, repositories, and testing.
  • Practice polymorphism and dependency substitution.

20. Conclusion

Interfaces and abstract classes are foundational tools for building structured Android applications with Kotlin. Interfaces define capabilities and contracts that multiple implementations can support, while abstract classes provide shared behavior and state for related subclasses. Using them appropriately improves code reuse, testability, flexibility, and maintainability.

Continue learning Android development through the JustAcademy Android App Development course. You can also register for a course demo or visit the JustAcademy website for more information.

Previous Next
whatsapp