I'm trying to get to know how to use flows in android app with clean architecture and MVVM using Room database. To do this i want to create basic notes app. Dependency injection is handled by Dagger Hilt.
My biggest problem is that when I expose flow trough my repository class everything works as it should. After adding note to database, flow is emmiting new list with new note added. But when I try to do same thing using use cases, changes in database in not emitted by flow.
My dao seems fine. It looks like that:
@Dao
interface NotesDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun addNote(noteEntity: NoteEntity)
@Update
suspend fun update(noteEntity: NoteEntity)
@Delete
suspend fun delete(noteEntity: NoteEntity)
@Query("SELECT * FROM notes WHERE (:query IS NULL OR :query = '' OR LOWER(title) LIKE '%' || LOWER(:query) || '%' OR LOWER(content) LIKE '%' || LOWER(:query) || '%')")
fun getNotesFlow(query: String? = null): Flow<List<NoteEntity>>
}
Implementation of local data source also works fine:
class LocalNotesDataSourceImpl(
private val notesDao: NotesDao
) : LocalNotesDataSource {
override suspend fun addNote(note: Note) {
notesDao.addNote(note.toNoteEntity())
}
override suspend fun updateNote(note: Note) {
notesDao.update(note.toNoteEntity())
}
override suspend fun deleteNote(note: Note) {
notesDao.delete(note.toNoteEntity())
}
override fun getNotesFlow(query: String?): Flow<List<Note>> {
return notesDao.getNotesFlow(query)
.map { notes ->
notes.map { it.toNote() }
}
}
}
Local data source is used in repository class:
class NotesRepositoryImpl(
private val localNotesDataSource: LocalNotesDataSource
) : NotesRepository {
override suspend fun addNote(note: Note) {
localNotesDataSource.addNote(note)
}
override suspend fun deleteNote(note: Note) {
localNotesDataSource.deleteNote(note)
}
override fun getNotesFlow(query: String?): Flow<List<Note>> {
return localNotesDataSource.getNotesFlow(query)
}
}
And now use cases that are not working properly. It's only using repository to use its methods to be used by view model (for now there is no additional logic in it). They look like that:
class GetNotesFlowUseCase(
private val notesRepository: NotesRepository
) {
fun invoke(query: String? = null) = notesRepository.getNotesFlow(query)
}
class AddNoteUseCase(
private val notesRepository: NotesRepository
) {
suspend fun invoke(note: Note) = notesRepository.addNote(note)
}
In DI i inject use cases like that:
@Module
@InstallIn(SingletonComponent::class)
object UseCaseModule {
@Provides
fun provideGetNotesFlowUseCase(notesRepository: NotesRepository): GetNotesFlowUseCase =
GetNotesFlowUseCase(notesRepository)
@Provides
fun provideAddNoteFlowUseCase(notesRepository: NotesRepository): AddNoteUseCase =
AddNoteUseCase(notesRepository)
}
And finally ui layer:
@HiltViewModel
class NotesListViewModel @Inject constructor(
private val getNotesFlowUseCase: GetNotesFlowUseCase,
private val archiveNoteUseCase: ArchiveNoteUseCase,
) : ViewModel() {
private val _uiState = MutableStateFlow(NoteListUiState())
val uiState = _uiState.asStateFlow()
init {
observeNotesFlow()
}
private fun observeNotesFlow() {
viewModelScope.launch {
//here when i use notesRepository.getNotesFlow(query) this works fine
getNotesFlowUseCase.invoke().collect { notes ->
_uiState.value =
_uiState.value.copy(
notes = notes.map { it.toNoteItemUi() },
isLoading = false
)
}
}
}
fun addNote() {
viewModelScope.launch {
//here when i use notesRepository.addNote(note) this works fine
addNoteUseCase.invoke(
Note(
id = UUID.randomUUID(),
title = "Title",
content = "Content",
timestamp = System.currentTimeMillis(),
isInTrash = false,
isArchived = false
)
)
}
}
}
@AndroidEntryPoint
class NotesListFragment : BaseFragment<FragmentNotesListBinding>() {
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentNotesListBinding =
FragmentNotesListBinding::inflate
private val viewModel: NotesListViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.uiState
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.onEach { uiState ->
Timber.tag("NOTES FLOW").d(uiState.notes.toString())
}
.launchIn(lifecycleScope)
binding.fabAddNote.setOnClickListener {
viewModel.addNote()
}
}
}
So in view model when i use use case to add note, note is added to database but is not emitted as change in flow from getting all notes. Using repository everythhing works. So my question is what is wrong in use cases that they don't work?
It turned out that the problem was in injecting database. It looked like that:
@Module
@InstallIn(SingletonComponent::class)
object FrameworkModule {
@Provides
fun provideNotesDatabase(@ApplicationContext appContext: Context) =
Room.databaseBuilder(
appContext,
NotesDatabase::class.java,
Globals.DATABASE_NAME
).build()
@Provides
fun provideNotesDao(notesDatabase: NotesDatabase): NotesDao =
notesDatabase.notesDao()
}
So everytime I was getting database it was creating a new instance of it. The solution was to add @Singleton
annotation so the database would have only one instance all the time.