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
ExternalNotesFileManagerclass, you create awriteTextFile()function. This function writes a note to external storage. First, you check if external storage is writable using theisExternalStorageWritable()function. If it’s not writable, you return early. TheisExternalStorageWritable()function checks if the external storage state isMEDIA_MOUNTED. -
You create a new
Fileobject calleddirectorythat points to the external storage directory. You use thecontext.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
Fileobject calledfilethat points to the note file you want to write. You use the note’s title as the file name. -
You serialize the
NoteEntityobject to a JSON string using theJson.encodeToString()function. You then write the string to the file using theoutputStream.write()function. You convert the description to a byte array using thetoByteArray()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 theisExternalStorageReadable()function. If it’s not readable, you return an empty list. The IDE will show an error because theisExternalStorageReadable()function isn’t yet implemented. You’ll implement it next. -
You create a new
Fileobject calleddirectorythat points to the external storage directory. You use thecontext.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.txtextension. -
You create an empty list called
notesto hold the notes you read from external storage. -
You loop through each file in the
noteFileslist. You read the file’s content using theFileInputStreamandbufferedReader().use { it.readText() }functions. -
You deserialize the content to a
NoteEntityobject using theJson.decodeFromString()function and add it to thenoteslist. -
You add the
NoteEntityobject to thenoteslist. - 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.