Save User State

Sep 10 2024 · Kotlin 1.9, Android 14, Android Studio Koala | 2024.1.1

Lesson 03: Read & Write Files

Read & Write Files in External Storage

Episode complete

Play next episode

Next
Transcript

Creating External Notes

Continuing from where we left, reopen the starter project, then build and run your app. You should see the note you just created earlier. Tap Create Note to navigate to the Create Note screen. Fill in the form with the title and description, select a priority, and select External Storage as the note location. Tap Create Note to create the note, but your note won’t save to external storage yet. You’ll implement the read and write operations to external storage in the next steps.

Inside the files package you created earlier, create a new class called ExternalNotesFileManager. This class will host functions to read and write to external storage.

Next, create a writeTextFile() function that writes a note to external storage. This is how your class will look:

import android.content.Context
import android.os.Environment
import com.kodeco.android.devscribe.data.local.NoteEntity
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.File
import java.io.IOException

class ExternalNotesFileManager(
  private val context: Context
) {
  fun writeTextFile(note: NoteEntity) {
    if (!isExternalStorageWritable()) {
      return
    }
    val directory = File(context.getExternalFilesDir(null), DIRECTORY_NAME)
    if (!directory.exists()) {
      directory.mkdirs()
    }
    val file = File(directory, "${note.title}.txt")
    try {
      file.outputStream().use { outputStream ->
        outputStream.write(Json.encodeToString(note).toByteArray())
      }
    } catch (e: IOException) {
      e.printStackTrace()
    }
  }

  private  fun isExternalStorageWritable(): Boolean =
    Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED

  companion object {
    const val DIRECTORY_NAME = "DevScribeNotesExternal"
  }
}

You’re doing several things in the code above:

  • In the new ExternalNotesFileManager class, you create a writeTextFile() function. This function writes a note to external storage. First, you check if external storage is writable using the isExternalStorageWritable() function. If it’s not writable, you return early. The isExternalStorageWritable() function checks if the external storage state is MEDIA_MOUNTED.
  • You create a new File object called directory that points to the external storage directory. You use the context.getExternalFilesDir(null) function to get the external storage directory.
  • You check if the directory exists. If it doesn’t, you create it using the mkdirs() function.
  • You create a new File object called file that points to the note file you want to write. You use the note’s title as the file name.
  • You serialize the NoteEntity object to a JSON string using the Json.encodeToString() function. You then write the string to the file using the outputStream.write() function. You convert the description to a byte array using the toByteArray() function.
  • The code is wrapped in a try-catch block to handle any exceptions that might occur during the write operation.

Still in the ExternalNotesFileManager class, create the readTextFile() function below the writeTextFile() function:

fun readTextFile(): List<NoteEntity> {
  if (!isExternalStorageReadable()) {
    return emptyList()
  }
  val directory = File(context.getExternalFilesDir(null), DIRECTORY_NAME)
  val noteFiles = directory.listFiles()?.filter { it.isFile && it.name.endsWith(".txt") }
  val notes = mutableListOf<NoteEntity>()
  noteFiles?.forEach { file ->
    try {
      val inputStream = FileInputStream(file)
      val content = inputStream.bufferedReader().use { it.readText() }
      notes.add(
        Json.decodeFromString(content)
      )
    } catch (e: IOException) {
      e.printStackTrace()
    }
  }
  return notes
}

Make sure you import java.io.FileInputStream. Also, you will need one more function for this code to compile, so disregard the missing function error for now.

In the code above:

  • Inside the readTextFile() function, you first check if external storage is readable. You make this check by using the isExternalStorageReadable() function. If it’s not readable, you return an empty list. The IDE will show an error because the isExternalStorageReadable() function isn’t yet implemented. You’ll implement it next.
  • You create a new File object called directory that points to the external storage directory. You use the context.getExternalFilesDir(null) function to get the external storage directory.
  • You get a list of files in the directory using the listFiles() function. You filter the files to only include files that end with the .txt extension.
  • You create an empty list called notes to hold the notes you read from external storage.
  • You loop through each file in the noteFiles list. You read the file’s content using the FileInputStream and bufferedReader().use { it.readText() } functions.
  • You deserialize the content to a NoteEntity object using the Json.decodeFromString() function and add it to the notes list.
  • You add the NoteEntity object to the notes list.
  • The code is wrapped in a try-catch block to handle any exceptions that might occur during the read operation.

Next, create the isExternalStorageReadable() function similar to the isExternalStorageWritable() function as follows:

private fun isExternalStorageReadable(): Boolean =
  Environment.getExternalStorageState() in
    setOf(Environment.MEDIA_MOUNTED, Environment.MEDIA_MOUNTED_READ_ONLY)

This function checks if the external storage state is either MEDIA_MOUNTED or MEDIA_MOUNTED_READ_ONLY. If it is, the function returns true. Otherwise, it returns false.

Next, add the ExternalNotesFileManager class. You add it to notesFileManagerModule inside the Modules.kt file as follows:

single { ExternalNotesFileManager(androidContext()) }

Make sure you import ExternalNotesFileManager.

As a final step to in Modules.kt, extend the parameters for viewModelModule:

val viewModelModule = module {
  viewModel { MainViewModel(get(), get(), get()) }
}

In the above code, the extra get() parameter prepares viewModel for the following step.

Next, add the ExternalNotesFileManager class as a dependency to the MainViewModel class. Then, update the ViewModel with code to write and read notes to external storage. Open the MainViewModel class and modify the class constructor. Include the ExternalNotesFileManager class as a parameter:

class MainViewModel(
  private val dataStoreManager: DataStoreManager,
  private val internalNotesFileManager: InternalNotesFileManager,
  private val externalNotesFileManager: ExternalNotesFileManager
): ViewModel() {
  // The rest of the code
}

As before, make sure you import ExternalNotesFileManager.

Now you can use the read and write functions in the ExternalNotesFileManager class to read and write notes to external storage.

Next, update the handleCreateNoteEvents to handle the new note location. Continuing in the MainViewModel class, update the is CreateNoteEvents.CreateNote case. Edit the when expression inside the handleCreateNoteEvents() function. Write the note to external storage if the note location is “External Storage”. The updated handleCreateNoteEvents() function should look like this:

fun handleCreateNoteEvents(event: CreateNoteEvents) {
  when(event) {
    is CreateNoteEvents.TitleChanged -> {
      _createNoteState.update {
        it.copy(title = event.title)
      }
    }
    is CreateNoteEvents.DescriptionChanged -> {
      _createNoteState.update {
        it.copy(description = event.description)
      }
    }
    is CreateNoteEvents.PriorityChanged -> {
      _createNoteState.update {
        it.copy(priority = event.priority)
      }
    }
    is CreateNoteEvents.CreateNote -> {
      if(createNoteState.value.isValid()) {
        viewModelScope.launch {
          val noteEntity = NoteEntity(
            title = createNoteState.value.title ?: "",
            description = createNoteState.value.description ?: "",
            priority = createNoteState.value.priority ?: "",
            timestamp = System.currentTimeMillis(),
            noteLocation = createNoteState.value.noteLocation ?: ""
          )
          when(noteEntity.noteLocation) {
            "Internal Storage" -> {
              internalNotesFileManager.writeTextFile(noteEntity)
            }
            "External Storage" -> {
              externalNotesFileManager.writeTextFile(noteEntity)
            }
            else -> {
              // TODO: Implement other note locations
            }
          }
        }
      }
    }

    is CreateNoteEvents.NoteLocationChanged -> {
      _createNoteState.update {
        it.copy(noteLocation = event.noteLocation)
      }
    }
  }
}

Finally, update the fetchNotes() function to read notes from external storage. Continuing in the MainViewModel class, update the fetchNotes() function. Read notes from external storage using the externalNotesFileManager.readTextFile() function. The updated fetchNotes() function should look like this:

private fun fetchNotes() {
  viewModelScope.launch {
    _notes.update {
      internalNotesFileManager.readTextFile() + externalNotesFileManager.readTextFile()
    }
  }
}

You have updated the fetchNotes() function to read notes from both internal and external storage. You use the internalNotesFileManager.readTextFile() and externalNotesFileManager.readTextFile() functions. These will read notes from internal and external storage. You then update the notes state with the notes read from both internal and external storage.

You updated the UI part in the previous section, so now build and run your app. You should see the note that you just created earlier. Tap Create Note to navigate to the Create Note screen. Fill in the form with the title and description, select a priority, and select External Storage as the note location. Tap Create Note to create the note, which saves the note to external storage and navigates back to the home screen. You should see the note you created displayed on the home screen. You’ll also see the ones saved on internal storage, though it will have a different icon to indicate that it’s saved in external storage.

Congratulations! You’ve successfully implemented read and write operations to external storage in your app.

See forum comments
Cinema mode Download course materials from Github
Previous: Read & Write Files in Internal Storage Next: Conclusion