Visibility Modifier in Kotlin
Sichtbarkeit steuert, wer deinen Kotlin-Code nutzen darf. Du lernst, Implementierungsdetails in Android-Apps gezielt zu begrenzen.
Visibility Modifier helfen dir, den Zugriff auf Klassen, Funktionen, Properties und Konstruktoren bewusst zu begrenzen. In Android-Projekten ist das kein akademisches Detail: Je weniger Implementierung du nach außen freigibst, desto leichter kannst du ViewModels, Repositorys, Compose-Komponenten und Hilfsfunktionen ändern, ohne ungewollt andere Teile der App zu brechen.
Was ist das?
Visibility Modifier sind Schlüsselwörter in Kotlin, mit denen du festlegst, von wo aus ein Codeelement erreichbar ist. Die wichtigsten Modifier sind public, private, internal und protected. Wenn du keinen Modifier angibst, ist ein Element in Kotlin standardmäßig public. Das bedeutet: Es ist von überall sichtbar, wo das Element importiert oder referenziert werden kann.
Das mentale Modell ist einfach genug für den Einstieg, aber wichtig für saubere Architektur: Nicht alles, was technisch funktioniert, sollte auch öffentlich erreichbar sein. Eine Klasse kann eine klare Außenseite haben, zum Beispiel eine Funktion loadProfile(), und gleichzeitig mehrere innere Hilfsfunktionen besitzen, die niemand direkt aufrufen soll. Diese Grenze schützt dich vor zufälliger Kopplung.
In Android-Apps begegnet dir Sichtbarkeit ständig. Ein ViewModel bietet Zustände und Aktionen für die UI an. Ein Repository kapselt Netzwerk- oder Datenbankzugriff. Eine Compose-Datei enthält oft öffentliche Screen-Funktionen und private kleinere Composables. Ohne Visibility Modifier wächst die öffentliche Oberfläche deines Codes schnell. Dann verlassen sich andere Dateien auf Details, die du eigentlich frei ändern wolltest.
public ist sinnvoll für echte APIs innerhalb deines Projekts: Screens, Use Cases, Datenmodelle oder Funktionen, die bewusst von anderen Schichten genutzt werden. private ist passend für Details, die nur in einer Klasse, einem Objekt oder einer Datei gebraucht werden. internal begrenzt den Zugriff auf dasselbe Kotlin-Modul. Das ist in Android besonders relevant, wenn dein Projekt mehrere Gradle-Module hat, etwa :app, :feature-login und :core-network. protected macht Mitglieder für eine Klasse und ihre Unterklassen sichtbar. Es gehört also vor allem zum Thema Vererbung.
Wie funktioniert es?
Kotlin unterscheidet Sichtbarkeit je nach Kontext. Auf oberster Dateiebene können Funktionen, Klassen und Properties public, private oder internal sein. private auf Dateiebene bedeutet: Nur Code in derselben Datei sieht dieses Element. Das ist sehr nützlich für Compose, weil du in einer Datei einen öffentlichen Screen und mehrere private Hilfs-Composables definieren kannst.
Innerhalb einer Klasse bedeutet private: Nur diese Klasse darf auf das Element zugreifen. Das passt gut für veränderbaren internen Zustand oder kleine Hilfsfunktionen. Wenn eine Property nur intern gesetzt werden soll, kannst du auch den Setter einschränken, etwa mit private set. So bleibt Lesen von außen erlaubt, Schreiben aber kontrolliert.
internal ist kein Paket-Schutz wie in manchen anderen Sprachen. Es bedeutet in Kotlin: sichtbar im selben Modul. Ein Modul ist dabei die Kompilationseinheit, meist ein Gradle-Modul. Für Android-Lernende ist das wichtig, weil Package-Namen allein keine stabile Architekturgrenze bilden. Nur weil zwei Klassen im gleichen Package liegen, heißt das nicht, dass du sie unkontrolliert voneinander abhängig machen solltest.
protected funktioniert nur innerhalb von Klassen und ist für Unterklassen sichtbar. In Kotlin ist protected nicht auf Package-Ebene sichtbar. Nutze es sparsam. Viele moderne Android-APIs setzen eher auf Komposition, Interfaces und kleine Klassen statt tiefer Vererbung. Wenn du protected verwendest, solltest du klar begründen können, welche Erweiterung eine Unterklasse wirklich braucht.
In Jetpack Compose ist Sichtbarkeit besonders praktisch, weil UI-Code oft in viele kleine Funktionen zerlegt wird. Die nach außen relevante Funktion ist zum Beispiel ProfileScreen. Kleinere Bausteine wie ProfileHeader, ErrorMessage oder ProfileContent können private bleiben, solange sie nur in derselben Datei verwendet werden. Dadurch bleibt die öffentliche UI-API schlank, und du kannst den Aufbau des Screens ändern, ohne andere Dateien anzupassen.
Für Tests spielt Sichtbarkeit ebenfalls eine Rolle. Ein häufiger Anfängerfehler ist, alles public zu machen, damit Tests leichter darauf zugreifen können. Das wirkt kurzfristig bequem, schwächt aber die Kapselung. Teste bevorzugt das sichtbare Verhalten einer Klasse. Wenn du ständig private Details testen möchtest, kann das ein Hinweis sein, dass eine Verantwortlichkeit in eine eigene Klasse ausgelagert werden sollte. Für modulweite Testbarkeit kann internal mit passenden Test-Setups sinnvoll sein, aber auch hier gilt: Sichtbarkeit ist ein Architekturwerkzeug, kein Ersatz für gutes Design.
In der Praxis
Stell dir einen einfachen Profil-Screen in Compose vor. Die App soll ein Profil anzeigen, aber der Aufbau der Unterelemente soll nicht zur öffentlichen API des Moduls werden. Dann kann deine Datei so aussehen:
data class ProfileUiState(
val name: String,
val email: String,
val isLoading: Boolean,
val errorMessage: String?
)
@Composable
fun ProfileScreen(
state: ProfileUiState,
onRetryClick: () -> Unit,
modifier: Modifier = Modifier
) {
when {
state.isLoading -> LoadingContent(modifier)
state.errorMessage != null -> ErrorContent(
message = state.errorMessage,
onRetryClick = onRetryClick,
modifier = modifier
)
else -> ProfileContent(
name = state.name,
email = state.email,
modifier = modifier
)
}
}
@Composable
private fun LoadingContent(modifier: Modifier = Modifier) {
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
@Composable
private fun ErrorContent(
message: String,
onRetryClick: () -> Unit,
modifier: Modifier = Modifier
) {
Column(modifier = modifier.padding(16.dp)) {
Text(text = message)
Button(onClick = onRetryClick) {
Text("Erneut versuchen")
}
}
}
@Composable
private fun ProfileContent(
name: String,
email: String,
modifier: Modifier = Modifier
) {
Column(modifier = modifier.padding(16.dp)) {
Text(text = name, style = MaterialTheme.typography.titleLarge)
Text(text = email, style = MaterialTheme.typography.bodyMedium)
}
}
Die öffentliche Oberfläche dieser Datei ist überschaubar: ProfileUiState und ProfileScreen dürfen von außen genutzt werden. Die drei Hilfs-Composables sind private, weil sie nur den internen Aufbau des Screens beschreiben. Wenn du später das Layout änderst, einzelne Elemente zusammenlegst oder weitere Zustände einführst, betrifft das nicht automatisch andere Dateien.
Eine gute Entscheidungsregel lautet: Beginne mit der kleinsten sinnvollen Sichtbarkeit und erweitere sie erst, wenn es einen echten Aufrufer gibt. Wenn eine Funktion nur in derselben Datei gebraucht wird, nutze private. Wenn sie innerhalb eines Gradle-Moduls von mehreren Dateien gebraucht wird, aber nicht Teil der Modul-API sein soll, prüfe internal. Wenn andere Module oder Schichten sie bewusst verwenden sollen, ist public passend. protected verwendest du nur, wenn Unterklassen ein Detail wirklich überschreiben oder nutzen müssen.
In einem modularen Android-Projekt kann internal dir helfen, Grenzen zu schützen. Ein Modul :core-network könnte einen öffentlichen UserApi bereitstellen, aber konkrete Implementierungen, Mapper oder Retrofit-Details internal halten. Das App-Modul sieht dann nur die API, nicht jedes technische Detail. Dadurch bleibt ein Austausch der Implementierung realistischer.
Eine typische Stolperfalle ist zu viel public aus Gewohnheit. Besonders bei Hilfsfunktionen, Mappern und Compose-Bausteinen entsteht schnell eine unübersichtliche API. Später weiß niemand mehr, ob eine Funktion gefahrlos geändert werden darf, weil sie theoretisch überall genutzt werden könnte. Die zweite Stolperfalle ist falsches Vertrauen in protected. Vererbung erzeugt enge Kopplung zwischen Basisklasse und Unterklassen. Wenn Unterklassen viele protected Details kennen müssen, ist die Basisklasse oft zu mächtig oder zu unklar geschnitten.
Du kannst dein Verständnis gut in Code-Reviews prüfen. Frage bei jeder neuen Klasse: Was ist die absichtliche Außenseite? Welche Funktionen sind reine Umsetzung? Welche Elemente müssen für Tests sichtbar sein, und welche Tests prüfen nur interne Schritte statt Verhalten? Auch der Compiler hilft dir: Setze eine Funktion testweise auf private oder internal und beobachte, welche Aufrufer brechen. Diese Fehlermeldungen zeigen dir, welche Abhängigkeiten dein Code wirklich hat.
Fazit
Visibility Modifier sind ein kleines Kotlin-Werkzeug mit großer Wirkung auf Wartbarkeit. Wenn du public, private, internal und protected bewusst einsetzt, machst du klare Zusagen darüber, welcher Code genutzt werden darf und welcher Code nur Umsetzung ist. Übe das an einer bestehenden Android-Datei: Markiere Hilfsfunktionen als private, prüfe Modulgrenzen mit internal, lasse Tests gegen sichtbares Verhalten laufen und besprich in einem Code-Review, ob die öffentliche API wirklich so groß sein muss.