Packages give Kotlin declarations organized names. Visibility determines which other code may use those declarations. The two mechanisms cooperate, but they are not the same: being in a nearby package does not automatically grant access to a private member, and internal means module access rather than Java-style package access.
The classes lesson introduces private state. Here you will extend that idea to files, modules, imports and property setters. The examples are separate Kotlin/JVM programs. You will design a small stock component whose callers can read a count but must use an approved method to change it.
A package names declarations; it does not load files
Write a package declaration near the start of a Kotlin source file. A class named Stock in package shop.inventory has the qualified name shop.inventory.Stock. Imports let another file use a shorter source name. An import does not create an object, run a function or fetch a dependency from the internet.
The Kotlin compiler accepts package declarations independently of a mandatory directory match, but projects normally align directories with packages for discoverability. A file under shop/inventory is easier to find when it declares that package. Build source roots determine which files participate in compilation; changing a package does not automatically move a file into the build.
Imports can target classes and top-level functions or properties. An alias such as import shop.inventory.Stock as InventoryStock resolves naming conflicts locally. The declaration's actual identity does not change. Prefer a role-specific alias to a name with an unexplained number, especially when two libraries both define Client or Result.
The four visibility choices
Public is the default visibility. A public declaration is available wherever its containing declaration is accessible. Internal limits access to the same Kotlin module. A module is a compilation boundary, such as a Gradle source set, not merely a directory containing related files.
Private has different useful meanings depending on scope. A private top-level function belongs to its source file. A private class member belongs to that class. Protected applies to class members and permits subclass access; it is not available for ordinary top-level declarations. Choose the smallest visibility that supports the intended collaboration.
Visibility is an API boundary, not secret encryption. An internal password constant is still present in source or compiled code. Keep credentials in controlled runtime configuration. Access modifiers prevent accidental or unsupported code dependencies; they do not replace authentication, file permissions or safe secret storage.
Property access and mutation can differ
A property can have a public getter and a private setter. This lets callers inspect state while requiring changes to go through methods that preserve rules. A stock count should not become negative merely because an unrelated caller assigns an arbitrary number.
The getter has the property's visibility. A setter can be more restricted, such as private set or internal set. A val property has no ordinary setter at all, but its referenced object can still be mutable. A public val MutableList exposes mutation through the list's methods even though the property binding cannot be reassigned.
Keep implementation storage private and expose a deliberate view or operation. Do not assume a read-only List is an immutable deep snapshot; another owner may still mutate its backing data, and element objects can remain mutable. State access and element ownership both deserve a defined contract.
Modules, tests and published interfaces
Internal is useful for code shared across files within one component without making it a supported external API. Kotlin's testing setup can allow the test source set to access internal declarations of main through the configured compilation relationship. That is a build arrangement, not a promise that any project with a similarly named test directory receives access.
When publishing a library, keep public parameter and return types accessible to callers. A public function cannot use an inaccessible private implementation type as though it were a useful external contract. Interfaces or public value types can describe the supported result while internal classes handle implementation.
Avoid making a declaration public solely to simplify one test. Test behavior through a meaningful interface or use the project's supported internal testing arrangement. Expanding visibility can create lasting dependencies that make later refactoring harder, even when it initially appears to save a small amount of test setup.
Source references and common mistakes
The package reference describes imports and aliases. The visibility reference explains scope-specific access. Do not import an inaccessible declaration and expect the import to bypass its modifier. Do not apply protected to a top-level helper. When access fails, inspect the declaration's scope, containing type and compilation module before loosening every modifier.
Kotlin Packages, Imports and Visibility Examples
Each example shows the code, the output it produces and what each part does.
A file-private helper in a named package
The helper is accessible within this file. DisplayLabel is available to other code in the same module but is not intended as a public library API. The main function can call both according to their scope. When compiling the file, its generated JVM main-class name includes the package, so an IDE or Gradle application configuration should use the corresponding qualified entry point.
package shop.labels
private fun cleanLabel(value: String): String = value.trim()
internal fun displayLabel(value: String): String = "[${cleanLabel(value)}]"
fun main() {
println(displayLabel(" Pen "))
}[Pen]Kotlin Packages, Imports and Visibility in Real Projects
Worked exercise: a readable count with controlled changes
Create a StockCounter with an initial quantity, a readable count and a private setter. The remove method must reject nonpositive quantities and requests exceeding available stock. This makes mutation policy visible while preserving a convenient read operation for a screen or report.
class StockCounter(initial: Int) {
var count: Int = initial
private set
init { require(initial >= 0) }
fun remove(quantity: Int) {
require(quantity > 0) { "Quantity must be positive" }
require(quantity <= count) { "Insufficient stock" }
count -= quantity
}
}
fun main() {
val stock = StockCounter(8)
stock.remove(3)
println(stock.count)
}5Callers cannot assign stock.count directly from unrelated code. The method enforces the adjustment rule and produces a predictable result. This remains an in-memory single-threaded example; visibility does not coordinate simultaneous database writers. A real inventory service must enforce its atomic stock rule at the persistence boundary too.
Kotlin Packages, Imports and Visibility FAQ
Is internal the same as package-private in Java?
No. Internal follows Kotlin's module boundary, which can contain several packages. A declaration can therefore be internal and accessible from another package in the same compilation module. Conversely, matching package names in separately compiled modules does not automatically grant Kotlin internal access. At Java interoperability boundaries, generated JVM visibility and Kotlin metadata create additional details, so do not use internal as a confidentiality mechanism or assume it maps directly to Java's package access rules.
Can an import make a private declaration accessible?
No. An import supplies a source-level name mapping after normal visibility rules are considered. It does not grant privileges or copy the declaration into the current file. If the API genuinely needs to be shared, expose a deliberate operation with an appropriate modifier or move related code into a sensible ownership boundary. Changing everything to public merely to make imports compile often creates a larger and less stable interface than the component requires.
Why can a caller read a property but not assign it?
The property may have a restricted setter, such as private set. That design intentionally separates observation from mutation. Callers can inspect the current count while a method controls which adjustments are permitted. It is useful when assignments must preserve an invariant. A restricted setter does not make a referenced mutable collection or object deeply immutable, so also consider whether the getter exposes capabilities that let callers bypass the intended mutation policy.
Must Kotlin package names match folders exactly?
The compiler's package declaration determines the qualified name; a strict folder match is not the language rule. Nevertheless, aligning packages and folders is a strong project convention because it helps people find source and understand ownership. Build source roots still decide which files are compiled. Follow the established project layout unless you have a concrete reason to differ, and remember that renaming a package can affect imports, generated main-class names and external consumers.
Kotlin Packages, Imports and Visibility Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
A count should be readable publicly but writable only by its class. Which design fits?
Kotlin Packages, Imports and Visibility Quiz
2 questions to check what you learned.
What is Kotlin's default visibility for an ordinary declaration?
- public
- protected
- package-private
What boundary does internal describe?
- Only the same folder
- The same Kotlin compilation module
- Every application installed on the computer