Kotlin Visibility Modifiers

Kotlin visibility modifiers are special keywords that determine the accessibility scope of classes, objects, interfaces, constructors, functions, and properties in your Kotlin code. Unlike Java, which has package-private as the default access level, Kotlin visibility modifiers default to public, making your code more open by design while still providing powerful encapsulation mechanisms.

The four Kotlin visibility modifiers are:

  • public - Accessible from anywhere (default)
  • private - Accessible only within the declaring scope
  • protected - Accessible within the class and its subclasses
  • internal - Accessible within the same module

Public Modifier - The Default Kotlin Visibility

The public modifier in Kotlin makes declarations accessible from anywhere in your codebase. What makes Kotlin unique is that public is the default visibility modifier, so you don’t need to explicitly declare it.

Public Modifier Properties

When you declare something as public in Kotlin:

  • It’s accessible from any file, package, or module
  • No explicit declaration needed (implicit by default)
  • Works exactly like Java’s public modifier
  • Can be overridden by subclasses with any visibility
// Public class (implicit)  
class PublicExample {  
    val publicProperty = "Accessible everywhere"  
      
    fun publicMethod() {  
        println("This method is public by default")  
    }  
}  
  
// Explicit public declaration (unnecessary but allowed)  
public class ExplicitPublic {  
    public val explicitProperty = "Explicitly public"  
}  

Public Modifier in Top-Level Declarations

When working with top-level declarations in Kotlin files, the public modifier allows access from any part of your application:

// File: Utils.kt  
val globalConstant = "Available everywhere"  
fun utilityFunction() = "Public utility function"  
class UtilityClass  
  
// These can be accessed from any other Kotlin file  

Private Modifier - Restricting Access Scope

The private modifier is the most restrictive Kotlin visibility modifier, limiting access to the immediate declaring scope. This modifier behaves differently depending on where it’s applied.

Private Modifier Properties

Private declarations have different scoping rules:

  • Top-level private: Visible only within the same file
  • Class-level private: Visible only within the declaring class
  • Cannot be overridden: Private members are not inherited
// File: PrivateExample.kt  
private val filePrivateConstant = "Only visible in this file"  
  
private fun filePrivateFunction() {  
    println("Only accessible within PrivateExample.kt")  
}  
  
class PrivateDemo {  
    private val classPrivateProperty = "Only visible in PrivateDemo class"  
      
    private fun classPrivateMethod() {  
        // Can access file-private declarations  
        println(filePrivateConstant)  
        filePrivateFunction()  
    }  
      
    fun publicMethodUsingPrivate() {  
        // Can access private members within same class  
        println(classPrivateProperty)  
        classPrivateMethod()  
    }  
}  

Private Constructors in Kotlin

You can create private constructors to control object instantiation:

class SingletonExample private constructor(val data: String) {  
    companion object {  
        private var instance: SingletonExample? = null  
          
        fun getInstance(data: String): SingletonExample {  
            if (instance == null) {  
                instance = SingletonExample(data)  
            }  
            return instance!!  
        }  
    }  
}  

Protected Modifier - Class Hierarchy Access

The protected modifier in Kotlin provides access to the declaring class and its subclasses. Unlike Java, Kotlin’s protected modifier doesn’t include package-level access.

Protected Modifier Properties

Protected declarations in Kotlin:

  • Not available for top-level declarations: You cannot declare top-level functions or classes as protected
  • Subclass accessible: Available in all subclasses regardless of package
  • Same class accessible: Available within the declaring class
  • Default inheritance: When overriding protected members, they remain protected unless explicitly changed
open class ProtectedParent {  
    protected val protectedProperty = "Accessible to subclasses"  
      
    protected open fun protectedMethod() {  
        println("Protected method in parent")  
    }  
      
    private val privateProperty = "Not accessible to subclasses"  
      
    fun demonstrateAccess() {  
        // Can access both protected and private within same class  
        println(protectedProperty)  
        println(privateProperty)  
        protectedMethod()  
    }  
}  
  
class ProtectedChild : ProtectedParent() {  
    fun childMethod() {  
        // Can access protected members from parent  
        println(protectedProperty)  
        protectedMethod()  
          
        // Cannot access private members from parent  
        // println(privateProperty) // Compilation error  
    }  
      
    override fun protectedMethod() {  
        // Overridden method is still protected by default  
        super.protectedMethod()  
        println("Overridden in child")  
    }  
}  

Protected Modifier with Abstract Classes

Protected modifiers work excellently with abstract classes for template method patterns:

abstract class DatabaseConnection {  
    protected abstract fun establishConnection(): Boolean  
    protected abstract fun executeQuery(query: String): List<String>  
      
    fun processData(query: String): List<String> {  
        return if (establishConnection()) {  
            executeQuery(query)  
        } else {  
            emptyList()  
        }  
    }  
}  
  
class MySQLConnection : DatabaseConnection() {  
    override protected fun establishConnection(): Boolean {  
        println("Connecting to MySQL database")  
        return true  
    }  
      
    override protected fun executeQuery(query: String): List<String> {  
        println("Executing MySQL query: $query")  
        return listOf("result1", "result2")  
    }  
}  

Internal Modifier - Module-Level Visibility

The internal modifier is unique to Kotlin and provides module-level visibility. This modifier is particularly useful for library development and large applications with multiple modules.

Internal Modifier Properties

Internal declarations offer module-scoped access:

  • Module-wide access: Visible throughout the entire module
  • Module boundary protection: Not accessible outside the module
  • Perfect for APIs: Ideal for exposing implementation details within a module while hiding them externally
  • Gradle/Maven module alignment: Works with build system module definitions
// File: InternalAPI.kt  
internal class DatabaseManager {  
    internal val connectionPool = mutableListOf<String>()  
      
    internal fun getConnection(): String {  
        return if (connectionPool.isNotEmpty()) {  
            connectionPool.removeAt(0)  
        } else {  
            "new-connection-${System.currentTimeMillis()}"  
        }  
    }  
      
    internal fun releaseConnection(connection: String) {  
        connectionPool.add(connection)  
    }  
}  
  
// File: PublicService.kt  
class UserService {  
    private val dbManager = DatabaseManager() // Can access internal class  
      
    fun getUser(id: String): String {  
        val connection = dbManager.getConnection() // Can access internal method  
        // Process user data  
        dbManager.releaseConnection(connection)  
        return "User-$id"  
    }  
}  

Internal Modifier in Android Development

The internal modifier is particularly powerful in Android module architecture:

// In your data module  
internal interface UserRepository {  
    suspend fun getUser(id: String): User  
}  
  
internal class UserRepositoryImpl : UserRepository {  
    override suspend fun getUser(id: String): User {  
        // Implementation details hidden from other modules  
        return User(id, "John Doe")  
    }  
}  
  
// Public API exposed to other modules  
class UserDataModule {  
    fun provideUserRepository(): UserRepository {  
        return UserRepositoryImpl()  
    }  
}  

Kotlin Visibility Modifiers in Different Contexts

Constructor Visibility

By default, Kotlin constructors are public, but you can modify their visibility:

class VisibilityConstructorDemo {  
    // Public constructor (default)  
    constructor(name: String) {  
        println("Public constructor: $name")  
    }  
      
    // Private secondary constructor  
    private constructor(id: Int, name: String) : this(name) {  
        println("Private constructor with ID: $id")  
    }  
      
    companion object {  
        fun createWithId(id: Int, name: String): VisibilityConstructorDemo {  
            return VisibilityConstructorDemo(id, name)  
        }  
    }  
}  
  
// Primary constructor with explicit visibility  
class RestrictedAccess private constructor(val secret: String) {  
    companion object {  
        fun createInstance(password: String): RestrictedAccess? {  
            return if (password == "correct") {  
                RestrictedAccess("Hidden secret")  
            } else null  
        }  
    }  
}  

Property Setters with Custom Visibility

Kotlin allows different visibility for property setters:

class PropertyVisibilityDemo {  
    var publicGetterPrivateSetter: String = "initial"  
        private set // Setter is private, getter is public  
      
    var internalProperty: String = "internal"  
        internal set // Both getter and setter are internal  
      
    protected var protectedProperty: String = "protected"  
        private set // Getter is protected, setter is private  
      
    fun updateProperties(value: String) {  
        // Can modify private setter within same class  
        publicGetterPrivateSetter = value  
        internalProperty = value  
        protectedProperty = value  
    }  
}  

Overriding and Kotlin Visibility Modifiers

When overriding members, Kotlin has specific rules for visibility modifiers:

open class BaseVisibility {  
    open protected fun protectedMethod() = "Base protected"  
    open internal fun internalMethod() = "Base internal"  
    open fun publicMethod() = "Base public"  
}  
  
class DerivedVisibility : BaseVisibility() {  
    // Protected method remains protected when not specified  
    override fun protectedMethod() = "Derived protected"  
      
    // Can make protected method public  
    public override fun publicMethod() = "Derived public"  
      
    // Internal method remains internal  
    override fun internalMethod() = "Derived internal"  
      
    // Cannot make public method more restrictive  
    // private override fun publicMethod() = "Error" // Compilation error  
}  

Best Practices for Kotlin Visibility Modifiers

When to Use Each Modifier

Use public when:

  • Creating APIs that need external access
  • Building utility functions used across your application
  • Defining data classes that represent your domain model

Use private when:

  • Implementing internal logic that shouldn’t be exposed
  • Creating helper functions specific to a class
  • Building singletons or factory patterns

Use protected when:

  • Designing inheritance hierarchies
  • Creating abstract base classes
  • Implementing template method patterns

Use internal when:

  • Building module-specific APIs
  • Creating implementation details that should be hidden from external modules
  • Developing libraries where you want to expose functionality within the library but not to consumers

Common Patterns in Android Development

// Repository pattern with internal implementation  
internal class NetworkUserRepository : UserRepository {  
    private val apiService = createApiService()  
      
    override suspend fun getUsers(): List<User> {  
        return try {  
            apiService.fetchUsers()  
        } catch (e: Exception) {  
            // Private error handling  
            handleNetworkError(e)  
            emptyList()  
        }  
    }  
      
    private fun handleNetworkError(error: Exception) {  
        // Private error handling logic  
        println("Network error: ${error.message}")  
    }  
}  
  
// ViewModel with mixed visibility  
class UserViewModel : ViewModel() {  
    private val repository: UserRepository = UserRepositoryImpl()  
      
    private val _users = MutableLiveData<List<User>>()  
    val users: LiveData<List<User>> = _users // Public getter, private backing property  
      
    fun loadUsers() { // Public method for UI interaction  
        viewModelScope.launch {  
            _users.value = repository.getUsers()  
        }  
    }  
      
    private fun handleError(error: Throwable) { // Private error handling  
        // Handle error logic  
    }  
}  

Advanced Kotlin Visibility Scenarios

Nested Classes and Visibility

class OuterClass {  
    private val outerPrivate = "Outer private"  
      
    class NestedClass {  
        fun accessOuter() {  
            // Cannot access outer class private members  
            // println(outerPrivate) // Compilation error  
        }  
    }  
      
    inner class InnerClass {  
        fun accessOuter() {  
            // Can access outer class private members  
            println(outerPrivate)  
        }  
    }  
      
    private class PrivateNestedClass {  
        // Only accessible within OuterClass  
        fun doSomething() = "Private nested functionality"  
    }  
}  

Interface Implementation with Visibility

interface PublicInterface {  
    fun publicInterfaceMethod(): String  
}  
  
internal interface InternalInterface {  
    fun internalInterfaceMethod(): String  
}  
  
class ImplementationExample : PublicInterface, InternalInterface {  
    // Must be public to satisfy public interface  
    override fun publicInterfaceMethod(): String = "Public implementation"  
      
    // Can be internal since interface is internal  
    override fun internalInterfaceMethod(): String = "Internal implementation"  
      
    private fun helperMethod(): String = "Private helper"  
}  

Complete Example: E-commerce Cart System

Here’s a comprehensive example demonstrating all Kotlin visibility modifiers in a real-world scenario:

// File: CartSystem.kt  
  
// Public data classes for external API  
data class Product(val id: String, val name: String, val price: Double)  
data class CartItem(val product: Product, val quantity: Int)  
  
// Internal cart management - hidden from external modules  
internal class CartManager {  
    private val items = mutableMapOf<String, CartItem>()  
      
    internal fun addItem(product: Product, quantity: Int) {  
        val existingItem = items[product.id]  
        if (existingItem != null) {  
            items[product.id] = existingItem.copy(quantity = existingItem.quantity + quantity)  
        } else {  
            items[product.id] = CartItem(product, quantity)  
        }  
        validateCart()  
    }  
      
    internal fun removeItem(productId: String) {  
        items.remove(productId)  
    }  
      
    internal fun getItems(): List<CartItem> = items.values.toList()  
      
    internal fun getTotalPrice(): Double = items.values.sumOf { it.product.price * it.quantity }  
      
    private fun validateCart() {  
        // Private validation logic  
        items.values.forEach { item ->  
            require(item.quantity > 0) { "Quantity must be positive" }  
        }  
    }  
}  
  
// Public shopping cart API  
class ShoppingCart {  
    private val cartManager = CartManager()  
      
    fun addProduct(product: Product, quantity: Int = 1) {  
        cartManager.addItem(product, quantity)  
    }  
      
    fun removeProduct(productId: String) {  
        cartManager.removeItem(productId)  
    }  
      
    fun getCartItems(): List<CartItem> = cartManager.getItems()  
      
    fun calculateTotal(): Double = cartManager.getTotalPrice()  
      
    fun checkout(): OrderSummary {  
        return createOrderSummary()  
    }  
      
    private fun createOrderSummary(): OrderSummary {  
        return OrderSummary(  
            items = getCartItems(),  
            total = calculateTotal(),  
            timestamp = System.currentTimeMillis()  
        )  
    }  
}  
  
// Abstract base for different cart types  
abstract class BaseCart {  
    protected abstract fun getDiscountRate(): Double  
      
    protected fun applyDiscount(originalPrice: Double): Double {  
        return originalPrice * (1 - getDiscountRate())  
    }  
      
    abstract fun calculateFinalPrice(): Double  
}  
  
class PremiumCart : BaseCart() {  
    private val cart = ShoppingCart()  
      
    override protected fun getDiscountRate(): Double = 0.1 // 10% discount  
      
    override fun calculateFinalPrice(): Double {  
        val originalTotal = cart.calculateTotal()  
        return applyDiscount(originalTotal)  
    }  
      
    fun addPremiumProduct(product: Product, quantity: Int = 1) {  
        cart.addProduct(product, quantity)  
    }  
}  
  
// Order summary for checkout  
data class OrderSummary(  
    val items: List<CartItem>,  
    val total: Double,  
    val timestamp: Long  
)  
  
// Usage example  
fun main() {  
    // Create products  
    val laptop = Product("1", "Gaming Laptop", 999.99)  
    val mouse = Product("2", "Wireless Mouse", 29.99)  
      
    // Create shopping cart  
    val cart = ShoppingCart()  
    cart.addProduct(laptop, 1)  
    cart.addProduct(mouse, 2)  
      
    // Display cart contents  
    println("Cart Items:")  
    cart.getCartItems().forEach { item ->  
        println("${item.product.name} - Quantity: ${item.quantity} - Price: $${item.product.price}")  
    }  
      
    println("Total: $${cart.calculateTotal()}")  
      
    // Create premium cart with discount  
    val premiumCart = PremiumCart()  
    premiumCart.addPremiumProduct(laptop, 1)  
    println("Premium cart total with discount: $${premiumCart.calculateFinalPrice()}")  
      
    // Checkout  
    val orderSummary = cart.checkout()  
    println("Order placed at ${orderSummary.timestamp} for $${orderSummary.total}")  
}  

Output:

Cart Items:  
Gaming Laptop - Quantity: 1 - Price: $999.99  
Wireless Mouse - Quantity: 2 - Price: $29.99  
Total: $1059.97  
Premium cart total with discount: $899.991  
Order placed at 1672531200000 for $1059.97  

This comprehensive example demonstrates how Kotlin visibility modifiers work together to create a well-encapsulated system where:

  • Public classes and methods form the external API

  • Internal classes handle module-specific implementation details

  • Private methods manage internal state and validation

  • Protected methods enable inheritance while maintaining encapsulation