13

I have a flow of Episode from a room database. I can observe as livedata this flow with no problem.

But I also would like to read the last value from this flow when the user clicks on a button. I tried with first() terminal flow operator but it does not compile. Can you please help or suggest something else?

The non-compiling attempt to read from a flow:

bd.buttonNext.setOnClickListener {
    lifecycleScope.launch {
        val episode: Episode? = viewModel.episodeFlow().first()           <=== Compile ERROR
        Snackbar.make(bd.root, "episode ${episode?.name}", Snackbar.LENGTH_SHORT).show()
    }
}

This flow comes from ROOM :

@Query("SELECT * FROM Episode WHERE id = :id")
fun getEpisode(id: Long): Flow<Episode?>

Repository:

fun getEpisode(episodeId: Long): Flow<Episode?> = dao.getEpisode(episodeId)

View model - The Id comes from a StateFlow :

fun episodeFlow(): Flow<Episode?> 
  = episodeIdStateFlow.flatMapLatest { episodeId ->
    repository.getEpisode(episodeId)
  }

Compilation error :

Unresolved reference. None of the following candidates is applicable because of receiver type mismatch: 
public fun <T> Array<out TypeVariable(T)>.first(): TypeVariable(T) defined in kotlin.collections
public inline fun <T> Array<out TypeVariable(T)>.first(predicate: (TypeVariable(T)) -> Boolean): TypeVariable(T) defined in kotlin.collections
public fun BooleanArray.first(): Boolean defined in kotlin.collections
public inline fun BooleanArray.first(predicate: (Boolean) -> Boolean): Boolean defined in kotlin.collections
public fun ByteArray.first(): Byte defined in kotlin.collections
public inline fun ByteArray.first(predicate: (Byte) -> Boolean): Byte defined in kotlin.collections
public fun CharArray.first(): Char defined in kotlin.collections
public inline fun CharArray.first(predicate: (Char) -> Boolean): Char defined in kotlin.collections
public fun CharSequence.first(): Char defined in kotlin.text

As a "workaround", I saved the episode in an instance variable like this but I would like to avoid doing it if possible :

var episode: Episode? = null
...
viewModel.episodeFlow().asLiveData().observe(this) { episode ->
   this.episode = episode
}
...
bd.buttonNext.setOnClickListener {
   Snackbar.make(bd.root, "episode ${episode?.name}", Snackbar.LENGTH_SHORT).show()
}

=================== UPDATE/SOLUTION 21/01/15 ==================

Solution inspired by beigirad (see his post below) using stateIn :

private val _episodeSF  = episodeFlow().stateIn(viewModelScope, SharingStarted.Eagerly, null)
val episodeSF: StateFlow<Episode?>
  get() = _episodeSF

fun episodeFlow(): Flow<Episode?> = episodeIdSF.flatMapLatest { episodeId ->
  repository.episodeFlow(episodeId)
}
beigirad
  • 4,986
  • 2
  • 29
  • 52
u2gilles
  • 6,888
  • 7
  • 51
  • 75

2 Answers2

14

You can convert your flow to StateFlow by stateIn extension and then use its value attribute to get the latest value.

fun episodeFlow(): StateFlow<Episode?> 
  = episodeIdStateFlow.flatMapLatest { episodeId ->
    repository.getEpisode(episodeId)
  }.stateIn(viewModelScope, SharingStarted.Lazily, initialValue)
beigirad
  • 4,986
  • 2
  • 29
  • 52
0

The code that you've pasted should work since launch's callback already runs inside a coroutine and you can call suspend functions from there.

A non blocking solution could be that you use LiveData or StateFlow. On both you can check for the property value which will be null if no values were emitted or will contain the last emitted value.

Could you provide more of the ViewModel code?

Some random IT boy
  • 7,569
  • 2
  • 21
  • 47