Kotlin Flow still active in fragment after success - android

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.

Related

Coroutine StateFlow.collect{} not firing

I'm seeing some odd behavior. I have a simple StateFlow<Boolean> in my ViewModel that is not being collected in the fragment. Definition:
private val _primaryButtonClicked = MutableStateFlow(false)
val primaryButtonClicked: StateFlow<Boolean> = _primaryButtonClicked
and here is where I set the value:
fun primaryButtonClick() {
_primaryButtonClicked.value = true
}
Here is where I'm collecting it.
repeatOnOwnerLifecycle {
launch(dispatchProvider.io()) {
freeSimPurchaseFragmentViewModel.primaryButtonClicked.collect {
if (it) {
autoCompletePlacesStateFlowModel.validateErrors()
formValidated = autoCompletePlacesStateFlowModel.validateAddress()
if (formValidated) {
freeSimPurchaseFragmentViewModel
.sumbitForm(autoCompletePlacesStateFlowModel.getStateFlowCopy())
}
}
}
}
}
repeatOnOwnerLifecycle:
inline fun Fragment.repeatOnOwnerLifecycle(
state: Lifecycle.State = Lifecycle.State.RESUMED,
crossinline block: suspend CoroutineScope.() -> Unit
) {
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(state) {
block()
}
}
What am I doing wrong? The collector never fires.
Does this make sense?
val primaryButtonClicked: StateFlow<Boolean> = _primaryButtonClicked.asStateFlow()
Also I couldn't understand the inline function part, because under the hood seems you wrote something like this
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
launch(dispatchProvider.io()) {
freeSimPurchaseFragmentViewModel.primaryButtonClicked.collect {
if (it) {
autoCompletePlacesStateFlowModel.validateErrors()
formValidated = autoCompletePlacesStateFlowModel.validateAddress()
if (formValidated) {
freeSimPurchaseFragmentViewModel
.sumbitForm(autoCompletePlacesStateFlowModel.getStateFlowCopy())
}
}
}
}
}
}
Why are you launching one coroutine in another and collect the flow from IO dispatcher? You need to collect the values from the main dispatcher.

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
}

#Composable invocations can only happen from the context of a #Composable functionn

How can I call a composable function from context of corrutines?
I trying the following code but I getting the error.
#Composable
fun ShowItems(){
var ListArticle = ArrayList<Article>()
lifecycleScope.launchWhenStarted {
// Triggers the flow and starts listening for values
viewModel.uiState.collect { uiState ->
// New value received
when (uiState) {
is MainViewModel.LatestNewsUiState.Success -> {
//Log.e(TAG,"${uiState.news}")
if(uiState.news != null){
for(i in uiState.news){
ListArticle.add(i)
}
context.ItemNews(uiState.news.get(4))
Log.e(TAG,"${uiState.news}")
}
}
is MainViewModel.LatestNewsUiState.Error -> Log.e(TAG,"${uiState.exception}")
}
}
}
}
You should do something like this:
#Composable
fun ShowItems(){
val uiState = viewModel.uiState.collectAsState()
// Mount your UI in according to uiState object
when (uiState.value) {
is MainViewModel.LatestNewsUiState.Success -> { ... }
is MainViewModel.LatestNewsUiState.Error -> { ... }
}
// Launch a coroutine when the component is first launched
LaunchedEffect(viewModel) {
// this call should change uiState internally in your viewModel
viewModel.loadYourData()
}
}

Why Live data not observe some value in race condition?

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

Handling error flow in single activity MVVM architecture: observers isn't triggered

I'm using MVVM in single activity application. As possible solution for error flow I decided to use activity as aggregation point for error representation(I considered that activity in this case is the most suitable place for this because it is basic View for any other Views in single activity scenario). I'm using SingleLiveEvent for passing errors(SingleLiveEvent implementation taken from here).
But in my case I don't receive any error updates from my ViewModels.
As example of my flow:
MessageDialogViewModel.kt
class MessageDialogViewModel(application: Application) : AndroidViewModel(application), MessageApi {
val TAG: String = "DialogViewModel: "
val message: MutableLiveData<String> = MutableLiveData()
val errorLiveEvent: SingleLiveEvent<BaseCommand> = SingleLiveEvent()
override fun sendMessage() {
Log.i(TAG, " sendMessage: start")
if (message.value.isNullOrBlank()) {
Log.i(TAG, " sendMessage: blank")
errorLiveEvent.value = BaseCommand.Error(null, "Message cannot be empty")
} else {
Log.i(TAG, " sendMessage: ${message.value}")
Repository.getInstance(getApplication()).sendMessage(message.value!!)
}
}
}
MainActivity.kt
private val loginViewModel: LoginViewModel by lazy {
ViewModelProviders.of(this).get(LoginViewModel::class.java)
}
private val mainTaskViewModel: MainTaskViewModel by lazy {
ViewModelProviders.of(this).get(MainTaskViewModel::class.java)
}
private val messageDialogViewModel: MessageDialogViewModel by lazy {
ViewModelProviders.of(this).get(MessageDialogViewModel::class.java)
}
//////
override fun onResume() {
super.onResume()
//fixme merge observers
loginViewModel.singleLiveEvent.observe(this,
Observer {
when (it) {
is BaseCommand.Success -> {
navigate()
}
is BaseCommand.Error -> showDialog(
errorTitle = it.error?.title,
errorMessage = it.error?.desc
)
}
})
mainTaskViewModel.errorLiveEvent.observe(this,
Observer {
when (it) {
is BaseCommand.Success -> {
}
is BaseCommand.Error -> {
showToast(it.error)
if (it.error?.code == 500) {
navController.navigate(R.id.action_mainTaskFragment_to_loginFragment)
}
}
}
}
)
messageDialogViewModel.errorLiveEvent.observe(this,
Observer {
Log.i(TAG,"messageDialogViewModel.errorLiveEvent")
when (it) {
is BaseCommand.Success -> {
}
is BaseCommand.Error -> {
showToast(null, it.errorMessage)
}
}
})
}
And in I don't receive anything in my MainActivity.
Any help or suggestion about better architecture solution appreciated.
Use errorLiveEvent.setValue(BaseCommand.Error(null, "Message cannot be empty")) instead of errorLiveEvent.value = BaseCommand.Error(null, "Message cannot be empty")

Categories

Resources