How the Muse Script Editor Manages and Persists Scripts Using the SQLDelight Driver
The Muse app persists scripts through a SQLDelight-generated database accessed via platform-specific SqlDriver implementations, with MuseRepo providing coroutine-wrapped CRUD operations that execute on background threads to maintain UI responsiveness.
The Muse application (available at kkoshin/muse) implements a robust local persistence layer for user-generated scripts using SQLDelight. The architecture separates platform-specific database drivers from a shared Kotlin Multiplatform repository, ensuring consistent script management across Android and iOS while keeping database operations off the main thread.
SQLDelight Database Architecture
The persistence layer centers on SQLDelight's code generation capabilities. The build configuration in muse/build.gradle.kts defines the AppDatabase schema, which generates type-safe Kotlin APIs from SQL statements at compile time.
The generated AppDatabase class exposes a scriptQueries property that serves as the data access object (DAO) for the script table. This table stores essential script metadata including UUID-based identifiers, titles, text content, and creation timestamps.
Platform-Specific Driver Implementation
Database connectivity relies on the DriverFactory expect/actual pattern to provide platform-appropriate SqlDriver instances.
On Android, the factory creates an AndroidSqliteDriver:
// muse/src/androidMain/kotlin/io/github/kkoshin/muse/repo/SqlDriver.android.kt
actual class DriverFactory {
actual fun createDriver(): SqlDriver =
AndroidSqliteDriver(AppDatabase.Schema, context, "app.db")
}
For iOS, the implementation uses NativeSqliteDriver:
// muse/src/iosMain/kotlin/io/github/kkoshin/muse/repo/SqlDriver.ios.kt
actual class DriverFactory {
actual fun createDriver(): SqlDriver =
NativeSqliteDriver(AppDatabase.Schema, "app.db")
}
These factories are injected via Koin in appModule.kt to instantiate a singleton AppDatabase that both platforms share:
val db = AppDatabase(DriverFactory().createDriver())
The MuseRepo Repository Layer
MuseRepo (located in muse/src/commonMain/kotlin/io/github/kkoshin/muse/repo/MuseRepo.kt) abstracts all database interactions behind suspend functions. The class receives the generated AppDatabase and exposes high-level methods for script management.
Core CRUD Operations
The repository implements four primary operations:
class MuseRepo(database: AppDatabase, private val pathManager: MusePathManager) {
private val scriptDao = database.scriptQueries
suspend fun queryAllScripts(): List<Script> = withContext(Dispatchers.IO) {
scriptDao.queryAllScripts().executeAsList()
.map { Script(Uuid.parse(it.id), it.title, it.text, it.created_At) }
}
suspend fun queryScript(id: Uuid): Script? = withContext(Dispatchers.IO) {
scriptDao.queryScirptById(id.toString())
.executeAsOneOrNull()
?.let { Script(Uuid.parse(it.id), it.title, it.text, it.created_At) }
}
suspend fun insertScript(script: Script) = withContext(Dispatchers.IO) {
scriptDao.insertScript(
script.id.toString(),
script.title,
script.text,
script.createAt
)
}
suspend fun deleteScript(id: Uuid) = withContext(Dispatchers.IO) {
scriptDao.deleteScriptById(id.toString())
}
}
Each method explicitly switches to Dispatchers.IO before invoking SQLDelight-generated queries, preventing database operations from blocking the UI thread.
Script Creation and Persistence Flow
When users create scripts in ScriptCreatorScreen.kt, the UI delegates persistence to MuseRepo:
// Inside ScriptCreatorScreen.kt – on Save button
scope.launch {
Script(text = content).let {
repo.insertScript(it) // persists via MuseRepo
onResult(it.id) // returns the new ID to the caller
}
}
The repository converts the domain Script object into primitive SQL parameters (converting UUIDs to strings) and executes the insertScript query generated by SQLDelight.
Loading Scripts for Dashboard and Editor
The dashboard retrieves all persisted scripts through DashboardViewModel:
class DashboardViewModel(private val repo: MuseRepo) : ViewModel() {
private val _scripts = MutableStateFlow<List<Script>>(emptyList())
val scripts: StateFlow<List<Script>> = _scripts.asStateFlow()
init {
viewModelScope.launch {
_scripts.value = repo.queryAllScripts()
}
}
}
For the script editor, EditorViewModel provides specialized access that splits script text into phrases:
class EditorViewModel(
private val speechProcessorManager: SpeechProcessorManager,
private val repo: MuseRepo,
) : ViewModel() {
suspend fun queryPhrases(scriptId: String): List<String>? =
repo.queryPhrases(Uuid.parse(scriptId))
}
The EditorScreen.kt consumes this flow to display script content while respecting MAX_TEXT_LENGTH constraints (10,000 characters) to avoid performance degradation during phrase extraction.
Practical Implementation Examples
Inserting a New Script
suspend fun createSampleScript(repo: MuseRepo) {
val script = Script(
id = Uuid.random(),
title = "My First Script",
text = "Hello world! This is a test script.",
createAt = Clock.System.now()
)
repo.insertScript(script) // persisting
println("Inserted script with id ${script.id}")
}
Retrieving Scripts for a List UI
@Composable
fun ScriptList(repo: MuseRepo) {
val scope = rememberCoroutineScope()
var scripts by remember { mutableStateOf<List<Script>>(emptyList()) }
LaunchedEffect(Unit) {
scope.launch {
scripts = repo.queryAllScripts()
}
}
LazyColumn {
items(scripts) { script ->
Text(text = script.title)
}
}
}
Loading Phrases in the Editor
suspend fun loadPhrases(repo: MuseRepo, scriptId: String): List<String> {
return repo.queryPhrases(Uuid.parse(scriptId)) ?: emptyList()
}
Summary
- SQLDelight generates type-safe
AppDatabaseandscriptQueriesDAO from SQL schemas defined inbuild.gradle.kts. - Platform drivers (
AndroidSqliteDriverandNativeSqliteDriver) provide native SQLite backends through theDriverFactoryabstraction. - MuseRepo encapsulates all CRUD logic in suspend functions executing on
Dispatchers.IO, ensuring thread safety. - UI layers (
ScriptCreatorScreen,DashboardViewModel,EditorViewModel) interact with the repository through coroutines, maintaining responsive interfaces during database operations. - The editor implements text segmentation with length limits to optimize performance during content processing.
Frequently Asked Questions
How does Muse handle database threading to prevent UI freezes?
All database operations in MuseRepo wrap SQLDelight queries inside withContext(Dispatchers.IO) blocks. This ensures that SQLite reads and writes execute on background threads, while the repository exposes suspend functions that allow UI components to await results without blocking the main thread.
What SQL driver does Muse use on Android versus iOS?
On Android, Muse uses AndroidSqliteDriver from the SQLDelight Android driver artifact, configured with the application context. On iOS, it uses NativeSqliteDriver from the native driver artifact. Both implement the common SqlDriver interface, allowing MuseRepo to remain platform-agnostic.
Where is the database schema defined in the Muse codebase?
The schema is defined through SQLDelight's Gradle plugin configuration in muse/build.gradle.kts (lines 154-160), which processes .sq files to generate the AppDatabase class and query interfaces. The generated code includes the script table structure and ScriptQueries DAO used by MuseRepo.
How does the editor retrieve script content for display?
EditorViewModel calls repo.queryPhrases(scriptId), which fetches the script by UUID and splits the text into manageable phrases. This method caps processing at MAX_TEXT_LENGTH (10,000 characters) to prevent ANR states during text segmentation, returning a List<String> suitable for the editor UI.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →