Emit a cold flow from UI Event into combine operator - kotlin-flow

How can I emit a flow into the combine operator of UseCaseFlowCollector based on a user event from onButtonPressed().
My understanding is that when onButtonPressed() is called this will emit a cold flow from useCaseFlowProducer.invoke() that will be collected by combine flow operator in UseCaseFlowCollector.invoke(). However in my current set up below, the combine{} flow lambda in UseCaseFlowCollector.invoke() is never called. What am I missing?
class MainActivityViewModel(
private val useCaseFlowProducer: UseCaseFlowProducer,
private val useCaseFlowCollector: UseCaseFlowCollector
) : ViewModel() {
init {
viewModelScope.launch {
useCaseFlowCollector.invoke().collect {
//collect results from doSomething()
}
}
}
fun onButtonPressed() {
viewModelScope.launch {
useCaseFlowProducer.invoke().collect()
}
}
}
class UseCaseFlowProducer(
private val repository1: Repository1,
private val repository2: Repository2,
) {
operator fun invoke(): Flow<Unit> = flow {
repository1.update()
repository2.update()
emit(Unit)
}
}
class UseCaseFlowCollector(
private val someOtherUseCase: SomeOtherUseCase,
private val useCaseFlowProducer: UseCaseFlowProducer
) {
operator fun invoke(): Flow<List<Result>> =
combine(someOtherUseCase().otherFlow(), UseCaseFlowProducer.invoke())
{ a, b ->
doSomething()
}
}

Related

How to collect StateFlow states from another class?

I have view model like this:
class SimpleViewModel : ViewModel() {
private val _state = MutableStateFlow(false)
val state: StateFlow<Boolean> = _state
}
How can I collect this state's values and call methods from another class like this:
class AnotherClass {
fun doWhenViewModelStateUpdateToTrue()
fun doWhenViewModelStateUpdateToFalse()
}
Your other class needs a reference to the state flow and to a CoroutineScope to run the collection in.
The CoroutineScope should have a lifecycle matching that of this class. So if it's a class you create in an Activity, for example, you would pass lifecycleScope.
class AnotherClass(
private val coroutineScope: CoroutineScope,
private val flowToCollect: Flow<Boolean>
) {
init {
coroutineScope.launch {
flowToCollect.collect {
if (it) doWhenViewModelStateUpdateToTrue()
else doWhenViewModelStateUpdateToFalse()
}
}
}
fun doWhenViewModelStateUpdateToTrue() {
//...
}
fun doWhenViewModelStateUpdateToFalse() {
//...
}
}

realTime List using callbackFlow from firestore

i'm facing hard times updating list of Orders in real time from firestore using stateflow !!
class RepositoryImp : Repository {
private fun Query.snapshotFlow(): Flow<QuerySnapshot> = callbackFlow {
val snapshott = addSnapshotListener { value, error ->
if (error != null) {
close()
return#addSnapshotListener
}
if (value != null)
trySend(value)
}
awaitClose {
snapshott.remove()
}
}
override fun getAllOrders() = flow<State<List<OrderModel>>> {
emit(State.loading())
val snapshot = ORDER_COLLECTION_REF.snapshotFlow()
.mapNotNull { it.toObjects(OrderModel::class.java) }
emit(State.success(snapshot)) // **HERE** !!!!!!
}.catch {
emit(State.failed(it.message.toString()))
}.flowOn(Dispatchers.IO)
}
i'm receiving the error from // emit(State.success(snapshot)) that says :
Type mismatch: inferred type is Flow<(Mutable)List<OrderModel!>> but List< OrderModel> was expected
sealed class State <T> {
class Loading <T> : State<T>()
data class Success <T> (val data: T) : State <T>()
data class Failed <T> (val message: String) : State <T>()
companion object {
fun <T> loading() = Loading <T>()
fun <T> success(data: T) = Success(data)
fun <T> failed(message: String) = Failed<T>(message)
}
}
My fun to LoadOrders :
private suspend fun loadOrders() {
viewModel.getAllOrders().collect { state ->
when (state) {
is State.Loading -> {
showToast("Loading")
}
is State.Success -> {
adapter.submitList(state.data)
}
is State.Failed -> showToast("Failed! ${state.message}")
}
}
}
Your snapshot variable is a Flow of lists, not a single List. If you want to just fetch the current list, you shouldn't use a flow for that. Instead use get().await().
override fun getAllOrders() = flow<State<List<OrderModel>>> {
emit(State.loading())
val snapshot = ORDER_COLLECTION_REF.get().await()
.let { it.toObjects(OrderModel::class.java) }
emit(State.success(snapshot))
}.catch {
emit(State.failed(it.message.toString()))
}.flowOn(Dispatchers.IO)
The flowOn call is actually unnecessary because we aren't doing anything blocking. await() is a suspend function.
Based on comments discussion below, supposing we want to show a loading state only before the first item, then show a series of success states, and we want to show an error and stop emitting once there's an error, we could do:
override fun getAllOrders() = flow<State<List<OrderModel>>> {
emit(State.loading())
val snapshots = ORDER_COLLECTION_REF.snapshotFlow()
.mapNotNull { State.success(it.toObjects(OrderModel::class.java)) }
emitAll(snapshots)
}.catch {
emit(State.failed(it.message.toString()))
}

How can I emit Result.Loading first before I query data and return the data of Result.Success in Android Studio?

I use the following Code A to query records ,the data are wrapped with sealed class Result<out R>.
The val queryList is assigned with Result.Loading first, then it is assigned with Result.Success and wrapped data, the different UI will be loaded based the different value of queryList.
I think the queryList is only assigned with Result.Loading onetime, the queryList will keep return Result.Success when I launch mViewMode.listRecord() again and again, right?
So I hope the queryList is always assigned with Result.Loading before I launch mViewMode.listRecord() and return Result.Success , how can I fix the code?
Maybe do I need to modify Code B? or do I need to redesign data structure? or is there the better solution?
Code A
#Composable
fun Greeting() {
Column( ) {
val aResult: Result<Flow<List<MRecord>>> = Result.Loading
val queryList by produceState(initialValue = aResult) {
value = mViewMode.listRecord()
}
when (queryList){
is Result.Error -> { ...}
is Result.Loading -> { ... }
is Result.Success -> { ... }
}
}
}
class SoundViewModel #Inject constructor(...): ViewModel()
{
fun listRecord(): Result<Flow<List<MRecord>>>{
return aRecordRepository.listRecord()
}
}
class RecordRepository #Inject constructor(private val mRecordDao:RecordDao){
fun listRecord(): Result<Flow<List<MRecord>>> {
val temp = mRecordDao.listRecord()
return Result.Success(temp)
}
}
interface RecordDao {
#Query("SELECT * FROM record_table ORDER BY createdDate desc")
fun listRecord(): Flow<List<MRecord>>
}
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>()
}
Code B
...
class RecordRepository #Inject constructor(private val mRecordDao:RecordDao){
fun listRecord(): Result<Flow<List<MRecord>>> {
val temp = mRecordDao.listRecord()
return Result.Success(temp) //How can I return Result.Loading first, then return Result.Success(temp)?
}
}
...
You can create a StateFlow in your view model representing Result and connect it to your RecordRepository as follows and then convert it to compose state using collectAsState
#Composable
fun Greeting(soundViewModel: SoundViewModel = SoundViewModel()) {
LaunchedEffect(Unit) {
soundViewModel.listRecord()
}
Column {
val queryList: Result by soundViewModel.dataResult.collectAsState()
when (queryList) {
is Result.Error -> {
...
}
is Result.Loading -> {
...
}
is Result.Success -> {
...
}
}
}
}
class SoundViewModel {
private val _dataResult: MutableStateFlow<Result> = MutableStateFlow(Result.Loading) // private mutable state flow
val dataResult = _dataResult.asStateFlow() // publicly exposed as read-only state flow
private val recordRepository = RecordRepository()
suspend fun listRecord() {
recordRepository.listRecord().collect {
_dataResult.value = Result.Success(it)
}
}
}
class RecordRepository {
fun listRecord(): Flow<List<Int>> = flow {
emit(listOf(1))
delay(1000L)
emit(listOf(2, 3))
}
}
sealed interface Result {
object Loading : Result
data class Success(val lst: List<Int>) : Result
data class Error(val err: Throwable) : Result
}
The tricky thing is: When you expose a Flow from Room, it only emits each list after there's a database change and a new query is completed. There is no in-between signal from the flow to indicate that the database change is detected but the new query isn't completed yet.
One possible solution is if you create a flow in your repository that when something that happens modifies the database, it restarts with a new emission of Result.Loading and then emits the DAO flow again. This way, your Flow is protected from missing any changes, even if you somehow miss showing a loading state.
You could use a shared flow in the Repository if there's more than one flow you want to handle this way. Use it with flatMapLatest, so every time you do something that is likely to cause a database change, the existing upstream listRecord flow from the DAO will be cancelled so you can get a new Loading state before collecting it again.
Disclaimer: I haven't tested this. It's only an idea.
class RecordRepository #Inject constructor(private val mRecordDao:RecordDao){
private expectedChangeTicker = MutableSharedFlow<Unit>(replay = 1, bufferOverflow = BufferOverflow.DROP_OLDEST)
suspend fun addSomething(someThing: SomeThing) {
// Call this in every repository function that might cause listRecord to change
expectedChangeTicker.emit(Unit)
mRecordDao.addSomething(someThing)
}
val listRecord: Flow<Result<List<MRecord>> =
expectedChangeTicker.flatMapLatest {
flow {
emit(Result.Loading)
emitAll(mRecordDao.listRecord().map { Result.Success(it) })
}
}
}
I don't know Compose, so I'm not sure how you should expose this Flow in your ViewModel. Notice I changed it from Result<Flow...> to Flow<Result...>, which I think is more likely what you need. Here is my guess at how it should be done:
class SoundViewModel #Inject constructor(...): ViewModel()
{
val listRecord: Flow<Result<List<MRecord>>> =
aRecordRepository.listRecord
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000), replay = 1)
}
#Composable
fun Greeting() {
Column( ) {
val aResult: Result<List<MRecord>> = Result.Loading
val queryList by produceState(initialValue = Result.Loading) {
value = mViewMode.listRecord
}
when (queryList){
is Result.Error -> { ...}
is Result.Loading -> { ... }
is Result.Success -> { ... }
}
}
}
I don't think you need produceState. You can simply collect the flow returned by Dao in your composable using collectAsState() extension function.
#Composable
fun Greeting() {
Column( ) {
val queryList by viewModel.listRecord.collectAsState(Result.Loading)
when (queryList){
is Result.Error -> { ...}
is Result.Loading -> { ... }
is Result.Success -> { ... }
}
}
}
class SoundViewModel #Inject constructor(...): ViewModel() {
val listRecord = aRecordRepository.listRecord()
}
class RecordRepository #Inject constructor(private val mRecordDao:RecordDao) {
fun listRecord(): Flow<List<MRecord>> {
return mRecordDao.listRecord()
}
}
Edit:
If you want to emit the loading state from the flow itself, you can do something like this:
class RecordRepository #Inject constructor(private val mRecordDao: RecordDao) {
fun listRecord(): Flow<Result<List<MRecord>>> {
return flow { // Create a new flow
emit(Result.Loading) // Emit loading state right away
mRecordDao.listRecord().collect {
emit(Result.Success(it)) // Emit success state upon receiving data from dao
}
}
}
}

How to ignore empty database result for the first time and wait for server result in application?

My app using room as a database and retrofit as a network calling api.
i am observing database only as a single source of truth. every thing is working fine. But i am not finding solution of one scenario.
Like for the first time when user open app it do following operations
fetch data from db
fetch data from server
because currently database is empty so it sends empty result to observer which hide progress bar . i want to discard that event and send result to observer when server dump data to database. even server result is empty. so progress bar should always hide once their is confirmation no data exists.
in other words application should always rely on database but if it empty then it should wait until server response and then notify observer.
this is my code
observer
viewModel.characters.observe(viewLifecycleOwner, Observer {
Log.e("status is ", "${it.message} at ${System.currentTimeMillis()}")
when (it.status) {
Resource.Status.SUCCESS -> {
binding.progressBar.visibility = View.GONE
if (!it.data.isNullOrEmpty()) adapter.setItems(ArrayList(it.data))
}
Resource.Status.ERROR -> {
Toast.makeText(requireContext(), it.message, Toast.LENGTH_SHORT).show()
binding.progressBar.visibility = View.GONE
}
Resource.Status.LOADING ->
binding.progressBar.visibility = View.VISIBLE
}
})
ViewModel
#HiltViewModel
class CharactersViewModel #Inject constructor(
private val repository: CharacterRepository
) : ViewModel() {
val characters = repository.getCharacters()
}
Repository
class CharacterRepository #Inject constructor(
private val remoteDataSource: CharacterRemoteDataSource,
private val localDataSource: CharacterDao
) {
fun getCharacters() : LiveData<Resource<List<Character>>> {
return performGetOperation(
databaseQuery = { localDataSource.getAllCharacters() },
networkCall = { remoteDataSource.getCharacters() },
saveCallResult = { localDataSource.insertAll(it.results) }
)
}
}
Utility function for all api and database handling
fun <T, A> performGetOperation(databaseQuery: () -> LiveData<T>,
countQuery: () -> Int,
networkCall: suspend () -> Resource<A>,
saveCallResult: suspend (A) -> Unit): LiveData<Resource<T>> =
liveData(Dispatchers.IO) {
emit(Resource.loading())
val source = databaseQuery().map { Resource.success(it,"database") }.distinctUntilChanged()
emitSource(source)
val responseStatus = networkCall()
if (responseStatus.status == SUCCESS) {
saveCallResult(responseStatus.data!!)
} else if (responseStatus.status == ERROR) {
emit(Resource.error(responseStatus.message!!))
}
}
LocalDataSource
#Dao
interface CharacterDao {
#Query("SELECT * FROM characters")
fun getAllCharacters() : LiveData<List<Character>>
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(characters: List<Character>)
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(character: Character)
}
DataSource
class CharacterRemoteDataSource #Inject constructor(
private val characterService: CharacterService
): BaseDataSource() {
suspend fun getCharacters() = getResult { characterService.getAllCharacters() }}
}
Base Data Source
abstract class BaseDataSource {
protected suspend fun <T> getResult(call: suspend () -> Response<T>): Resource<T> {
try {
Log.e("status is", "started")
val response = call()
if (response.isSuccessful) {
val body = response.body()
if (body != null) return Resource.success(body,"server")
}
return error(" ${response.code()} ${response.message()}")
} catch (e: Exception) {
return error(e.message ?: e.toString())
}
}
private fun <T> error(message: String): Resource<T> {
Timber.d(message)
return Resource.error("Network call has failed for a following reason: $message")
}
}
Character Service
interface CharacterService {
#GET("character")
suspend fun getAllCharacters() : Response<CharacterList>
}
Resource
data class Resource<out T>(val status: Status, val data: T?, val message: String?) {
enum class Status {
SUCCESS,
ERROR,
LOADING
}
companion object {
fun <T> success(data: T,message : String): Resource<T> {
return Resource(Status.SUCCESS, data, message)
}
fun <T> error(message: String, data: T? = null): Resource<T> {
return Resource(Status.ERROR, data, message)
}
fun <T> loading(data: T? = null): Resource<T> {
return Resource(Status.LOADING, data, "loading")
}
}
}
CharacterList
data class CharacterList(
val info: Info,
val results: List<Character>
)
What is the best way by that i ignore database if it is empty and wait for server response and then notify observer

Listening to coroutine from view cant be done from the views init

I am trying to listen to my ViewModels MutableStateFlow from my FlutterSceneView. But I get the following error when trying to set the listener from the views init:
Suspend function 'listenToBackgroundColor' should be called only from a coroutine or another suspend function
class FlutterSceneView(context: Context, private val viewModel: FlutterSceneViewModelType): PlatformView {
private val context = context
private val sceneView = SceneView(context)
init {
listenToBackgroundColor() // Error here
}
private suspend fun listenToBackgroundColor() {
viewModel.colorFlow.collect {
val newColor = Color.parseColor(it)
sceneView.setBackgroundColor(newColor)
}
}
}
My ViewModel:
interface FlutterSceneViewModelType {
var colorFlow: MutableStateFlow<String>
}
class FlutterSceneViewModel(private val database: Database): FlutterSceneViewModelType, ViewModel() {
override var colorFlow = MutableStateFlow<String>("#FFFFFF")
init {
listenToBackgroundColorFlow()
}
private fun listenToBackgroundColorFlow() {
database.backgroundColorFlow.watch {
colorFlow.value = it.hex
}
}
}
the .watch call is a helper I have added so that this can be exposed to iOS using Kotlin multi-platform, it looks as follows but I can use collect instead if necessary:
fun <T> Flow<T>.asCommonFlow(): CommonFlow<T> = CommonFlow(this)
class CommonFlow<T>(private val origin: Flow<T>) : Flow<T> by origin {
fun watch(block: (T) -> Unit): Closeable {
val job = Job()
onEach {
block(it)
}.launchIn(CoroutineScope(Dispatchers.Main + job))
return object : Closeable {
override fun close() {
job.cancel()
}
}
}
}
I resolved this by using viewModel context:
private fun listenToBackgroundColor() {
viewModel.colorFlow.onEach {
val newColor = Color.parseColor(it)
sceneView.setBackgroundColor(newColor)
}.launchIn(viewModel.viewModelScope)
}
I had to import the following into my ViewModel:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
from:
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0")