Search code examples
androidmvvmandroid-roomandroid-viewmodelkotlin-flow

Problem with observing room database flows trough usecases


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?


Solution

  • 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.