Vibe-Coding im Selbstversuch: Wie ich ohne Programmierkenntnisse mit ChatGPT meine erste App entwickelt habe 🚀

Eine eigene Android-App programmieren, ohne Kotlin zu beherrschen, ohne Erfahrung mit Android Studio zu haben und ohne vorher einen mehrmonatigen Programmierkurs zu absolvieren? Vor nicht allzu langer Zeit hätte ich diese Idee vermutlich für ziemlich unrealistisch gehalten. Meine bisherigen Codekenntnisse beschränken sich im Wesentlichen auf HTML und CSS. Damit kann ich Websites erstellen und gestalten, eine native Smartphone-App ist jedoch eine ganz andere Baustelle. Genau deshalb wollte ich ausprobieren, wie weit man heute mit KI kommen kann. Nicht mit dem Ziel, zunächst Android-Entwicklung zu lernen und anschließend eine App zu programmieren. Ich wollte den umgekehrten Weg gehen: Die App sollte entstehen, während die KI den technischen Teil übernimmt. Das Experiment wurde zu einem kleinen Vibe-Coding-Projekt. Und das Ergebnis überraschte mich selbst: Am Ende des Projekts lief meine selbst erstellte Notiz-App nicht nur im Android-Emulator, sondern als richtige APK auf meinem Samsung-Smartphone. In diesem Blogartikel zeige ich, wie ich meine erste kleine App mit ChatGPT erstellt habe.

Ideen und Vorüberlegungen

Softwareentwicklung verändert sich radikal. Wir leben in einer Zeit, in der uns die KI immer mehr Arbeiten abnimmt, uns die Arbeit enorm erleichtert und an manchen Stellen auch wegnimmt. Je nachdem, wie man es betrachtet. Dabei bin ich gar kein Softwareentwickler, sondern Webdesigner. Und auch im Webdesign und der -entwicklung ist KI fleißig bei der Sache. Ganze Webprojekte lassen sich mit künstlicher Intelligenz planen und realisieren.

Bereits vor dem Erscheinen der ersten LLM’s gab es No-Code-Ansätze, bei denen eine Software oder Applikation ganz ohne Programmierkenntnisse erstellt werden konnte. Seit einigen Jahren beherrscht das Thema Vibe-Coding die Welt der Softwareentwicklung. Bei Vibe-Coding dient die künstliche Intelligenz als Co-Pilot und Werkzeug und setzt die Anforderungen des Entwicklers um.

Vibe-Coding erfordert keine Programmierkenntnisse und setzt die Anforderungen des Entwicklers mittels Prompt-Engineering um. Das ist im Jahr 2026 bereits Realität und wird so umgesetzt. Ich habe mir vor zwei Jahren bereits Gedanken dazu gemacht, doch nie fand ich die Zeit und die Motivation, um Vibe-Coding auszuprobieren. Außerdem fühlte sich das für mich irgendwie falsch an. Ich sollte etwas programmieren, ohne ein Programmierer zu sein und ohne eine Programmiersprache zu beherrschen. Da fühlte sich für mich wie Betrug an.

Das wäre so, als könnte ich plötzlich ein Passagierflugzeug fliegen, ohne eine Pilotenausbildung zu haben. Bis ich vor ein paar Tagen einen Artikel auf Heise gelesen hatte, in dem berichtet wurde, wie MIT-Studenten mithilfe der KI ein Jet-Triebwerk innerhalb eines Monats konstruiert haben. Ganz ohne Vorkenntnisse haben sie ein Gasturbinentriebwerk entwickelt. Das hat mich dermaßen fasziniert, dass ich mein altes Vorhaben, eine App mit Vibe-Coding zu erstellen, in Angriff nahm.

Alles begann mit einem Prompt bei ChatGPT:

Ich habe schon eine Website mit deiner Hilfe erstellt und das hat super funktioniert. Jetzt will ich eine einfache, kleine App für Android programmieren, ganz ohne Vorerfahrungen und nur mit der KI. Jetzt weiß ich nicht, was für eine Einsteiger-App geeignet ist. Kannst du mir Ideen geben? Außerdem, kannst du mir einen Überblick geben, aus welchen Komponenten eine App besteht? Bei einer Website ist es in der Regel einfach, ein HTML-Dokument und ein CSS-Dokument sind ausreichend für eine einfache Website. Wie ist es bei einer App?

So nahmen die Dinge ihren Lauf.

Von HTML und CSS zur Android-App

Webentwicklung und App-Entwicklung unterscheiden sich erheblich. Für eine einfache Website reichen im Grunde bereits zwei Dateien: Ein HTML-Dokument beschreibt die Struktur, während CSS für das Erscheinungsbild zuständig ist. Wer möchte, ergänzt JavaScript für Interaktivität.

Bei einer nativen Android-App sieht die Sache komplexer aus. Es gibt ein komplettes Projekt mit verschiedenen Verzeichnissen, Konfigurationsdateien, Ressourcen, Abhängigkeiten, Datenbankkomponenten und natürlich dem eigentlichen Programmcode.

Meine Ausgangslage war deshalb ziemlich eindeutig: HTML und CSS waren mir vertraut, Kotlin und Jetpack Compose dagegen überhaupt nicht.

Ich wollte allerdings auch nicht erst mehrere Monate Kotlin lernen. Das war nicht der Sinn des Experiments. Stattdessen sollte ChatGPT die Rolle eines persönlichen Entwicklers beziehungsweise Programmierassistenten übernehmen. Meine Aufgabe bestand vor allem darin, Entscheidungen zu treffen, Code an die richtigen Stellen zu kopieren, das Ergebnis zu testen und auftretende Fehler zurückzumelden.

Das ist ziemlich genau das, was inzwischen häufig als Vibe-Coding bezeichnet wird.

Welche App eignet sich für den Einstieg?

Am Anfang stand natürlich die Frage, was ich überhaupt programmieren sollte.

Eine Wetter-App, ein Taschenrechner, eine To-do-Liste oder vielleicht ein kleines Spiel? Für das erste Projekt sollte die Anwendung überschaubar bleiben. Gleichzeitig wollte ich aber auch etwas entwickeln, das mehr kann als nur einen Text oder einen Button auf dem Bildschirm anzuzeigen.

Die Entscheidung fiel schließlich auf eine Notiz-App.

Das erwies sich als gute Wahl. Eine Notiz-App ist konzeptionell einfach zu verstehen, bietet aber trotzdem genügend Möglichkeiten, typische Bestandteile einer richtigen Anwendung kennenzulernen.

Das Ziel für die erste Version war überschaubar: Notizen erstellen, speichern, anzeigen, bearbeiten und wieder löschen.

Was zunächst nach fünf einfachen Funktionen klingt, führt bei einer Android-App bereits zu einigen interessanten technischen Herausforderungen.

Android Studio als Entwicklungsumgebung

Als Erstes installierte ich Android Studio. Das ist Googles beziehungsweise JetBrains’ Entwicklungsumgebung für Android-Anwendungen.

Nach dem Anlegen eines neuen Projekts entstand eine Projektstruktur, die mit einer klassischen Website zunächst wenig gemeinsam hat.

Statt index.html und style.css begegneten mir unter anderem Kotlin-Dateien, Gradle-Konfigurationen, Ressourcenordner, ein Android-Manifest und Theme-Dateien.

Als Programmiersprache verwendeten wir Kotlin. Die Benutzeroberfläche wurde mit Jetpack Compose erstellt. Compose verfolgt einen deklarativen Ansatz: Die Oberfläche wird direkt durch Kotlin-Code beschrieben.

Ich musste dafür allerdings nicht zunächst die komplette Funktionsweise lernen. Genau das war Teil des Experiments. ChatGPT erzeugte den benötigten Code, während ich ihn in Android Studio einfügte und das Ergebnis überprüfte.

Der erste Erfolg: Die App läuft im Emulator

Bevor überhaupt eine Notiz gespeichert werden konnte, musste die Anwendung zunächst einmal starten.

Android Studio stellt dafür virtuelle Android-Geräte zur Verfügung. Ich richtete einen virtuellen Pixel 7 ein und startete die Anwendung dort.

Auch das verlief nicht sofort problemlos. Zunächst meldete Android Studio:

No target device found.

Nachdem der Emulator eingerichtet war, dauerte dessen Start ungewöhnlich lange. Es folgte etwas Fehlersuche, bis das virtuelle Smartphone schließlich korrekt lief.

Android Studio

Dann erschien zum ersten Mal unsere Oberfläche mit der Überschrift:

Meine Notizen

Das war zwar noch keine besonders spektakuläre App, aber ein wichtiger Moment: Der selbst erstellte Kotlin-Code lief tatsächlich als native Android-Anwendung.

Room: Die Notizen müssen dauerhaft gespeichert werden

Eine Notiz-App wäre ziemlich nutzlos, wenn sämtliche Einträge nach dem Beenden der Anwendung wieder verschwinden würden.

Deshalb benötigte die App eine lokale Datenbank. Dafür kam Room zum Einsatz. Room gehört zum Android-Ökosystem und stellt eine Abstraktionsschicht für SQLite bereit.

Wir legten dafür mehrere Kotlin-Dateien an.

Note.kt beschreibt eine einzelne Notiz und deren Daten. Zunächst bestand eine Notiz im Wesentlichen aus einer ID, einem Titel und dem eigentlichen Inhalt.

package com.example.meinenotizen

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "notes")
data class Note(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,

    val title: String,

    val content: String,

    val createdAt: Long = System.currentTimeMillis(),

    val isFavorite: Boolean = false
)

NoteDao.kt enthält die Datenbankoperationen. Darüber können Notizen eingefügt, ausgelesen, aktualisiert und gelöscht werden.

package com.example.meinenotizen

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update

@Dao
interface NoteDao {

    @Insert
    suspend fun insertNote(note: Note)

    @Update
    suspend fun updateNote(note: Note)

    @Delete
    suspend fun deleteNote(note: Note)

    @Query("SELECT * FROM notes ORDER BY id DESC")
    suspend fun getAllNotes(): List<Note>
}

NoteDatabase.kt wiederum stellt die eigentliche Room-Datenbank für die Anwendung bereit.

package com.example.meinenotizen

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

@Database(
    entities = [Note::class],
    version = 3,
    exportSchema = false
)
abstract class NoteDatabase : RoomDatabase() {

    abstract fun noteDao(): NoteDao

    companion object {

        @Volatile
        private var INSTANCE: NoteDatabase? = null

        fun getDatabase(context: Context): NoteDatabase {

            return INSTANCE ?: synchronized(this) {

                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    NoteDatabase::class.java,
                    "notes_database"
                )
                    .fallbackToDestructiveMigration()
                    .build()

                INSTANCE = instance

                instance
            }
        }
    }
}
package com.example.meinenotizen

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.example.meinenotizen.ui.theme.MeineNotizenTheme
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            MeineNotizenTheme {
                NotizenApp()
            }
        }
    }
}

@Composable
fun NotizenApp() {

    val context = LocalContext.current

    val database = remember {
        NoteDatabase.getDatabase(context)
    }

    val noteDao = database.noteDao()

    var notizen by remember {
        mutableStateOf<List<Note>>(emptyList())
    }

    var neueNotizOffen by remember {
        mutableStateOf(false)
    }

    var ausgewaehlteNotiz by remember {
        mutableStateOf<Note?>(null)
    }

    val coroutineScope = rememberCoroutineScope()

    LaunchedEffect(Unit) {
        notizen = noteDao.getAllNotes()
    }

    when {

        neueNotizOffen -> {

            NeueNotizBildschirm(

                onSpeichern = { titel, inhalt ->

                    coroutineScope.launch {

                        val neueNotiz = Note(
                            title = titel,
                            content = inhalt
                        )

                        noteDao.insertNote(neueNotiz)
                        notizen = noteDao.getAllNotes()
                        neueNotizOffen = false
                    }
                },

                onAbbrechen = {
                    neueNotizOffen = false
                }
            )
        }

        ausgewaehlteNotiz != null -> {

            NotizBearbeitenBildschirm(

                notiz = ausgewaehlteNotiz!!,

                onSpeichern = { bearbeiteteNotiz ->

                    coroutineScope.launch {

                        noteDao.updateNote(bearbeiteteNotiz)
                        notizen = noteDao.getAllNotes()
                        ausgewaehlteNotiz = null
                    }
                },

                onLoeschen = { notiz ->

                    coroutineScope.launch {

                        noteDao.deleteNote(notiz)
                        notizen = noteDao.getAllNotes()
                        ausgewaehlteNotiz = null
                    }
                },

                onAbbrechen = {
                    ausgewaehlteNotiz = null
                }
            )
        }

        else -> {

            NotizenStartseite(

                notizen = notizen,

                onNeueNotiz = {
                    neueNotizOffen = true
                },

                onNotizAuswaehlen = { notiz ->
                    ausgewaehlteNotiz = notiz
                },

                onFavoritAendern = { notiz ->

                    coroutineScope.launch {

                        val geaenderteNotiz = notiz.copy(
                            isFavorite = !notiz.isFavorite
                        )

                        noteDao.updateNote(geaenderteNotiz)
                        notizen = noteDao.getAllNotes()
                    }
                }
            )
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NotizenStartseite(
    notizen: List<Note>,
    onNeueNotiz: () -> Unit,
    onNotizAuswaehlen: (Note) -> Unit,
    onFavoritAendern: (Note) -> Unit
) {

    var suchText by remember {
        mutableStateOf("")
    }

    var nurFavoriten by remember {
        mutableStateOf(false)
    }

    var sortierung by remember {
        mutableStateOf("Neueste")
    }

    var sortierMenueOffen by remember {
        mutableStateOf(false)
    }

    val gefilterteNotizen = notizen.filter { notiz ->

        val passtZurSuche =
            notiz.title.contains(
                suchText,
                ignoreCase = true
            ) ||
                    notiz.content.contains(
                        suchText,
                        ignoreCase = true
                    )

        val passtZumFavoritenFilter =
            !nurFavoriten || notiz.isFavorite

        passtZurSuche && passtZumFavoritenFilter

    }.let { liste ->

        when (sortierung) {

            "Älteste" ->
                liste.sortedBy { it.createdAt }

            "A–Z" ->
                liste.sortedBy {
                    it.title.lowercase(Locale.getDefault())
                }

            else ->
                liste.sortedByDescending { it.createdAt }
        }
    }

    Scaffold(

        topBar = {

            TopAppBar(
                title = {

                    Text(
                        text = "Meine Notizen",
                        fontWeight = FontWeight.SemiBold
                    )
                }
            )
        },

        floatingActionButton = {

            FloatingActionButton(
                onClick = onNeueNotiz
            ) {

                Text(
                    text = "+",
                    style = MaterialTheme.typography.headlineMedium
                )
            }
        }

    ) { innerPadding ->

        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding)
        ) {

            OutlinedTextField(
                value = suchText,

                onValueChange = {
                    suchText = it
                },

                label = {
                    Text("Notizen durchsuchen")
                },

                modifier = Modifier
                    .fillMaxWidth()
                    .padding(
                        start = 16.dp,
                        end = 16.dp,
                        top = 8.dp
                    ),

                singleLine = true
            )

            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(
                        start = 16.dp,
                        end = 16.dp,
                        top = 10.dp
                    ),

                horizontalArrangement = Arrangement.spacedBy(8.dp)
            ) {

                FilterChip(
                    selected = !nurFavoriten,

                    onClick = {
                        nurFavoriten = false
                    },

                    label = {
                        Text("Alle")
                    }
                )

                FilterChip(
                    selected = nurFavoriten,

                    onClick = {
                        nurFavoriten = true
                    },

                    label = {
                        Text("★ Favoriten")
                    }
                )
            }

            Column(
                modifier = Modifier.padding(
                    start = 16.dp,
                    end = 16.dp,
                    bottom = 4.dp
                )
            ) {

                OutlinedButton(
                    onClick = {
                        sortierMenueOffen = true
                    }
                ) {
                    Text("Sortierung: $sortierung")
                }

                DropdownMenu(
                    expanded = sortierMenueOffen,

                    onDismissRequest = {
                        sortierMenueOffen = false
                    }
                ) {

                    DropdownMenuItem(
                        text = {
                            Text("Neueste zuerst")
                        },

                        onClick = {
                            sortierung = "Neueste"
                            sortierMenueOffen = false
                        }
                    )

                    DropdownMenuItem(
                        text = {
                            Text("Älteste zuerst")
                        },

                        onClick = {
                            sortierung = "Älteste"
                            sortierMenueOffen = false
                        }
                    )

                    DropdownMenuItem(
                        text = {
                            Text("Titel AZ")
                        },

                        onClick = {
                            sortierung = "AZ"
                            sortierMenueOffen = false
                        }
                    )
                }
            }

            if (notizen.isEmpty()) {

                Column(
                    modifier = Modifier
                        .fillMaxSize()
                        .padding(24.dp),

                    verticalArrangement = Arrangement.Center
                ) {

                    Text(
                        text = "Noch keine Notizen",
                        style = MaterialTheme.typography.headlineSmall
                    )

                    Spacer(
                        modifier = Modifier.height(8.dp)
                    )

                    Text(
                        text = "Tippe auf +, um deine erste Notiz zu erstellen.",
                        style = MaterialTheme.typography.bodyLarge
                    )
                }

            } else if (gefilterteNotizen.isEmpty()) {

                Column(
                    modifier = Modifier
                        .fillMaxSize()
                        .padding(24.dp),

                    verticalArrangement = Arrangement.Center
                ) {

                    Text(
                        text = if (nurFavoriten) {
                            "Keine Favoriten gefunden."
                        } else {
                            "Keine passende Notiz gefunden."
                        },

                        style = MaterialTheme.typography.bodyLarge
                    )
                }

            } else {

                LazyColumn(
                    modifier = Modifier
                        .fillMaxSize()
                        .padding(
                            start = 16.dp,
                            end = 16.dp,
                            top = 8.dp,
                            bottom = 96.dp
                        ),

                    verticalArrangement = Arrangement.spacedBy(12.dp)
                ) {

                    items(
                        items = gefilterteNotizen,
                        key = { notiz -> notiz.id }
                    ) { notiz ->

                        NotizKarte(
                            notiz = notiz,

                            onClick = {
                                onNotizAuswaehlen(notiz)
                            },

                            onFavoritClick = {
                                onFavoritAendern(notiz)
                            }
                        )
                    }
                }
            }
        }
    }
}

@Composable
fun NotizKarte(
    notiz: Note,
    onClick: () -> Unit,
    onFavoritClick: () -> Unit
) {

    Card(
        onClick = onClick,

        modifier = Modifier.fillMaxWidth(),

        elevation = CardDefaults.cardElevation(
            defaultElevation = 2.dp
        )
    ) {

        Column(
            modifier = Modifier.padding(18.dp)
        ) {

            Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.SpaceBetween
            ) {

                Text(
                    text = if (notiz.title.isBlank()) {
                        "Ohne Titel"
                    } else {
                        notiz.title
                    },

                    style = MaterialTheme.typography.titleMedium,
                    fontWeight = FontWeight.SemiBold,

                    modifier = Modifier.weight(1f)
                )

                TextButton(
                    onClick = onFavoritClick
                ) {

                    Text(
                        text = if (notiz.isFavorite) {
                            "★"
                        } else {
                            "☆"
                        },

                        style = MaterialTheme.typography.headlineSmall
                    )
                }
            }

            Text(
                text = formatDatum(notiz.createdAt),
                style = MaterialTheme.typography.bodySmall
            )

            if (notiz.content.isNotBlank()) {

                Spacer(
                    modifier = Modifier.height(8.dp)
                )

                Text(
                    text = notiz.content,
                    style = MaterialTheme.typography.bodyMedium,
                    maxLines = 4
                )
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NeueNotizBildschirm(
    onSpeichern: (String, String) -> Unit,
    onAbbrechen: () -> Unit
) {

    var titel by remember {
        mutableStateOf("")
    }

    var inhalt by remember {
        mutableStateOf("")
    }

    Scaffold(

        topBar = {

            TopAppBar(
                title = {
                    Text("Neue Notiz")
                }
            )
        }

    ) { innerPadding ->

        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding)
                .padding(20.dp)
        ) {

            OutlinedTextField(
                value = titel,

                onValueChange = {
                    titel = it
                },

                label = {
                    Text("Titel")
                },

                modifier = Modifier.fillMaxWidth(),
                singleLine = true
            )

            Spacer(
                modifier = Modifier.height(16.dp)
            )

            OutlinedTextField(
                value = inhalt,

                onValueChange = {
                    inhalt = it
                },

                label = {
                    Text("Notiz")
                },

                modifier = Modifier
                    .fillMaxWidth()
                    .weight(1f),

                minLines = 8
            )

            Spacer(
                modifier = Modifier.height(20.dp)
            )

            Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.spacedBy(12.dp)
            ) {

                OutlinedButton(
                    onClick = onAbbrechen,
                    modifier = Modifier.weight(1f)
                ) {
                    Text("Abbrechen")
                }

                Button(
                    onClick = {

                        if (titel.isNotBlank() || inhalt.isNotBlank()) {

                            onSpeichern(
                                titel,
                                inhalt
                            )
                        }
                    },

                    modifier = Modifier.weight(1f)
                ) {
                    Text("Speichern")
                }
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NotizBearbeitenBildschirm(
    notiz: Note,
    onSpeichern: (Note) -> Unit,
    onLoeschen: (Note) -> Unit,
    onAbbrechen: () -> Unit
) {

    var titel by remember(notiz.id) {
        mutableStateOf(notiz.title)
    }

    var inhalt by remember(notiz.id) {
        mutableStateOf(notiz.content)
    }

    var loeschDialogAnzeigen by remember {
        mutableStateOf(false)
    }

    Scaffold(

        topBar = {

            TopAppBar(
                title = {
                    Text("Notiz bearbeiten")
                }
            )
        }

    ) { innerPadding ->

        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding)
                .padding(20.dp)
        ) {

            OutlinedTextField(
                value = titel,

                onValueChange = {
                    titel = it
                },

                label = {
                    Text("Titel")
                },

                modifier = Modifier.fillMaxWidth(),
                singleLine = true
            )

            Spacer(
                modifier = Modifier.height(16.dp)
            )

            OutlinedTextField(
                value = inhalt,

                onValueChange = {
                    inhalt = it
                },

                label = {
                    Text("Notiz")
                },

                modifier = Modifier
                    .fillMaxWidth()
                    .weight(1f),

                minLines = 8
            )

            Spacer(
                modifier = Modifier.height(20.dp)
            )

            Button(
                onClick = {

                    if (titel.isNotBlank() || inhalt.isNotBlank()) {

                        onSpeichern(
                            notiz.copy(
                                title = titel,
                                content = inhalt
                            )
                        )
                    }
                },

                modifier = Modifier.fillMaxWidth()
            ) {
                Text("Änderungen speichern")
            }

            Spacer(
                modifier = Modifier.height(10.dp)
            )

            OutlinedButton(
                onClick = onAbbrechen,
                modifier = Modifier.fillMaxWidth()
            ) {
                Text("Abbrechen")
            }

            Spacer(
                modifier = Modifier.height(10.dp)
            )

            TextButton(
                onClick = {
                    loeschDialogAnzeigen = true
                },

                modifier = Modifier.fillMaxWidth()
            ) {
                Text("Notiz löschen")
            }
        }
    }

    if (loeschDialogAnzeigen) {

        AlertDialog(

            onDismissRequest = {
                loeschDialogAnzeigen = false
            },

            title = {
                Text("Notiz löschen?")
            },

            text = {
                Text(
                    "Möchtest du diese Notiz wirklich löschen? " +
                            "Dieser Vorgang kann nicht rückgängig gemacht werden."
                )
            },

            confirmButton = {

                Button(
                    onClick = {
                        loeschDialogAnzeigen = false
                        onLoeschen(notiz)
                    }
                ) {
                    Text("Löschen")
                }
            },

            dismissButton = {

                TextButton(
                    onClick = {
                        loeschDialogAnzeigen = false
                    }
                ) {
                    Text("Abbrechen")
                }
            }
        )
    }
}

fun formatDatum(timestamp: Long): String {

    val formatter = SimpleDateFormat(
        "dd.MM.yyyy · HH:mm",
        Locale.getDefault()
    )

    return formatter.format(Date(timestamp))
}

Spätestens an diesem Punkt war das Projekt weit von meinen HTML- und CSS-Kenntnissen entfernt. Trotzdem musste ich nicht im Detail verstehen, wie Room intern arbeitet. Die KI lieferte mir den Code und erklärte mir, in welche Datei er gehörte.

Es gab auch Fehler

Vibe-Coding bedeutet allerdings nicht, dass man einen Prompt eingibt und fünf Minuten später eine perfekte Anwendung erhält.

Ein gutes Beispiel war die Einrichtung von Room. Das Projekt verwendete eine sehr aktuelle Version des Android Gradle Plugins mit integriertem Kotlin. Dadurch kam es zu einem Konflikt mit KSP und den Kotlin Source Sets.

Android Studio meldete unter anderem:

Using kotlin.sourceSets DSL to add Kotlin sources is not allowed with built-in Kotlin.

Dazu kamen zunächst nicht auflösbare Room-Abhängigkeiten, weil eine falsche Versionsnummer verwendet worden war.

Genau hier zeigte sich für mich eine wichtige Eigenschaft des KI-gestützten Programmierens: Die KI produziert nicht zwangsläufig auf Anhieb den richtigen Code.

Der entscheidende Vorteil besteht vielmehr darin, dass man ihr die Fehlermeldung zurückgeben kann.

Ich kopierte die Fehlermeldungen oder schickte Screenshots aus Android Studio. Anschließend wurde die Ursache analysiert und der nächste Lösungsweg vorgeschlagen.

Nach einigen Korrekturen meldete Android Studio schließlich:

BUILD SUCCESSFUL

Die erste dauerhaft gespeicherte Notiz

Danach kam einer der wichtigsten Tests des gesamten Projekts.

Ich erstellte eine Notiz, speicherte sie und beendete die Anwendung. Anschließend startete ich sie erneut.

Die Notiz war noch da.

Damit funktionierte die persistente Speicherung über Room tatsächlich. Aus einer Benutzeroberfläche mit ein paar Eingabefeldern war eine kleine funktionierende Anwendung geworden.

Von diesem Punkt an konnten wir die App Schritt für Schritt erweitern.

Notizen bearbeiten und löschen

Als Nächstes kamen die klassischen CRUD-Funktionen hinzu. CRUD steht für Create, Read, Update und Delete, also Erstellen, Lesen, Aktualisieren und Löschen von Daten.

Eine vorhandene Notiz konnte nun angetippt und anschließend bearbeitet werden.

Außerdem erhielt die App eine Löschfunktion. Anfangs wurde eine Notiz direkt gelöscht. Das war zwar technisch funktional, aber aus Sicht der Benutzerfreundlichkeit nicht optimal.

Deshalb ergänzten wir einen Bestätigungsdialog:

Notiz löschen?“

Erst nach einer zusätzlichen Bestätigung wird der Datensatz tatsächlich aus der Datenbank entfernt.

Damit verhielt sich die Anwendung bereits wesentlich stärker wie eine normale Smartphone-App.

Material 3 statt Entwickler-Prototyp

Bis dahin funktionierte die App, besonders hübsch sah sie aber noch nicht aus.

Deshalb überarbeiteten wir die Oberfläche mit Material 3.

Die Startseite bekam eine richtige App-Leiste. Die einzelnen Notizen wurden als Cards dargestellt und ein schwebender Plus-Button am unteren Bildschirmrand ersetzte den einfachen „Neue Notiz“-Button.

Damit änderte sich der Charakter der Anwendung deutlich. Aus einem technischen Prototyp wurde langsam eine App, die man tatsächlich benutzen konnte.

Datum und Uhrzeit für jede Notiz

Danach erweiterten wir das Datenmodell.

Jede neue Notiz sollte automatisch einen Zeitstempel erhalten. Dazu bekam das Note-Objekt das zusätzliche Feld createdAt.

Beim Anlegen einer neuen Notiz wird seitdem die aktuelle Zeit gespeichert. Auf der Startseite erscheint beispielsweise:

11.08.2026 · 07:30

Weil sich dadurch die Struktur der Datenbank änderte, musste gleichzeitig die Versionsnummer der Room-Datenbank erhöht werden.

Das war für mich ein interessantes Beispiel dafür, wie eng Benutzeroberfläche und Datenmodell bei einer App miteinander verbunden sein können. Eine scheinbar kleine Funktion wie eine Datumsanzeige erfordert eben nicht nur ein zusätzliches Textelement auf dem Bildschirm, sondern auch eine Änderung der gespeicherten Daten.

Eine Suchfunktion kommt hinzu

Mit zunehmender Zahl von Notizen wird eine einfache Liste schnell unübersichtlich.

Also erhielt die App ein Suchfeld.

Die Suche berücksichtigt sowohl den Titel als auch den Inhalt einer Notiz. Bereits während der Eingabe wird die angezeigte Liste gefiltert.

Suche ich beispielsweise nach „Einkauf“, erscheinen nur noch Notizen, in deren Titel oder Inhalt dieser Begriff vorkommt.

Damit wurde aus dem ursprünglichen Experiment langsam eine tatsächlich alltagstaugliche kleine Notiz-App.

Favoriten mit Stern

Als nächste Funktion kamen Favoriten hinzu.

Jede Notiz kann mit einem Stern markiert werden. Der Favoritenstatus wird ebenfalls dauerhaft in der Room-Datenbank gespeichert.

Über zwei Filter lässt sich anschließend zwischen allen Notizen und ausschließlich den Favoriten wechseln:

Alle | ★ Favoriten

Auch hierfür musste das Datenmodell erneut erweitert und die Datenbankversion erhöht werden.

Gerade diese schrittweise Entwicklung gefiel mir an dem Projekt. Wir versuchten nicht, von Anfang an eine komplette Anwendung zu programmieren. Stattdessen entstand zunächst eine minimale Version, die anschließend Funktion für Funktion erweitert wurde.

Notizen sortieren

Zuletzt ergänzten wir noch eine Sortierfunktion.

Die Notizen können inzwischen nach drei Kriterien angezeigt werden:

  • Neueste zuerst
  • Älteste zuerst
  • Titel von A bis Z

Damit lassen sich Suche, Favoritenfilter und Sortierung miteinander kombinieren.

Für ein ursprünglich als kleines Experiment begonnenes Projekt war der Funktionsumfang inzwischen erstaunlich groß geworden.

Ein eigenes Design und App-Icon

Auch optisch sollte die App irgendwann nicht mehr wie ein frisch angelegtes Android-Studio-Projekt aussehen.

Deshalb bekam sie ein eigenes blaues beziehungsweise indigofarbenes Farbschema. Zusätzlich erstellte ich über das Asset Studio von Android Studio ein eigenes Launcher-Icon: ein weißes Dokument-Symbol auf indigofarbenem Hintergrund.

Als Arbeitstitel blieb zunächst „Meine Notizen“ bestehen.

Für eine tatsächliche Veröffentlichung würde ich diesen Namen allerdings ändern. Im Play Store existieren bereits andere Anwendungen mit ähnlichen beziehungsweise identischen Bezeichnungen. Für ein öffentliches Projekt wäre ein eigenständiger Markenname sinnvoller.

Von Android Studio zur APK

Der vielleicht spannendste Schritt folgte zum Schluss.

Bis dahin lief die App hauptsächlich im virtuellen Pixel innerhalb von Android Studio. Aber natürlich wollte ich wissen, ob daraus tatsächlich eine installierbare Android-Anwendung werden kann.

Android Studio erzeugte dafür eine APK:

app-debug.apk

Diese Datei übertrug ich auf mein Samsung-Smartphone und installierte sie dort.

Und tatsächlich: Die App startete auf einem echten Smartphone.

Fertige App

Nicht als Website im Browser, nicht als Simulation und nicht als eingebettete Web-App, sondern als installierte Android-Anwendung mit eigenem App-Symbol.

Ich konnte Notizen erstellen und speichern, die Anwendung schließen und später wieder öffnen. Die gespeicherten Daten waren weiterhin vorhanden.

Damit war das ursprüngliche Experiment erfolgreich abgeschlossen.

Was hat Vibe-Coding dabei tatsächlich geleistet?

Das Interessante an diesem Projekt ist für mich weniger die Notiz-App selbst. Notiz-Apps gibt es schließlich mehr als genug.

Interessant ist vielmehr die Art, wie sie entstanden ist.

Ich konnte vor Beginn des Projekts keine Android-Apps entwickeln. Ich hatte keine praktischen Kotlin-Kenntnisse und kannte Jetpack Compose oder Room nicht aus eigener Entwicklungserfahrung.

Meine Ausgangsbasis waren Kenntnisse in HTML und CSS sowie Erfahrung mit Websites.

Trotzdem konnte ich innerhalb vergleichsweise kurzer Zeit eine native Android-Anwendung erstellen.

Die Arbeitsteilung war dabei ziemlich eindeutig: Ich definierte, was die Anwendung können sollte, traf Entscheidungen über Funktionen und Design, bediente Android Studio und testete das Ergebnis. Die KI erzeugte große Teile des Codes, erklärte die notwendigen Schritte und half bei Fehlermeldungen.

Dabei habe ich schnell eine wichtige Arbeitsweise entwickelt: Statt ständig einzelne Codezeilen an verschiedenen Stellen zu verändern, ließ ich mir zunehmend komplette Dateien neu erzeugen. Ich ersetzte beispielsweise die gesamte MainActivity.kt, wenn eine größere Funktion hinzukam.

Das reduzierte Fehler und machte den Prozess für jemanden ohne Kotlin-Erfahrung wesentlich übersichtlicher.

Vibe-Coding bedeutet nicht fehlerfreies Programmieren

Das Experiment hat aber auch die Grenzen gezeigt.

KI-generierter Code ist nicht automatisch korrekt. Falsche Bibliotheksversionen, inkompatible Konfigurationen oder ungeeignete Imports können genauso auftreten wie beim normalen Programmieren.

Der Unterschied liegt im Umgang damit.

Statt selbst stundenlang Dokumentationen und Foren zu durchsuchen, konnte ich die konkrete Fehlermeldung an die KI weitergeben. Oft ließ sich dadurch schnell eingrenzen, an welcher Stelle das Problem lag.

Das bedeutet allerdings auch: Testen wird beim Vibe-Coding besonders wichtig.

Nur weil ein KI-System behauptet, dass etwas funktionieren müsste, bedeutet das noch lange nicht, dass es funktioniert. Entscheidend war während des gesamten Projekts immer Android Studios BUILD SUCCESSFUL – und letztlich der Test auf dem echten Smartphone.

Vibe-Coding im Selbstversuch – Fazit

Ja, was soll ich sagen? Stolz betrachte ich meine erste, mit der KI erstellte App auf meinem Samsung-Smartphone. Dabei habe ich selbst nur mit Copy & Paste den Code kopiert, den zuvor ChatGPT generiert hat. Aus einer spontanen Idee wurde innerhalb kurzer Zeit eine funktionierende Android-Anwendung. Was mit der Frage begann, ob ich mithilfe von KI überhaupt eine App entwickeln kann, endete innerhalb von ein paar Stunden mit einer APK auf meinem eigenen Smartphone. Die App kann Notizen dauerhaft speichern, bearbeiten und löschen. Sie besitzt eine Suche, Favoriten, Zeitstempel, verschiedene Sortierungen, eine Material-3-Oberfläche und ein eigenes App-Icon. Technisch kommen unter anderem Kotlin, Jetpack Compose und Room zum Einsatz.

Dabei musste ich diese Technologien nicht erst vollständig lernen, bevor ich mit dem Projekt beginnen konnte. Genau darin liegt für mich die spannendste Erkenntnis des Experiments: Vibe-Coding senkt die Schwelle zwischen einer Idee und einem funktionierenden Prototyp enorm. Es macht Fachwissen nicht wertlos und die KI macht durchaus Fehler. Aber sie ermöglicht Menschen, Projekte anzugehen, die sie sich aufgrund fehlender Kenntnisse in einer bestimmten Programmiersprache bisher vielleicht gar nicht zugetraut hätten.

Sladjan Lazic

Comments

Schreiben Sie einen Kommentar

Ihre E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert