Skip to main content

Kotlin Visibility Modifiers

When designing classes and other structures in Kotlin, controlling the visibility of your code components is essential for maintaining proper encapsulation. Visibility modifiers allow you to define what parts of your code can be accessed from other parts of your program. This tutorial covers Kotlin's visibility modifiers, how they work, and best practices for using them effectively.

Introduction to Visibility Modifiers

Visibility modifiers (also called access modifiers) in Kotlin determine which parts of your code can access a particular class, function, property, or other declarations. Kotlin provides four visibility modifiers:

  • public (default)
  • private
  • protected
  • internal

Understanding these modifiers is crucial for writing maintainable and secure code by enforcing proper encapsulation principles.

The Public Modifier

The public modifier makes the declaration visible everywhere. It is the default visibility in Kotlin, meaning if you don't specify any modifier, public is applied automatically.

kotlin
// This class is public by default
class User(val name: String, val age: Int) {
// This function is also public by default
fun greet() {
println("Hello, my name is $name!")
}
}

// Usage
fun main() {
val user = User("Alice", 25)
user.greet() // Accessible everywhere
println("Name: ${user.name}, Age: ${user.age}") // Properties are also accessible
}

Output:

Hello, my name is Alice!
Name: Alice, Age: 25

The Private Modifier

The private modifier restricts the visibility of a declaration to the file (for top-level declarations) or class (for class members) containing the declaration.

Private Class Members

kotlin
class BankAccount(private val accountNumber: String, initialBalance: Double) {
private var balance = initialBalance

fun deposit(amount: Double) {
if (amount > 0) {
balance += amount
println("Deposited $amount. New balance: $balance")
}
}

fun withdraw(amount: Double) {
if (amount <= balance && amount > 0) {
balance -= amount
println("Withdrew $amount. New balance: $balance")
} else {
println("Insufficient funds or invalid amount")
}
}

fun getBalance(): Double {
return balance
}

// Cannot access accountNumber directly from outside the class
}

fun main() {
val account = BankAccount("123456789", 1000.0)
account.deposit(500.0)
account.withdraw(200.0)
println("Current balance: ${account.getBalance()}")

// This would cause a compilation error:
// println(account.accountNumber)
// println(account.balance)
}

Output:

Deposited 500.0. New balance: 1500.0
Withdrew 200.0. New balance: 1300.0
Current balance: 1300.0

Private Top-Level Declarations

kotlin
// File: Helpers.kt
private fun formatCurrency(amount: Double): String {
return "$$amount"
}

fun displayPrice(price: Double) {
println("The price is ${formatCurrency(price)}")
}

// formatCurrency is only accessible within this file

The Protected Modifier

The protected modifier makes a member visible within the class and all its subclasses. Note that protected is not available for top-level declarations.

kotlin
open class Animal(protected val species: String) {
protected fun makeNoise() {
println("Some generic animal noise")
}

fun describe() {
println("This animal is a $species")
makeNoise() // Can access protected function
}
}

class Dog : Animal("Canine") {
fun bark() {
println("Woof! Woof!")
println("I am a $species") // Can access protected property from parent
makeNoise() // Can access protected function from parent
}
}

fun main() {
val animal = Animal("Unknown")
animal.describe()

val dog = Dog()
dog.bark()
dog.describe()

// These would cause compilation errors:
// println(animal.species)
// animal.makeNoise()
// println(dog.species)
}

Output:

This animal is a Unknown
Some generic animal noise
Woof! Woof!
I am a Canine
Some generic animal noise
This animal is a Canine
Some generic animal noise

The Internal Modifier

The internal modifier makes the declaration visible within the same module. A module is a set of Kotlin files compiled together, such as:

  • An IntelliJ IDEA module
  • A Maven project
  • A Gradle source set
  • A set of files compiled with one invocation of the Kotlin compiler
kotlin
// File: Module1.kt
internal class DatabaseHelper {
fun connect() {
println("Connecting to database...")
}
}

fun initDatabase() {
val helper = DatabaseHelper() // Accessible within the same module
helper.connect()
}

In the same module, you can access this class:

kotlin
// File: App.kt (same module)
fun main() {
val dbHelper = DatabaseHelper() // Works fine
dbHelper.connect()

initDatabase()
}

But in a different module, this would cause compilation errors.

Visibility Modifiers for Constructors

In Kotlin, constructors can also have visibility modifiers:

kotlin
class Server private constructor(val port: Int) {
companion object {
fun createServer(port: Int): Server {
if (port in 1024..65535) {
return Server(port)
} else {
throw IllegalArgumentException("Invalid port number")
}
}
}

fun start() {
println("Server started on port $port")
}
}

fun main() {
// This would cause a compilation error:
// val server = Server(8080)

// This is the correct way to create a Server instance
val server = Server.createServer(8080)
server.start()
}

Output:

Server started on port 8080

Real-World Application: Building a Library

Let's see how visibility modifiers can be used in a real-world scenario - creating a simple library for geometric calculations:

kotlin
// File: Geometry.kt
package com.example.geometry

// Public API - accessible to library users
public class Rectangle(
val width: Double,
val height: Double
) {
fun area(): Double = calculateArea(width, height)

fun perimeter(): Double = 2 * (width + height)

companion object {
fun square(side: Double): Rectangle {
return Rectangle(side, side)
}
}
}

// Internal implementation - only accessible within the library
internal fun calculateArea(width: Double, height: Double): Double {
return width * height
}

// Private utility - only accessible in this file
private fun validateDimension(value: Double): Boolean {
return value > 0
}

// Public API - accessible to library users
public class Circle(val radius: Double) {
init {
require(radius > 0) { "Radius must be positive" }
}

fun area(): Double = Math.PI * radius * radius

fun circumference(): Double = 2 * Math.PI * radius
}

When users of your library use your code, they would interact with it like this:

kotlin
// File: UserCode.kt
import com.example.geometry.*

fun main() {
val rect = Rectangle(5.0, 3.0)
println("Rectangle area: ${rect.area()}")
println("Rectangle perimeter: ${rect.perimeter()}")

val square = Rectangle.square(4.0)
println("Square area: ${square.area()}")

val circle = Circle(2.5)
println("Circle area: ${circle.area()}")
println("Circle circumference: ${circle.circumference()}")

// These would cause compilation errors:
// calculateArea(5.0, 3.0) - internal function not accessible
// validateDimension(5.0) - private function not accessible
}

Output:

Rectangle area: 15.0
Rectangle perimeter: 16.0
Square area: 16.0
Circle area: 19.634954084936208
Circle circumference: 15.707963267948966

Best Practices for Using Visibility Modifiers

  1. Start with the most restrictive modifier: Begin with private and only increase visibility as needed.
  2. Hide implementation details: Make internal workings private and only expose what clients need.
  3. Protected for planned inheritance: Use protected only for members that subclasses will likely need.
  4. Use internal for module-level sharing: When functionality needs to be shared across files but not outside the module.
  5. Keep the public API minimal: Only make declarations public that are meant to be part of your API.

Summary

Kotlin visibility modifiers are powerful tools for controlling access to your code components:

  • public (default): Visible everywhere
  • private: Visible inside the containing file or class
  • protected: Visible inside the containing class and its subclasses
  • internal: Visible inside the same module

Using these modifiers correctly helps you create well-structured, maintainable code with clear boundaries between implementation details and public API. Proper encapsulation through visibility modifiers is a cornerstone of writing robust object-oriented code.

Exercises

  1. Create a Car class with private properties for fuel level and engine status, with public methods to start/stop the car and check status.
  2. Design a BankAccount hierarchy with protected methods that only derived classes like SavingsAccount and CheckingAccount can access.
  3. Create a simple module with internal utility classes that can be accessed throughout the module but not from outside it.
  4. Build a class with a private constructor and a companion object factory method that validates input parameters.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)