Build a Markdown Editor with Compose Desktop: File Access | Tutorial #5
Video: Build a Markdown Editor with Compose Desktop: File Access | Tutorial #5 by Taught by Celeste AI - AI Coding Coach
Watch full page →Build a Markdown Editor with Compose Desktop: File Access
In this tutorial, you'll learn how to implement file input/output operations in a Kotlin Compose Desktop Markdown editor. The example covers using File.readText() and File.writeText() for reading and saving files, integrating JFileChooser dialogs for open/save actions, and persisting app settings with JSON serialization.
Code
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import java.io.File
import javax.swing.JFileChooser
import javax.swing.filechooser.FileNameExtensionFilter
// Serializable data class for app settings
@Serializable
data class AppState(val lastOpenedFilePath: String? = null, val darkMode: Boolean = true)
// Singleton FileManager for file operations and config persistence
object FileManager {
private val configFile = File(System.getProperty("user.home"), ".mdeditor_config.json")
private val json = Json { prettyPrint = true }
var appState: AppState = loadConfig()
fun loadConfig(): AppState {
return if (configFile.exists()) {
try {
json.decodeFromString(configFile.readText())
} catch (e: Exception) {
AppState()
}
} else {
AppState()
}
}
fun saveConfig(state: AppState) {
configFile.writeText(json.encodeToString(state))
}
fun openFileDialog(): File? {
val chooser = JFileChooser()
chooser.fileFilter = FileNameExtensionFilter("Markdown files", "md", "markdown", "txt")
val result = chooser.showOpenDialog(null)
return if (result == JFileChooser.APPROVE_OPTION) chooser.selectedFile else null
}
fun saveFileDialog(): File? {
val chooser = JFileChooser()
chooser.fileFilter = FileNameExtensionFilter("Markdown files", "md", "markdown", "txt")
val result = chooser.showSaveDialog(null)
return if (result == JFileChooser.APPROVE_OPTION) chooser.selectedFile else null
}
}
@Composable
fun MarkdownEditor() {
var content by remember { mutableStateOf("") }
var currentFile by remember { mutableStateOf(null) }
MaterialTheme(colorScheme = if (FileManager.appState.darkMode) darkColorScheme() else lightColorScheme()) {
Column(modifier = Modifier.fillMaxSize().padding(8.dp)) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = {
FileManager.openFileDialog()?.let { file ->
content = file.readText()
currentFile = file
FileManager.appState = FileManager.appState.copy(lastOpenedFilePath = file.absolutePath)
FileManager.saveConfig(FileManager.appState)
}
}) {
Text("Open")
}
Button(onClick = {
val file = currentFile ?: FileManager.saveFileDialog()
file?.let {
it.writeText(content)
currentFile = it
FileManager.appState = FileManager.appState.copy(lastOpenedFilePath = it.absolutePath)
FileManager.saveConfig(FileManager.appState)
}
}) {
Text("Save")
}
}
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = content,
onValueChange = { content = it },
modifier = Modifier.fillMaxSize(),
textStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace),
label = { Text("Markdown Content") },
singleLine = false,
maxLines = Int.MAX_VALUE
)
}
}
}
Key Points
- Use
File.readText()andFile.writeText()for straightforward file reading and writing in Kotlin. JFileChooserwithFileNameExtensionFilterenables native open/save dialogs filtered by file type.- Persist app configuration using
@Serializabledata classes andkotlinx.serializationJSON with pretty printing. - A singleton object (like
FileManager) centralizes file operations and config management for easier maintenance. - Use
OutlinedTextFieldwith monospace font for a comfortable code editing experience in Compose Desktop.