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 Internal Storage

Episode complete

Play next episode

Next
Transcript

Creating Internal Notes

Open the starter project in the IDE, then build and run it. You’ll see a text with “No notes available” displayed on the home screen. This is because you still need to add notes. Tap the Create Note button to navigate to the Create Note screen. For now, you won’t be able to fill in the details and create a note. You’ll add this functionality in this lesson.

Start by creating a class that manages all the read and write operations to internal storage. Inside the data package, create a new package named files. Inside this package, create a new class called InternalNotesFileManager. In this class create a writeTextFile() function that writes a note to internal storage. This is how your class will look:

import android.content.Context
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 InternalNotesFileManager(
  private val context: Context
) {
  
  fun writeTextFile(note: NoteEntity) {
    val directory = File(context.filesDir, 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()
    }
  }

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

To break down the code above:

  • You pass Context as a constructor parameter to the InternalNotesFileManager class. This is because you need the context to access the internal storage directory.
  • You create the writeTextFile() function that takes a NoteEntity object as a parameter. This object contains the title and description of the note you want to write to internal storage.
  • You create a new File object called directory that points to the internal storage directory. You create a new directory called DevScribeNotesInternal if it doesn’t exist.
  • You create a new File object called file that points to the file you want to write to. The file name is the title of the note.
  • 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().use function. The use function ensures that the output stream is closed after writing the note.
  • All the code is wrapped in a try-catch block. This handles any exceptions that might occur during the write operation.

Next in the InternalNotesFileManager class, create a readTextFile() function below the writeTextFile() function:

fun readTextFile(): List<NoteEntity> {
  val directory = File(context.filesDir, 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.

In the code above, you:

  • Create a new File object called directory that points to the internal storage directory.
  • Get a list of all the files in the directory that have a .txt extension. You filter out any files that aren’t .txt files. This is because you save notes as .txt files.
  • Create an empty list called notes that will hold all the notes you read from internal storage.
  • Loop through all the note files and read the content of each file using the inputStream.bufferedReader().use function. You deserialize the content to a NoteEntity object using the Json.decodeFromString() function and add it to the notes list.
  • The code is wrapped in a try-catch block to handle any exceptions that might occur during the read operation.
  • You return the list of notes you read from internal storage.

With this, you can now use the InternalNotesFileManager class to read and write notes to internal storage. You’re going to wire up the functionality to the ViewModel and UI next.

First, you need to add the InternalNotesFileManager class to your Koin modules so you can inject it into your ViewModel. Head over to di/Modules.kt and add the following code:

val notesFileManagerModule = module {
  single { InternalNotesFileManager(androidContext()) }
}

Once again, make sure you import InternalNotesFileManager.

Here, you add the InternalNotesFileManager class as a singleton dependency. You pass the androidContext() function as a parameter to the InternalNotesFileManager class. This function provides the context needed to access the internal storage directory. Remember to add the notesFileManagerModule to the appModules list. The list is located at the bottom of the file. notesFileManagerModule will now be added to the dependency graph. Your appModules should now look like this:

val appModules = listOf(
  dataStoreModule,
  viewModelModule,
  notesFileManagerModule
)

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

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

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

Next, add the InternalNotesFileManager class as a dependency to the MainViewModel class. Open the MainViewModel.kt file and modify the class constructor. You will include the InternalNotesFileManager class as a parameter:

class MainViewModel(
  private val dataStoreManager: DataStoreManager,
  private val internalNotesFileManager: InternalNotesFileManager
): ViewModel() {

// The rest of the code
}

Like before, import InternalNotesFileManager.

Now you can use the read and write functions in the InternalNotesFileManager class to read and write notes to internal storage.

Next, in your MainViewModel class, create the handleCreateNoteEvents function. This function will handle all the functionality to create a note:

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)
            }
            else -> {
              // TODO: Implement other note locations
            }
          }
        }
      }
    }
    is CreateNoteEvents.NoteLocationChanged -> {
      _createNoteState.update {
        it.copy(noteLocation = event.noteLocation)
      }
    }
  }
}

Make sure you import com.kodeco.android.devscribe.ui.state.CreateNoteEvents.

The handleCreateNoteEvents(event: CreateNoteEvents) function handles different types of note creation events. The event parameter is of type CreateNoteEvents, which is a sealed class representing different types of events. It is called when you create a note. You have a when statement to handle different types of CreateNoteEvents:

  • CreateNoteEvents.TitleChanged: This event is triggered when the title of the note changes. TitleChanged, updates the _createNoteState StateFlow variable. This variable represents the state of the note you create with the new title.

  • CreateNoteEvents.DescriptionChanged: This event is similar to the title change event. But, it’s triggered when the description of the note changes.

  • CreateNoteEvents.PriorityChanged: This event is triggered when the priority of the note changes. Here you update the _createNoteState with the new priority.

  • CreateNoteEvents.CreateNote: This event is triggered when a user taps the Create Note button in the Create Note Screen. You first check if the current state of the note is valid i.e. all the fields are not null. If it is, a coroutine is launched in the viewModelScope. This creates a NoteEntity object. The object values are set to the current state’s title, description, priority, current time as timestamp, and note location. You then write the NoteEntity to internal storage if the note location that you selected on the UI is “Internal Storage”. If the note location is something else, you don’t do anything for now. You’ll be adding more note locations and functionality to save later on.

  • CreateNoteEvents.NoteLocationChanged: This event is triggered when the location of the note changes. Here, you update the _createNoteState with the new note location.

Next, create a function to read the notes from internal storage. Still in the MainViewModel class below the handleCreateNoteEvents() function, add the following code:

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

The function fetches notes from internal storage. You launch a coroutine in the viewModelScope to read notes from internal storage. The code uses the internalNotesFileManager.readTextFile() function. You then update the _notes StateFlow variable with the notes you read from internal storage.

Next, add a call to the fetchNotes() function in the init block of the MainViewModel class. The modified init block should look like this:

init {
  fetchSelectedFilter()
  fetchNotes()
}

The last part updates your UI code. It will call the handleCreateNoteEvents() function. The call is made when the user fills in the form in the Create Note Screen and taps the Create Note button. Head over to ui/views/CreatNoteScreen.kt. Replace the CreateNoteScreenContent composable inside the Scaffolld content slot with:

CreateNoteScreenContent(
  createNoteState = createNoteState,
  onTitleChange = { title ->
    viewModel.handleCreateNoteEvents(CreateNoteEvents.TitleChanged(title))
  },
  onDescriptionChange = { description ->
    viewModel.handleCreateNoteEvents(CreateNoteEvents.DescriptionChanged(description))
  },
  onPriorityChange = { priority ->
    viewModel.handleCreateNoteEvents(CreateNoteEvents.PriorityChanged(priority))
  },
  onCreateNote = {
    viewModel.handleCreateNoteEvents(CreateNoteEvents.CreateNote)
    navigateBack()
  },
  onNoteLocationChange = { noteLocation ->
    viewModel.handleCreateNoteEvents(CreateNoteEvents.NoteLocationChanged(noteLocation))
  }
)

Add the import com.kodeco.android.devscribe.ui.state.CreateNoteEvents.

In the code above, you update the callbacks for the different user actions. You call the handleCreateNoteEvents() function with the appropriate CreateNoteEvents event. When the user taps the Create Note button, you call the CreateNoteEvents.CreateNote event. You then navigate back to the previous screen.

Build and run your app. You should see an empty screen as you haven’t added any notes yet.

Tap Create Note to navigate to the Create Note screen. Fill in the form with the title, description, select a priority, and select Internal Storage as the note location. Tap Create Note to create the note which saves the note to internal storage and navigates back to the home screen. You should see the note you created displayed on the home screen.

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

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