The following must be fixed in your DAO:
- As noted in the answer to your previous question, when a function returns a Flow, it should't also be suspending.
- Limiting the result to a single entry isn't needed when selecting only a single id (assuming
id is annotated with @PrimaryKey in your entity and is therefore guaranteed to be unique).
- When an id is passed that has no corresponding entry in the database, the database will return
null. To prevent your app from crashing in that case, you need to explicitly allow the flow to also accept null values, i.e. Flow<Note?>.
Your DAO function should therefore look like this:
@Query("SELECT * FROM notes WHERE id = :id")
fun getSingleNoteByID(id: Int): Flow<Note?>
Now, the view model cannot simply obtain the flow from this function in a property as you did it with getAllByDate(), because you somehow need to provide the id that is unknown at this point.
The solution is to use a flow for the selected id itself and manage it in the view model. Then you can base the database flow on that flow.
You can create a MutableStateFlow in the view model like this:
private val selectedNoteId = MutableStateFlow<Int?>(null)
fun selectNote(id: Int) {
selectedNoteId.value = id
}
A MutableStateFlow is a container for a single value (here initialized with null) which can be observed for changes. You can get and set its value using the value property. The MutableStateFlow is also a fully-fledged flow, so you can do anything with it that you can do with flows in general, like calling flatMapLatest on it:
val selectedNote: StateFlow<Note?> = selectedNoteId
.flatMapLatest {
if (it == null) flowOf(null)
else dao.getSingleNoteByID(it)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5.seconds),
initialValue = null,
)
flatMapLatest replaces the current flow (here, selectedNoteId) with another flow, taking the current flow's value into account. And now dao.getSingleNoteByID() can be called with that id. This should only happen, though, when the id isn't null. If it is, a new flow emitting just null is returned instead. Since the dao flow can also contain null, you now cannot differentiate between a not-selected id and a non-existing id anymore. To fix that you could wrap the content of the flow into a dedicated state type like a SelectedNoteState where you could have different objects representing different cases. You could employ sealed interfaces to realize that.
The returning flow is then converted to a StateFlow as usual. This flow can now be used in your composables the same as you do with notesByDate. It doesn't contain a list of notes though, just a single Note - or none (i.e. the value is null) when nothing was selected.
Regarding repositories, they are just simple classes that encapsulate your data sources (f.e. NoteDao) to keep the view model independent from the actual implementation.
Conforming to the Recommended app architecture you would separate the code of your app into a data layer (your database f.e. and repositories) and the ui layer (the view model and your composables). The view model connects the composables with the data layer, accessing only the repositories so the view model can be kept agnostic about the actual data sources, like your Room database.
If, for example, you want to replace Room by another database solution some day, you would just need to update the repository because that is the only place in your app where the Room database was directly accessed. The entire ui layer (including the view model) would stay the same.
A NoteRepository could be as simple as this:
@Singleton
class NoteRepository @Inject constructor(
private val dao: NoteDao,
) {
fun getAllByDate(): Flow<List<Note>> = dao.getAllByDate()
fun getSingleNoteByID(id: Int): Flow<Note?> = dao.getSingleNoteByID(id)
suspend fun upsert(note: Note) = dao.upsert(note)
suspend fun delete(note: Note) = dao.delete(note)
}
The view model would then replace its dependency
private val dao: NoteDao
with
private val noteRepository: NoteRepository
The repository is annotated with @Singleton so your dependency injection will only instantiate this once. If you use the same repository in multiple view models, they will all get the same instance. This is currently not important because the repository does not hold any state itself (it only passes the data from the database through), but when you choose to implement a cache in the future or if the repositry would hold some other state, then this becomes more important.
Repositories can also merge multiple data sources when appropriate, and you can easily switch out the real repository for a dummy implementation when writing unit tests for the ui, keeping the testing setup simple.