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.
// 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
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
// 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.
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
// 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:
// 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:
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:
// 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:
// 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
- Start with the most restrictive modifier: Begin with
private
and only increase visibility as needed. - Hide implementation details: Make internal workings private and only expose what clients need.
- Protected for planned inheritance: Use
protected
only for members that subclasses will likely need. - Use internal for module-level sharing: When functionality needs to be shared across files but not outside the module.
- 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 everywhereprivate
: Visible inside the containing file or classprotected
: Visible inside the containing class and its subclassesinternal
: 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
- Create a
Car
class with private properties for fuel level and engine status, with public methods to start/stop the car and check status. - Design a
BankAccount
hierarchy withprotected
methods that only derived classes likeSavingsAccount
andCheckingAccount
can access. - Create a simple module with internal utility classes that can be accessed throughout the module but not from outside it.
- Build a class with a private constructor and a companion object factory method that validates input parameters.
Additional Resources
- Kotlin Official Documentation on Visibility Modifiers
- Clean Code by Robert C. Martin - A great resource on code structure and encapsulation principles
- Effective Java by Joshua Bloch - Contains excellent advice on API design (many principles apply to Kotlin as well)
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)