'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 |
