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
Contextas a constructor parameter to theInternalNotesFileManagerclass. This is because you need the context to access the internal storage directory. -
You create the
writeTextFile()function that takes aNoteEntityobject as a parameter. This object contains the title and description of the note you want to write to internal storage. -
You create a new
Fileobject calleddirectorythat points to the internal storage directory. You create a new directory calledDevScribeNotesInternalif it doesn’t exist. -
You create a new
Fileobject calledfilethat points to the file you want to write to. The file name is the title of the note. -
You serialize the
NoteEntityobject to a JSON string using theJson.encodeToString()function. You then write the string to the file using theoutputStream().usefunction. Theusefunction 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
Fileobject calleddirectorythat points to the internal storage directory. -
Get a list of all the files in the directory that have a
.txtextension. You filter out any files that aren’t.txtfiles. This is because you save notes as.txtfiles. -
Create an empty list called
notesthat 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().usefunction. You deserialize the content to aNoteEntityobject using theJson.decodeFromString()function and add it to thenoteslist. - 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_createNoteStateStateFlow 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_createNoteStatewith 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 theviewModelScope. This creates aNoteEntityobject. The object values are set to the current state’s title, description, priority, current time as timestamp, and note location. You then write theNoteEntityto 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_createNoteStatewith 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.