Back to Blog

Build a Markdown Editor with Compose Desktop: File Access | Tutorial #5

Sandy LaneSandy Lane

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() and File.writeText() for straightforward file reading and writing in Kotlin.
  • JFileChooser with FileNameExtensionFilter enables native open/save dialogs filtered by file type.
  • Persist app configuration using @Serializable data classes and kotlinx.serialization JSON with pretty printing.
  • A singleton object (like FileManager) centralizes file operations and config management for easier maintenance.
  • Use OutlinedTextField with monospace font for a comfortable code editing experience in Compose Desktop.