Search code examples
androidandroid-jetpack-composeandroid-viewmodelkotlin-flow

MutableStateFlow not emmiting values after launching app or changes in a database


I am new in android development and creating notes app using the room library. I am using MutableStateFlow in viewModel to observe data from the database. But MutableStateFlow not emmiting values when the app launched and also after changes in the database.

viewModel code

package com.example.composenotes.ui.screens

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.composenotes.data.Note
import com.example.composenotes.data.NotesRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject

@HiltViewModel
class HomeScreenViewModel @Inject constructor(notesRepository: NotesRepository) :
    ViewModel() {

    private val _notes: MutableStateFlow<List<Note>> = MutableStateFlow(
        notesRepository.getAllNotes().stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000L),
            initialValue = listOf()
        ).value
    )


    val notes: StateFlow<List<Note>> = _notes
}

compose code

package com.example.composenotes.ui.screens

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.Card
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import com.example.composenotes.data.Note
import com.example.composenotes.ui.NotesAppScreen
import com.example.composenotes.ui.ScaffoldState

@Composable
fun HomeScreen(
    onComposing: (ScaffoldState) -> Unit,
    onNoteClick: (note: Note) -> Unit,
    modifier: Modifier = Modifier,
    onFabClick: () -> Unit,
    viewModel: HomeScreenViewModel = hiltViewModel()
) {
    val context = LocalContext.current

    LaunchedEffect(key1 = Unit) {
        onComposing(
            ScaffoldState(
                topAppBarTitle = context.getString(NotesAppScreen.Start.title),
                fab = {
                    FloatingActionButton(onClick = onFabClick) {
                        Icon(imageVector = Icons.Filled.Add, contentDescription = "Create new note")
                    }
                })
        )
    }

    val notes = viewModel.notes.collectAsState()


    if (notes.value.isEmpty()) {
        NoNotesCreatedMessage()
    } else {
        AllNotes(
            notes = notes.value,
            onNoteClick = onNoteClick,
            modifier = modifier.padding(vertical = 16.dp)
        )
    }

}

@Composable
fun NoNotesCreatedMessage(modifier: Modifier = Modifier) {
    Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        Text(text = "No notes created yet!", fontSize = 24.sp, color = Color.LightGray)
    }
}

@Composable
fun AllNotes(notes: List<Note>, onNoteClick: (note: Note) -> Unit, modifier: Modifier = Modifier) {
    LazyColumn(modifier = modifier, content = {
        items(notes) {
            NoteCard(
                note = it, onNoteClick = onNoteClick, modifier = Modifier.padding(
                    horizontal = 16.dp, vertical = 4.dp
                )
            )
        }
    })
}

@Composable
fun NoteCard(note: Note, onNoteClick: (note: Note) -> Unit, modifier: Modifier = Modifier) {
    Card(modifier = modifier
        .clickable {
            onNoteClick(note)
        }
        .fillMaxWidth()) {
        Column(modifier = Modifier.padding(12.dp)) {
            Text(
                text = note.date,
                fontSize = 10.sp,
                modifier = Modifier.padding(bottom = 8.dp),
                color = Color.Gray
            )

            if (!note.title.isNullOrEmpty()) {
                Text(
                    text = note.title,
                    fontWeight = FontWeight.Bold,
                    maxLines = 1,
                    modifier = Modifier.padding(bottom = 4.dp),
                )
            }
            Text(
                text = note.description, maxLines = 1
            )
        }

    }
}

When I did following changes in viewModel it is working. I want to know why it is not working with MutableStateFlow

package com.example.composenotes.ui.screens

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.composenotes.data.Note
import com.example.composenotes.data.NotesRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject

@HiltViewModel
class HomeScreenViewModel @Inject constructor(notesRepository: NotesRepository) :
    ViewModel() {
    
    val notes: StateFlow<List<Note>> = notesRepository.getAllNotes().stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000L),
        initialValue = listOf()
    )
}


Solution

  • In your existing code snippet what you are doing is that you are getting a flow converting it into a StateFlow, then you just take first-ever value when you say .value

    private val _notes: MutableStateFlow<List<Note>> = MutableStateFlow(
        notesRepository.getAllNotes().stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000L),
            initialValue = listOf()
        ).value
    )
    
    
    val notes: StateFlow<List<Note>> = _notes
    

    Ideally, when you are consuming a room database flow you can do as follows, in the below it is taking the flow returned by getAllNotes() and makes a ```StateFlow`` out of it, under the hood the original flow is being observed while a subscription is there.

    val notes: StateFlow<List<Note>> = notesRepository.getAllNotes().stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000L),
            initialValue = listOf()
        )