'Why does a Flow which return records from Room keep to emit data when I add onStart { }? Is it bugs of Room or Compose?

I query records and return them as Flow<List<RecordEntity>> with Room.

I run code A, and get result A as I expected.

I hope to display a Loading UI before I get the query records, so I add .onStart() to the end of Flow<Result<List<MRecord>>> and assign Result.Loading before I query records in code B.

I run code B. I find the system keeps emitting data, you can see result B.

What's wrong with my code B?

Code A

@Composable
fun Greeting(
    name: String,
    mViewMode:SoundViewModel= viewModel()
) {
    Column(
        
    ) {
        val myResult by mViewMode.listRecord().collectAsState(initial =Result.Error(Exception()) )

       when (val ss = myResult){
            is Result.Error  ->  { Log.e("My","Is Error")   }
            is Result.Loading -> { Log.e("My","Is loading") }
            is Result.Success -> { Log.e("My","Success")    }
        }
}


@HiltViewModel
class SoundViewModel @Inject constructor(
    private val aSoundMeter: RecordRepository
): ViewModel()
{
    fun listRecord(): Flow<Result<List<MRecord>>> {
        return  aSoundMeter.listRecord()
    }

}


@Dao
interface  RecordDao { 
    @Query("SELECT * FROM record_table ORDER BY createdDate desc")
    fun listRecord():  Flow<List<RecordEntity>>
}


sealed class Result<out R> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
    object Loading : Result<Nothing>()
}


class RecordRepository @Inject constructor(private val mRecordDao:RecordDao): IRecordRepository {
    override fun listRecord(): Flow<Result<List<MRecord>>> {      
        val data : Flow<Result<List<MRecord>>> = mRecordDao.listRecord().map { Result.Success(listEntityToModel(it)) }      
        return data  //It's OK
    }
}

Result A

2022-04-04 12:07:09.766 20036-20036/info.dodata.soundmeter E/My: Is Error

2022-04-04 12:07:10.142 20036-20036/info.dodata.soundmeter E/My: Success

Code B

//...The same with Code A

class RecordRepository @Inject constructor(private val mRecordDao:RecordDao): IRecordRepository {

    override fun listRecord(): Flow<Result<List<MRecord>>> {
        val ini:   Result<List<MRecord>> =Result.Loading
        val data : Flow<Result<List<MRecord>>> = mRecordDao.listRecord().map { Result.Success(listEntityToModel(it)) }
        return data.onStart { emit(ini)}        
    }
}

Result B

2022-04-04 12:08:09.941 20124-20124/info.dodata.soundmeter E/My: Is Error

2022-04-04 12:08:10.233 20124-20124/info.dodata.soundmeter E/My: Is loading

2022-04-04 12:08:10.290 20124-20124/info.dodata.soundmeter E/My: Success

2022-04-04 12:08:10.306 20124-20124/info.dodata.soundmeter E/My: Is loading

2022-04-04 12:08:10.324 20124-20124/info.dodata.soundmeter E/My: Success

2022-04-04 12:08:10.338 20124-20124/info.dodata.soundmeter E/My: Is loading

2022-04-04 12:08:10.355 20124-20124/info.dodata.soundmeter E/My: Success

2022-04-04 12:08:10.371 20124-20124/info.dodata.soundmeter E/My: Is loading

2022-04-04 12:08:10.389 20124-20124/info.dodata.soundmeter E/My: Success

2022-04-04 12:08:10.405 20124-20124/info.dodata.soundmeter E/My: Is loading

2022-04-04 12:08:10.423 20124-20124/info.dodata.soundmeter E/My: Success

2022-04-04 12:08:10.440 20124-20124/info.dodata.soundmeter E/My: Is loading

2022-04-04 12:08:10.454 20124-20124/info.dodata.soundmeter E/My: Success

2022-04-04 12:08:10.592 20124-20124/info.dodata.soundmeter E/My: Success

2022-04-04 12:08:10.605 20124-20124/info.dodata.soundmeter E/My: Is loading

2022-04-04 12:08:10.637 20124-20124/info.dodata.soundmeter E/My: Success

2022-04-04 12:08:10.659 20124-20124/info.dodata.soundmeter E/My: Success

...

Added Content

I run Code C and get Result C.

It seems that listRecord() is correct, and return data.onStart { emit(ini)} doesn't keep to emit.

What's wrong with Code B?

Code C

@Composable
fun Greeting(
    name: String,
    mViewMode:SoundViewModel= viewModel()
) {
    Column(      
    ) {
    }

      LaunchedEffect(Unit) {
         Log.e("My","Start")

         val s= mViewMode.listRecord()
          s.collect { i->
            Log.e("My", i.toString())
          }

       }
}

//...The same with Code A

class RecordRepository @Inject constructor(private val mRecordDao:RecordDao): IRecordRepository {

    override fun listRecord(): Flow<Result<List<MRecord>>> {
        val ini:   Result<List<MRecord>> =Result.Loading
        val data : Flow<Result<List<MRecord>>> = mRecordDao.listRecord().map { Result.Success(listEntityToModel(it)) }
        return data.onStart { emit(ini)}        
    }
}

Result C

2022-04-04 17:34:38.370 23855-23855/info.dodata.soundmeter E/My: Start

2022-04-04 17:34:38.379 23855-23855/info.dodata.soundmeter E/My: info.dodata.soundmeter.domain.model.Result$Loading@85ca168

2022-04-04 17:34:38.450 23855-23855/info.dodata.soundmeter E/My: Success(data=[MRecord(id=13, createdDate=java.util.GregorianCalendar[time=1648902202034,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=java.util.SimpleTimeZone[id=GMT,offset=0,dstSavings=3600000,useDaylight=false,startYear=0,startMode=0,startMonth=0,startDay=0,startDayOfWeek=0,startTime=0,startTimeMode=0,endMode=0,endMonth=0,endDay=0,endDayOfWeek=0,endTime=0,endTimeMode=0],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2022,MONTH=3,WEEK_OF_YEAR=14,WEEK_OF_MONTH=1,DAY_OF_MONTH=2,DAY_OF_YEAR=92,DAY_OF_WEEK=7,DAY_OF_WEEK_IN_MONTH=1,AM_PM=1,HOUR=0,HOUR_OF_DAY=12,MINUTE=23,SECOND=22,MILLISECOND=34,ZONE_OFFSET=0,DST_OFFSET=0], min=0.0, avg=0.0, max=0.0, level=Quiet, alarmed=false, soundFileName=, description=OK Sat Apr 02 12:23:22 GMT 2022), MRecord(id=12, createdDate=java.util.GregorianCalendar[time=1648902199093,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=java.util.SimpleTimeZone[id=GMT,offset=0,dstSavings=3600000,useDaylight=false,startYear=0,startMode=0,startMonth=0,startDay=0,startDayOfWeek=0,startTime=0,startTimeMode=0,endMode=0,endMonth=0,endDay=0,endDayOfWeek=0,endTime=0,endTimeMode=0],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2022,MONTH=3,WEEK_OF_YEAR=14,WEEK_OF_MONTH=1,DAY_OF_MONTH=2,DAY_OF_YEAR=92,DAY_OF_WEEK=7,DAY_OF_WEEK_IN_MONTH=1,AM_PM=1,HOUR=0,HOUR_OF_DAY=12,MINUTE=23,SECOND=19,MILLISECOND=93,ZONE_OFFSET=0,DST_OFFSET=0], min=0.0, avg=0.0, max=0.0, level=Quiet, alarmed=false, soundFileName=, description=OK Sat Apr 02 12:23:19 GMT 2022), MRecord(id=11, 
...

Latest Content

To nglauber: Thanks!

I update my code as Code D and get Result D based your thinking.

There are a couple problems:

Question 1: I collect flow as State in Compose, and you collect flow as hot flow in ViewModel, it will retain memeory and waster resources, is it a good way?

Question 2: If I add a record in the Compose UI, the override fun listRecord() should be re-launched, emit(Result.Loading) should be re-launched, but in fact only emitAll(s) is re-launched the result just like Result E.

Code D

@Composable
fun Greeting(
    name: String,
    mViewMode:SoundViewModel= viewModel()
) {
    Column(
        
    ) {
       val myResult by mViewMode.uiState.collectAsState()

       when (val ss = myResult){
            is Result.Error  ->  { Log.e("My","Is Error")   }
            is Result.Loading -> { Log.e("My","Is loading") }
            is Result.Success -> { Log.e("My","Success")    }
        }
}

@HiltViewModel
class SoundViewModel @Inject constructor(
    private val aSoundMeter: RecordRepository
): ViewModel()
{
private var loadBooksJob: Job? = null

    private val _uiState = MutableStateFlow< Result<List<MRecord>> > ( Result.Error(Exception()) )
    val uiState = _uiState.asStateFlow()

    init {
        loadBooks()
    }

    fun loadBooks() {
        loadBooksJob?.cancel()
        loadBooksJob = viewModelScope.launch {
            listRecord().collect { resultState ->
                _uiState.value= resultState
            }
        }
    }

    fun listRecord(): Flow<Result<List<MRecord>>> {
        return  aSoundMeter.listRecord()
    }

}

class RecordRepository @Inject constructor(private val mRecordDao:RecordDao): IRecordRepository {

    override fun listRecord(): Flow<Result<List<MRecord>>> {

        return flow {
           emit(Result.Loading)
           delay(10)
           val s=mRecordDao.listRecord().map { Result.Success(listEntityToModel(it)) }
           emitAll(s)
        }
    }
}

//...The same with Code A

Result D

2022-04-09 10:06:30.779 8287-8287/info.dodata.soundmeter E/My: Is loading
2022-04-09 10:06:31.146 8287-8287/info.dodata.soundmeter E/My: Success

Result E (After I add a record in Compose UI)

2022-04-09 10:06:30.779 8287-8287/info.dodata.soundmeter E/My: Is loading
2022-04-09 10:06:31.146 8287-8287/info.dodata.soundmeter E/My: Success
2022-04-09 10:06:31.146 8287-8287/info.dodata.soundmeter E/My: Success


Solution 1:[1]

I'm not sure if I totally understood your question, but I did a similar implementation in my project in terms of displaying a loading indicator before show the the data.

This is what I did: In my repository, I wrapped the room call inside of a flow block, and there, I emit a loading result and then emit the result of the Room flow. Like this:

override fun loadBooks(): Flow<ResultState<List<Book>>> {
    return flow {
        emit(ResultState.Loading)
        delay(5000) // this is a fake delay 
                    // just to give a change to show the indicator
        emitAll(
            bookDao.bookByTitle().map { books ->
                ResultState.Success(books.map { book ->
                    BookConverter.toData(book)
                })
            }
        )
    }
}

See the complete code here.

Bring idea to your example, it would be something like:

override fun listRecord(): Flow<Result<List<MRecord>>> {
    return flow {
        emit(Result.Loading)
        // give the delay that you want...
        emitAll(
            mRecordDao.listRecord()
                .map { Result.Success(listEntityToModel(it)) }
        )
    }        
}

Also, make sure you're providing the data in your view model and properly close the jobs...

Something like this:

@HiltViewModel
class BookListViewModel @Inject constructor(
    private val bookUseCase: BookUseCase
) : ViewModel() {
    private var loadBooksJob: Job? = null
    
    private val _uiState = MutableStateFlow(BookListUiState())
    val uiState = _uiState.asStateFlow()

    init {
        loadBooks()
    }

    fun loadBooks() {
        loadBooksJob?.cancel()
        loadBooksJob = viewModelScope.launch {
            bookUseCase.listBooks().collect { resultState ->
                _uiState.update {
                    it.copy(bookListState = resultState)
                }
            }
        }
    }
    ...

and finally in the screen...

@Composable
fun BookListScreen(
    viewModel: BookListViewModel,
    ...
) {
    val booksListUiState by viewModel.uiState.collectAsState()
    ...

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1