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:
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.
When you declare something as public in Kotlin:
// 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"
}
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
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 declarations have different scoping rules:
// 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()
}
}
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!!
}
}
}
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 declarations in Kotlin:
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 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")
}
}
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 declarations offer module-scoped access:
// 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"
}
}
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()
}
}
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
}
}
}
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
}
}
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
}
Use public
when:
Use private
when:
Use protected
when:
Use internal
when:
// 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
}
}
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 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"
}
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