This question already has answers here:
Collect from several stateflows
(4 answers)
Closed 1 year ago.
This code invokes two methods on the same viewmodel and listens for updates. But only the first method completes, the second does not event trigger.
private fun initData() {
lifecycleScope.launchWhenStarted {
viewModel.incrementCount().collect {
info { "Count: $it" }
}
viewModel.getAllTeams().collect {
when (it) {
is State.Success -> {
info { "Got teams with size: ${it.result}.size" }
}
is State.Error -> {
info { "Error getting teams: ${it.message}" }
}
State.Loading -> {
info { "Loading all teams" }
}
}
}
}
}
ViewModel
class DashboardViewModel : ViewModel(), com.droid.common.Logger {
fun incrementCount(): MutableStateFlow<Int> {
val countState = MutableStateFlow(0)
viewModelScope.launch {
repeat(5) {
countState.value = (it)
delay(1000)
}
}
return countState
}
fun getAllTeams(): MutableStateFlow<State> {
val state = MutableStateFlow<State>(State.None)
state.value = State.Loading
viewModelScope.launch {
try {
val allTeams = FootballAPIClient.apiService.getAllTeams()
state.value = State.Success(allTeams)
} catch (exception: Exception) {
error { "Error getting all teams: ${exception.message}" }
state.value = State.Error(exception.message.toString())
}
}
return state
}
However, calling them with separate lifecycleScope works
private fun initData() {
lifecycleScope.launchWhenStarted {
viewModel.incrementCount().collect {
info { "Count: $it" }
}
}
lifecycleScope.launchWhenStarted {
viewModel.getAllTeams().collect {
when (it) {
is State.Success -> {
info { "Got teams with size: ${it.result}.size" }
}
is State.Error -> {
info { "Error getting teams: ${it.message}" }
}
State.Loading -> {
info { "Loading all teams" }
}
}
}
}
}
I can't seem to understand this behavior, anybody knows why?
You will need different coroutines, since collect() is a suspending function that suspends until your Flow terminates.
The problem with launchWhenStarted is that while your newly emitted items will not be processed your producer will still run in the background.
For collecting multiple flows the currently recommended way is:
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.incrementCount().collect { ... }
}
launch {
viewModel.getAllTeams().collect { ... }
}
}
}
Related
I have a base Remote Config initiallizer class
abstract class RemoteConfig: KoinComponent {
protected val config = get<FirebaseRemoteConfig>()
protected val dispatchers = get<AppDispatchers>()
protected abstract fun initValues()
init {
CoroutineScope(dispatchers.io).launch {
initConfig()
}
}
private suspend fun initConfig() {
config.fetchAndActivate().await().let { activated ->
if (activated) {
initValues()
}
}
}
}
where FirebaseRemoteConfig set as single
single {
Firebase.remoteConfig.apply {
val interval = if (BuildConfig.DEBUG) 0 else 900
setConfigSettingsAsync(
remoteConfigSettings { minimumFetchIntervalInSeconds = interval.toLong() }
)
}
}
and this part of code
config.fetchAndActivate().await().let { activated ->
if (activated) {
initValues()
}
}
doesn't work - activated variable never equals true
but if I change this part of code to
config.fetchAndActivate().addOnCompleteListener { task ->
if (task.isSuccessful) {
initValues()
}
}
it works fine
Can you help me to understand, what's wrong with my initial way?
I have implemented MVVM architecture in one of my projects. In which I have created a ViewModel, Repository, and ViewModelFactory to make API call. I'm showing a list of products on the wish list. There is a facility where you can remove a particular product from the wish list. When my activity opens its API calls and renders the data but when I remove the product from the recycler view list need to make an API call which gets the remaining list of products available in the wishlist. When I change the configuration of the device it also makes API calls. How can I survive the configuration changes?
Below is the code from Activity
val repository = WishlistRepository(activity)
wishlistViewModel = ViewModelProvider(
this,
WishlistViewModelFactory(repository)
)[WishlistViewModel::class.java]
wishlistObservables()
if (preferenceUtils.getPrefBoolean(Constants.IS_LOGGED_IN)) {
if (wishlistViewModel.wishListLiveData.value == null) {
prepareParams()
}
} else {
val intent = Intent(activity, LoginActivity::class.java)
intent.putExtra("fromScreen", 2)
startActivity(intent)
}
prepareParams method
private fun prepareParams() {
if (Utils.isNetworkAvailable(activity)) {
val params = mutableMapOf<String, String>()
params["customer_id"] = preferenceUtils.getPreString(Constants.USER_ID)
params["store"] = preferenceUtils.getPreInt(Constants.STORE).toString()
wishlistViewModel.getWishList(params)
} else {
Utils.snackBar(binding.coordinatorWishlist, getString(R.string.network))
}
}
ViewModel Code
val wishListLiveData: LiveData<Resource<WishlistModel>> get() = repository.wishLiveData
fun getWishList(params: Map<String, String>) {
repository.getWishlistApiCall(params)
}
Repository Code
private val wishListLiveData = MutableLiveData<Resource<WishlistModel>>()
val wishLiveData:LiveData<Resource<WishlistModel>> get() = wishListLiveData
fun getWishlistApiCall(params:Map<String,String>){
if (Utils.isNetworkAvailable(activity)) {
wishListLiveData.postValue(Resource.Loading())
retroService.getWishListApiCall(params).enqueue(object : Callback<WishlistModel> {
override fun onResponse(call: Call<WishlistModel>, response: Response<WishlistModel>) {
try {
if (response.body() != null) {
if (response.isSuccessful) {
response.body()?.let {
if (it.code == 200) {
if (it.status == "1") {
wishListLiveData.postValue(Resource.Success(it))
} else {
wishListLiveData.postValue(Resource.Error(it.message))
}
} else if (it.code == 404) {
wishListLiveData.postValue(
Resource.Error(
activity.getString(
R.string.four_hundred_four_not_found
)
)
)
} else if (it.code == 500) {
wishListLiveData.postValue(
Resource.Error(
activity.getString(
R.string.five_hundred_server_error
)
)
)
} else {
wishListLiveData.postValue(Resource.Error(it.message))
}
}
} else {
wishListLiveData.postValue(Resource.Error(response.message()))
}
} else {
wishListLiveData.postValue(Resource.Error(response.message()))
}
} catch (e: Exception) {
wishListLiveData.postValue(Resource.Error(e.message.toString()))
}
}
override fun onFailure(call: Call<WishlistModel>, t: Throwable) {
try {
wishListLiveData.postValue(Resource.Error(t.message.toString()))
} catch (e: Exception) {
wishListLiveData.postValue(Resource.Error(e.message.toString()))
}
}
})
} else {
wishListLiveData.postValue(Resource.Error(activity.getString(R.string.network)))
}
}
ViewModelFactory Code
class WishlistViewModelFactory(private val repository: WishlistRepository) :
ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return WishlistViewModel(repository) as T
}
}
Remove wishlist observables
private fun removeObservables() {
removeWishlistViewModel.removeWishListLiveData.observe(this) {
when (it.status) {
Status.LOADING -> {
Utils.showDialog(activity)
}
Status.SUCCESS -> {
Utils.hideDialog()
if (it.data != null) {
it.data.let { removeWishModel ->
Utils.snackBar(
binding.coordinatorWishlist,
removeWishModel.message, Constants.SNACK_SUCCESS_COLOR
)
val activityFragmentMessageEvent =
Events.ActivityFragmentMessage("Wishlist")
EventBus.getDefault().post(activityFragmentMessageEvent)
prepareParams()
}
}
}
Status.ERROR -> {
Utils.hideDialog()
Utils.snackBar(binding.coordinatorWishlist, it.errorMessage.toString())
}
}
}
}
When I call prepareParams method again it's will make an API call of getting the wish-listed products. After that when I change the configuration from portrait to landscape it's starting again to make API calls again and again.
How can I resolve this issue? Please help me to resolve this issue with MVVM. I'm begginner in MVVM architecture.
Thank you
Hello I tried to make Coroutines Flow (using callbackFlow) and tried to convert it to live data, but it seems it's not updating.
You can see my code below:
#ExperimentalCoroutinesApi
suspend fun checkInDanger(): Flow<NetworkStatus<List<UserFire>>> = callbackFlow {
val check = fs.collection(UserFire.COLLECTION).whereEqualTo(UserFire.DANGER, true)
.addSnapshotListener { value, e ->
if (e != null) {
trySend(NetworkStatus.Failed("Error occurred\n${e.code}"))
return#addSnapshotListener
}
if (value == null || value.isEmpty) trySend(NetworkStatus.Empty)
else {
val users = value.map { it.toObject(UserFire::class.java) }
trySend(NetworkStatus.Success(users))
}
}
awaitClose { }
}.flowOn(Dispatchers.IO)
On my repositories:
#ExperimentalCoroutinesApi
override suspend fun checkInDanger(): Flow<Status<List<User>>> = flow {
when (val result = network.checkInDanger().first()) {
is NetworkStatus.Success -> emit(Status.Success(result.data.map {
MapVal.userFireToDom(it)
}))
is NetworkStatus.Empty -> emit(Status.Success(listOf<User>()))
is NetworkStatus.Failed -> emit(Status.Error(null, result.error))
}
}
In my ViewModel:
val checkInDanger = liveData(Dispatchers.IO) {
try {
useCase.checkInDanger().collectLatest {
emit(it)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
But when I changed the value in my Firebase, it's not fetching new data...
Anyone know why it's not fixed
I kind of find the way, but it's with callback, if we used callback, it can fetch data update even without live-data, but for my case I tried to push the callback to live data again,
so the code will be like this:
fun checkInDanger(networkStatus: (NetworkStatus<List<UserFire>>) -> Unit) {
fs.collection(UserFire.COLLECTION).whereEqualTo(UserFire.DANGER, true)
.addSnapshotListener { value, e ->
if (e != null) {
networkStatus(NetworkStatus.Failed("Error occurred\n${e.code}"))
return#addSnapshotListener
}
if (value == null || value.isEmpty) networkStatus(NetworkStatus.Empty)
else {
val users = value.map { it.toObject(UserFire::class.java) }
networkStatus(NetworkStatus.Success(users))
}
}
}
In my repositories:
override fun checkInDanger(callback: (Status<List<User>>) -> Unit) {
network.checkInDanger { result ->
when (result) {
is NetworkStatus.Success -> callback(Status.Success(result.data.map {
MapVal.userFireToDom(it)
}))
is NetworkStatus.Empty -> callback(Status.Success(listOf<User>()))
is NetworkStatus.Failed -> callback(Status.Error(null, result.error))
}
}
}
In my ViewModel:
fun checkInDanger(callback: (Status<List<User>>) -> Unit) = useCase.checkInDanger { callback(it) }
val setUsers = MutableLiveData<List<User>>().apply { this.value = listOf() }
val users: LiveData<List<User>> = setUsers
In my UI Class (Fragment Main):
val inDangerCallback: (Status<List<User>>) -> Unit = {
if (relative != null) {
when (it) {
is Status.Success ->
viewModel.setUsers.value =
it.data?.filter { user -> user.username in relative!!.pure }
else -> requireView().createSnackBar(it.error!!, 1000)
}
}
}
viewModel.checkInDanger(inDangerCallback)
viewModel.users.observe(viewLifecycleOwner) { users ->
println(users.size})
users?.forEach { user -> println(user.username) }
}
And the code can run perfectly and update automatically...
I try to handle clicks on my buttons and send action to viewModel
private fun subscribeUI() {
lifecycleScope.launch {
binding.loginButton
.clicks()
.onEach { }
.map { Action.WelcomeAction.SelectLogin }
.collect { viewModel.actions.offer(it) }
binding.homeButton
.clicks()
.onEach { }
.map { Action.WelcomeAction.SelectHome }
.collect { viewModel.actions.offer(it) }
binding.registerButton
.clicks()
.onEach {}
.map { Action.WelcomeAction.SelectRegister }
.collect { viewModel.actions.offer(it) }
}
}
Only action from login button comes to my view model. How can I merge these three flows into one? Probably that's the problem there are 3 action streams to view model
private fun subscribeUI() {
merge(
binding.loginButton.clicks().map { Action.WelcomeAction.SelectLogin },
binding.homeButton.clicks().map { Action.WelcomeAction.SelectHome },
binding.registerButton.clicks().map { Action.WelcomeAction.SelectRegister }
)
.onEach { viewModel.actions.offer(it) }
.launchIn(lifecycleScope)
}
Trying to stick to the MVVM pattern rules. The final data should come to the ViewModel (I'm making an exception for Glide, but that's not the point).
Created several subcollections in the document (likes, comments, images, and so on).
After creating them, I ran into the problem of requesting the Firestore inside the main request. The data just doesn't have time to arrive.
How do I wait for data before "data.add" in such a situation?
ViewModel
init {
data = loadData()
}
fun getData(loadNextData: Boolean): LiveData<Response<List<Model>>> {
if (loadNextData)
data = loadData()
return data
}
private fun loadData(): LiveData<Response<List<Model>>> {
return liveData(viewModelScope.coroutineContext + Dispatchers.IO) {
emit(Response.Loading)
val result = repository.loadData()
if (result is Response.Success || result is Response.Error)
emit(result)
}
}
Repository
suspend fun loadData(): Response<List<Model>> {
return suspendCoroutine { continuation ->
if (LAST_DOCUMENT == null) {
firebaseRepository.getReference(DEFAULT_COLLECTION_NAME, DEFAULT_ORDER_FIELD, DEFAULT_ORDER_DIRECTION)
} else {
firebaseRepository.getReference(DEFAULT_COLLECTION_NAME, DEFAULT_ORDER_FIELD, DEFAULT_ORDER_DIRECTION, LAST_DOCUMENT!!)
}
.get()
.addOnSuccessListener { query ->
try {
LAST_DOCUMENT = query.documents.lastOrNull()
query.documents.forEach { document ->
document.toObject(ModelDTO::class.java)?.let {
it.id = document.id
it.likes_count = // TODO
it.comments_count = // TODO
it.images_uri.add(//TODO)
data.add(it.mapToEntity())
}
}
continuation.resume(Response.Success(data))
} catch (exception: Exception) {
continuation.resume(Response.Error(exception))
}
}
.addOnFailureListener { exception ->
continuation.resume(Response.Error(exception))
}
}
}