Why Live data not observe some value in race condition? - android

I have some problem with my live data. when I want to set value in my live data in race condition some value not observe in my fragement. here is my detail code:
Here Is My View Model
class AuthNewViewModel(
private val coroutineDispatcher: CoroutineDispatcher,
private val authUseCase: AuthUseCase
): ViewModel() {
private val _sendCode = MutableLiveData<DataResult<SendCode?>>()
val sendCode: LiveData<DataResult<SendCode?>> = _sendCode
fun sendCode(req:CodeReq) {
viewModelScope.launch(coroutineDispatcher) {
_sendCode.postValue(DataResult.Loading(true))
authUseCase.sendCode(req)
.flowOn(Dispatchers.IO)
.catch { error ->
_sendCode.postValue(error.isError())
}
.onCompletion {
_sendCode.postValue(DataResult.Loading(false))
}
.collect { data ->
when (data.meta?.code ?: 0) {
in 200..299 -> {
_sendCode.postValue(DataResult.Success(data.data))
}
else -> {
_sendCode.postValue(DataResult.ErrorMeta(data.data, data.errorData))
}
}
}
}
}
}
Here is when I want to triggred my view mode
authNewVM.sendCode(
CodeReq(
phone = phoneOrEmail,
channel = channel,
type = Constants.AuthConstants.TYPE_SIGNUP
)
)
Here is when the data is observe
authNewVM.sendCode.observe(viewLifecycleOwner, Observer {
when(it){
is DataResult.Success -> {
Logs.d("result success")
}
is DataResult.Error -> {
Logs.d("result error")
}
is DataResult.Loading -> {
Logs.d("result loading ${it.isLoading}")
}
is DataResult.ErrorMeta -> {
Logs.d("result error meta")
}
}
})
And the result
result loading true
result loading false
View some time not observe state success, whereas my view model is set value on succes to..

Related

Stop collecting Flow in ViewModel when app in background

Need to collect flow in ViewModel and after some data modification, the UI is updated using _batteryProfileState.
Inside compose I'm collecting states like this
val batteryProfile by viewModel.batteryProfileState.collectAsStateWithLifecycle()
batteryProfile.voltage
In ViewModel:
private val _batteryProfileState = MutableStateFlow(BatteryProfileState())
val batteryProfileState = _batteryProfileState.asStateFlow()
private fun getBatteryProfileData() {
viewModelScope.launch {
// FIXME In viewModel we should not collect it like this
_batteryProfile(Unit).collect { result ->
_batteryProfileState.update { state ->
when(result) {
is Result.Success -> {
state.copy(
voltage = result.data.voltage?.toString()
?.plus(result.data.voltageUnit
)
}
is Result.Error -> {
state.copy(
errorMessage = _context.getString(R.string.something_went_wrong)
)
}
}
}
}
}
}
The problem is when I put my app in the background the _batteryProfile(Unit).collect does not stop collecting while in UI batteryProfile.voltage stop updating UI which is correct behavior as I have used collectAsStateWithLifecycle() for UI.
But I have no idea how to achieve the same behavior for ViewModel.
In ViewModel I have used stateIn operator and access data like below everything working fine now:
val batteryProfileState = _batteryProfile(Unit).map { result ->
when(result) {
is Result.Success -> {
BatteryProfileState(
voltage = result.data.voltage?.toString()
?.plus(result.data.voltageUnit.unit)
?: _context.getString(R.string.msg_unknown),
)
}
is Result.Error -> {
BatteryProfileState(
errorMessage = _context.getString(R.string.something_went_wrong)
)
}
}
}.stateIn(viewModelScope, WhileViewSubscribed, BatteryProfileState())
collecting data in composing will be the same
Explanation: WhileViewSubscribed Stops updating data while the app is in the background for more than 5 seconds.
val WhileViewSubscribed = SharingStarted.WhileSubscribed(5000)
You can try to define getBatteryProfileData() as suspend fun:
suspend fun getBatteryProfileData() {
// FIXME In viewModel we should not collect it like this
_batteryProfile(Unit).collect { result ->
_batteryProfileState.update { state ->
when(result) {
is Result.Success -> {
state.copy(
voltage = result.data.voltage?.toString()
?.plus(result.data.voltageUnit
)
}
is Result.Error -> {
state.copy(
errorMessage = _context.getString(R.string.something_went_wrong)
)
}
}
}
}
}
And than in your composable define scope:
scope = rememberCoroutineScope()
scope.launch {
yourviewmodel.getBatteryProfileData()
}
And I think you can move suspend fun getBatteryProfileData() out of ViewModel class...

How to chain flows with different return types based on emitted values and collect their results?

I have a situation where I have to execute 3 network requests one after the other collect their results (which are of different types).
Following is the relevant part of the code :
Resource.kt
sealed class Resource<T>(val data: T? = null, val message: String? = null) {
class Loading<T>(data: T? = null): Resource<T>(data)
class Success<T>(data: T?): Resource<T>(data)
class Error<T>(message: String, data: T? = null): Resource<T>(data, message)
}
Repository.kt
override fun getReportData(profileId: Int): Flow<Resource<ProfileReport>> =
flow {
emit(Resource.Loading<ProfileReport>())
var report: ProfileReport? = null
try {
// Api is available as a retrofit implementation
report = api.getReport(profileId).toProfileReport()
} catch (e: HttpException) {
emit(
Resource.Error<ProfileReport>(
message = "An unknown http exception occured"
)
)
}
if (report!= null) {
emit(Resource.Success<ProfileReport>(data = report))
}
}
Say I have 3 such flows to fetch data in my repository and they have different return types (ex: ProfileReport, ProfileInfo, ProfileStatus).
Now in my viewmodel I have a function to execute these flows and perform actions on the values emitted such as :
ViewModel.kt
fun getProfileData(profileId: Int) {
getReportData(profileId)
.onEach { result ->
when (result) {
is Resource.Loading -> {
_loading.value = true
}
is Resource.Error -> {
_loading.value = false
// UI event to display error snackbar
}
is Resource.Success -> {
_loading.value = false
if (result.data != null) {
_report.value = _report.value.copy(
// Use result here
)
}
}
}
}.launchIn(viewModelScope)
}
This works ok for one flow but how can I execute 3 flows one after the other.
That is, execute first one and if its successful, execute second one and so on, and if all of them are successful use the results.
I did it like this :
fun getProfileData(profileId: Int) {
getReportData(profileId)
.onEach { result1 ->
when (result1) {
is Resource.Loading -> {/*do stuff*/}
is Resource.Error -> {/*do stuff*/}
is Resource.Success -> {
getProfileStatus(profileId)
.onEach { result2 ->
is Resource.Loading -> {/*do stuff*/}
is Resource.Error -> {/*do stuff*/}
is Resource.Success -> {
getProfileInfo(profileId)
.onEach { result3 ->
is Resource.Loading -> {/*do stuff*/}
is Resource.Error -> {/*do stuff*/}
is Resource.Success -> {
/*
Finally update viewmodel state
using result1, result2 and result3
*/
}
}.launchIn(viewModelScope)
}
}.launchIn(viewModelScope)
}
}
}.launchIn(viewModelScope)
}
But, this feels too cumbersome and probably there is a better way to chain flows based on success condition and collect results at the end. I checked some ways that use combine() or flatMapMerge() but was unable to use them in this situation.
Is there a way to achieve this? Or is this approach itself wrong from a design perspective maybe?
I think this could be modeled much more cleanly using imperative coroutines than with flows. Since you're overriding functions, this depends on you being able to modify the supertype abstract function signatures.
This solution doesn't use Resource.Loading, so you should remove that to make smart casting easier.
suspend fun getReportData(profileId: Int): Resource<ProfileReport> =
try {
val report = api.getReport(profileId).toProfileReport()
Resource.Success<ProfileReport>(data = report)
} catch (e: HttpException) {
Resource.Error<ProfileReport>(
message = "An unknown http exception occured"
)
}
//.. similar for the other two functions that used to return flows.
fun getProfileData(profileId: Int) {
viewModelScope.launch {
// do stuff to indicate 1st loading state
when(val result = getReportData(profileId)) {
Resource.Error<ProfileReport> -> {
// do stuff for error state
return#launch
}
Resource.Success<ProfileReport> -> {
// do stuff with result
}
}
// Since we returned when there was error, we know first
// result was successful.
// do stuff to indicate 2nd loading state
when(val result = getProfileStatus(profileId)) {
Resource.Error<ProfileStatus> -> {
// do stuff for error state
return#launch
}
Resource.Success<ProfileStatus> -> {
// do stuff with result
}
}
// do stuff to indicate 3rd loading state
when(val result = getProfileInfo(profileId)) {
Resource.Error<ProfileInfo> -> {
// do stuff for error state
return#launch
}
Resource.Success<ProfileInfo> -> {
// do stuff with result
}
}
}
}
If you want to keep your current Flows, you could collect your flows this way to avoid the deep nesting. This works because your source flows are designed to be finite (they aren't repeatedly emitting new values indefinitely, but have only one final result).
fun getProfileData(profileId: Int) = viewModelScope.launch {
var shouldBreak = false
getReportData(profileId).collect { result ->
when (result) {
is Resource.Loading -> { /*do stuff*/ }
is Resource.Error -> {
/*do stuff*/
shouldBreak = true
}
is Resource.Success -> { /*do stuff*/ }
}
}
if (shouldBreak) return#launch
getProfileStatus(profileId).collect { result ->
when (result) {
is Resource.Loading -> { /*do stuff*/ }
is Resource.Error -> {
/*do stuff*/
shouldBreak = true
}
is Resource.Success -> { /*do stuff*/ }
}
}
if (shouldBreak) return#launch
getProfileInfo(profileId).collect { result ->
when (result) {
is Resource.Loading -> { /*do stuff*/ }
is Resource.Error -> { /*do stuff*/ }
is Resource.Success -> { /*do stuff*/ }
}
}
}

How to omit the last value of a liveData when the fragment / activity is recreated?

I have a problem with liveData in a particular case. When the response from a http service is Code = 2, it means that the session token has expired. In that case I navigate to the LoginFragment to login the user again. If the user logs in, then I return to the fragment which was previously and when I start to observe the liveData in onViewCreated function, it gives me its last value which is: Code = 2, so the Application navigates back to the login, which is wrong.
I have a Sealed Class:
sealed class Resource<T>(
var data: T? = null,
val message: String? = null,
val Code: Int? = null
) {
class Success<T>(data: T?) : Resource<T>(data)
class Error<T>(message: String, code: Int? = null) : Resource<T>(message = message, Code = code)
class Loading<T> : Resource<T>()
}
This is the code on the ViewModel:
val mLiveData: MutableLiveData<Resource<Data>> = MutableLiveData()
fun getData() {
viewModelScope.launch {
mLiveData.postValue(Resource.Loading())
try {
if (app.hasInternetConnection()) {
// get Data From API
val response = repository.getData()
if(response.isSuccessful){
mLiveData.postValue(Resource.Success(parseSuccessResponse(response.body())))
} else {
mLiveData.postValue(Resource.Error(parseErrorResponse(response.body())))
}
} else {
mLiveData.postValue(Resource.Error("Connection Fail"))
}
} catch (t: Throwable) {
when (t) {
is IOException -> mLiveData.postValue(Resource.Error("Connection Fail"))
else -> mLiveData.postValue(Resource.Error("Convertion Fail"))
}
}
}
}
This is the code on the fragment, observeForData() is called in onViewCreated function:
private fun observeForData() {
mLiveData.getData.observe(viewLifecycleOwner, Observer { response ->
when (response) {
is Resource.Success -> {
isLoading = false
updateUI(response.data)
}
is Resource.Error -> {
isLoading = false
if (response.Code == 2) {
// Token Expired
navigateToLogin()
} else {
showErrorMessage(response.message)
}
}
is Resource.Loading -> {
isLoading = true
}
}
})
}
How can i solve this?
Is there a way to remove the last value or state from a liveData when the fragment is destroyed when navigating to the LoginFragment?
Thank you.
One often-suggested solution is SingleLiveEvent, which is a simple class you can copy-paste into your project.
For a framework solution, I suggest SharedFlow. Some Android developers recommend switching from LiveData to Flows anyway to better decouple data from views. If you give SharedFlow a replay value of 0, new Activities and Fragments that observe it will not get the previous value, only newly posted values.
Sample untested ViewModel code:
val dataFlow: Flow<Resource<Data>> = MutableSharedFlow(replay = 0)
init {
viewModelScope.launch {
// Same as your code, but replace mLiveData.postValue with dataFlow.emit
}
}
And in the Fragment:
private fun observeForData() {
isLoading = true
lifecycleScope.launch {
mLiveData.dataFlow
.flowWithLifecycle(this, Lifecycle.State.STARTED)
.collect { onDataResourceUpdate(it) }
}
}
// (Broken out into function to reduce nesting)
private fun onDataResourceUpdate(resource: Resource): Unit = when(resource) {
is Resource.Success -> {
isLoading = false
updateUI(response.data)
}
is Resource.Error -> {
isLoading = false
if (response.Code == 2) {
// Token Expired
navigateToLogin()
} else {
showErrorMessage(response.message)
}
}
is Resource.Loading -> isLoading = true
}
To change last updated value for live data,You can set "Resource" class with default null values when onDestroy().
onDestroy(){
//in java ,it will be new Resource instance
Resource resourceWithNull=new Resource();
mLiveData.setValue(resourceWithNull);
}
when you relaunch the fragment live data will emit Resource with null value as response.
Then You can write your code with in observer
if(response.data!=null)
{
//your code
}

Kotlin Flow still active in fragment after success

I've a fragment making a network request based on the result, I'm navigating to the next fragment.
I am not able to go back to the previous fragment, this is the issue: https://streamable.com/4m2vzg
This is the code in the previous fragment
class EmailInputFragment :
BaseFragment<FragmentEmailInputBinding>(FragmentEmailInputBinding::inflate) {
private val viewModel by viewModels<EmailInputViewModel>()
private lateinit var progressButton: ProgressButton
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.emailToolbar.setNavigationOnClickListener {
val activity = activity as AuthActivity
activity.onSupportNavigateUp()
}
binding.emailNextButton.pbTextview.text = getString(R.string.next)
binding.emailNextButton.root.setOnClickListener {
checkValidEmail()
}
binding.enterEmail.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
checkValidEmail()
}
false
}
binding.enterEmail.doAfterTextChanged {
binding.enterEmailLayout.isErrorEnabled = false
}
viewLifecycleOwner.lifecycleScope.launch {
viewModel.emailCheck.collect {
when (it) {
State.Empty -> {
}
is State.Failed -> {
Timber.e(it.message)
progressButton.buttonFinished("Next")
}
State.Loading -> {
progressButton.buttonActivate("Loading")
}
is State.Success<*> -> {
it.data as EmailCheckModel
when (it.data.registered) {
// New User
0 -> {
findNavController().navigate(
EmailInputFragmentDirections.actionEmailFragmentToSignupFragment(
binding.enterEmail.text.toString().trim()
)
)
}
// Existing User
1 -> {
findNavController().navigate(
EmailInputFragmentDirections.actionEmailFragmentToPasswordInputFragment(
binding.enterEmail.text.toString().trim()
)
)
}
// Unverified user
2 -> {
findNavController().navigate(
EmailInputFragmentDirections.actionEmailFragmentToVerifyUserFragment(
"OK"
)
)
}
}
}
}
}
}
}
private fun checkValidEmail() {
if (!binding.enterEmail.text.toString().trim().isValidEmail()) {
binding.enterEmailLayout.error = "Please enter valid Email ID"
return
}
progressButton = ProgressButton(requireContext(), binding.emailNextButton.root)
viewModel.checkUser(binding.enterEmail.text.toString().trim())
}
}
When I press back from the next fragment, as the state is still Success the flow is being collected and goes to next fragment, I've tried this.cancel to cancel the coroutine on create and still doesn't work.
How do I go about this?
Moving the flow collect to the onClick of the button throws a error that navigation action cannot be found for the destination
I put a workaround of resetting the state of the flow back to State.EMPTY on success using
viewModel.resetState()
in onSuccess, I don't think this is the best way, any suggestions?
ViewModel code:
private val _emailCheckResponse = MutableStateFlow<State>(State.Empty)
val emailCheck: StateFlow<State> get() = _emailCheckResponse
If your viewModel.emailCheck flow is a hot flow, then you need to manage its life cycle by yourself. If it is not a hot Flow, then you need to use LiveData to control the interface instead of simply collecting Flow. You should convert the flow to LiveData, and add the Observer to LiveData at the corresponding location.
There is no API related to the interface life cycle in Cold Flow, but the life cycle is already managed in LiveData.
viewModel.emailCheckLiveData.observe(viewLifecycleOwner, {
when (it) {
State.Empty -> {
}
is State.Failed -> {
Timber.e(it.message)
progressButton.buttonFinished("Next")
}
State.Loading -> {
progressButton.buttonActivate("Loading")
}
is State.Success<*> -> {
it.data as EmailCheckModel
if (it.data.registered) {
val action =
EmailInputFragmentDirections.actionEmailFragmentToPasswordInputFragment(
binding.enterEmail.text.toString().trim()
)
findNavController().navigate(action)
} else {
val action =
EmailInputFragmentDirections.actionEmailFragmentToSignupFragment(
binding.enterEmail.text.toString().trim()
)
findNavController().navigate(action)
}
}
})
You need to define emailCheckLiveData. In Flow.asLiveData()
private val _emailCheckResponse = MutableStateFlow<State>(State.Empty)
val emailCheck: StateFlow<State> get() = _emailCheckResponse
private var mJob: Job? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
lifecycleScope.launchWhenResumed {
if (mJob?.isActive == true) return
mJob = _emailCheckResponse.collectLatest {
when (it) {
State.Empty -> {
}
is State.Failed -> {
Timber.e(it.message)
progressButton.buttonFinished("Next")
}
State.Loading -> {
progressButton.buttonActivate("Loading")
}
is State.Success<*> -> {
it.data as EmailCheckModel
if (it.data.registered) {
val action =
EmailInputFragmentDirections.actionEmailFragmentToPasswordInputFragment(
binding.enterEmail.text.toString().trim()
)
findNavController().navigate(action)
} else {
val action =
EmailInputFragmentDirections.actionEmailFragmentToSignupFragment(
binding.enterEmail.text.toString().trim()
)
findNavController().navigate(action)
}
}
}
}
}
}
override fun onDestroy() {
mJob?.apply {
if (isActive) cancel()
}
super.onDestroy()
}
After some time, stumbled on this article.
https://proandroiddev.com/flow-livedata-what-are-they-best-use-case-lets-build-a-login-system-39315510666d
Scrolling down to the bottom gave the solution to my problem.

Callback to Observable

I've got this function getting documents from Cloud Firestore:
fun getBasicItems(callback: (MutableList<FireStoreBasicItem>) -> Unit) {
fireStore.collection("BasicItems")
.get()
.addOnCompleteListener { task ->
if (task.isSuccessful) {
val basicItems = mutableListOf<FireStoreBasicItem>()
for (document in task.result!!) {
val fireStoreBasicItem = document.toObject(FireStoreBasicItem::class.java)
basicItems.add(fireStoreBasicItem)
callback(basicItems)
}
}
}
}
In my ViewModel I want to transform this to an Observable an then to a ViewState:
private fun loadDataTransformer(): ObservableTransformer<ItemEvent.LoadDataEvent, ItemsViewState> {
return ObservableTransformer { event ->
event.map {
itemRepository.getBasicItems(){myBasicItemList -> Observable.just(myBasicItemList)}
}
}
I tried it also with Observable.fromCallable. What am I doing wrong?
EDIT: My Solution
private fun loadDataTransformer(): ObservableTransformer<ItemEvent.LoadDataEvent, ItemsViewState> {
return ObservableTransformer { event ->
event.flatMap {
Single.create<MutableList<FireStoreBasicItem>> {
itemRepository.getBasicItems { myBasicItemList ->
it.onSuccess(myBasicItemList)
}
}.toObservable()
.map {
ItemsViewState.ItemDataState(it)
}
}
}
}
I would like to suggest you to use Single instead of Observable if you are expecting only one list of items. Then you can use Single.create:
private fun loadDataTransformer(): Single<ItemsViewState> =
Single.create { emitter ->
itemRepository.getBasicItems() { myBasicItemList ->
val viewState = // do some transformations
emitter.onSuccess(viewState)
}
}

Categories

Resources