What I'm trying to do is to use the Navigation controller inside a LiveData observer, so when the user clicks an item from the list it notifies the ViewModel, then the ViewModel updates the data and when this happens the fragment observes this and navigates to the next.
My problem is that for some reason the observer gets called twice and the second time I get an exception saying that the destination is unknown to this NavController.
My Fragment onCLick:
override fun onClick(view: View?) {
viewModel.productSelected.observe(viewLifecycleOwner, Observer<ProductModel> {
try {
this.navigationController.navigate(R.id.action_product_list_to_product_detail)
} catch (e: IllegalArgumentException) { }
})
val itemPosition = view?.let { recyclerView.getChildLayoutPosition(it) }
viewModel.onProductSelected(listWithHeaders[itemPosition!!].id)
}
And in my ViewModel:
fun onProductSelected(productId: String) {
productSelected.value = getProductById(productId)
}
It's called twice because first you subscribe and so you get a default value back, then you change a value in your productSelected LiveData and so your observer gets notified again.
Thereof, start observing after onProductSelected is called as below:
override fun onClick(view: View?) {
val itemPosition = view?.let { recyclerView.getChildLayoutPosition(it) }
viewModel.onProductSelected(listWithHeaders[itemPosition!!].id)
viewModel.productSelected.observe(viewLifecycleOwner, Observer<ProductModel> {
try {
this.navigationController.navigate(R.id.action_product_list_to_product_detail)
} catch (e: IllegalArgumentException) { }
})
}
Once again, beware that once you start observing your LiveData it will get notified each time productSelected is changed. Is it what you want? If not, then you should remove the observer once it's used once.
Catching the exception may work, but it can also make you miss several other issues. It might be better to check the current layout with the destination to validate if the user is already there. Another alternative that I prefer is to check with the previous destination, something like:
fun Fragment.currentDestination() = findNavController().currentDestination
fun Fragment.previousDestination() = findNavController().previousBackStackEntry?.destination
fun NavDestination.getDestinationIdFromAction(#IdRes actionId: Int) = getAction(actionId)?.destinationId
private fun Fragment.isAlreadyAtDestination(#IdRes actionId: Int): Boolean {
val previousDestinationId = previousDestination()?.getDestinationIdFromAction(actionId)
val currentDestinationId = currentDestination()?.id
return previousDestinationId == currentDestinationId
}
fun Fragment.navigate(directions: NavDirections) {
if (!isAlreadyAtDestination(directions.actionId)) {
findNavController().navigate(directions)
}
}
Basically, here we validate that we are not already at the destination. This can be done by comparing the previous action destination with the current destination. Let me know if the code helps!
Related
So basically, on the snackbar action button, I want to Retry API call if user click on Retry.
I have used core MVVM architecture with Flow. I even used Flow between Viewmodel and view as well. Please note that I was already using livedata between view and ViewModel, but now the requirement has been changed and I have to use Flow only. Also I'm not using and shared or state flow, that is not required.
Code:
Fragment:
private fun apiCall() {
viewModel.fetchUserReviewData()
}
private fun setObservers() {
lifecycleScope.launch {
viewModel.userReviewData?.collect {
LogUtils.d("Hello it: " + it.code)
setLoadingState(it.state)
when (it.status) {
Resource.Status.ERROR -> showErrorSnackBarLayout(-1, it.message, {
// Retry action button logic
viewModel.userReviewData = null
apiCall()
})
}
}
}
Viewmodel:
var userReviewData: Flow<Resource<ReviewResponse>>? = emptyFlow<Resource<ReviewResponse>>()
fun fetchUserReviewData() {
LogUtils.d("Hello fetchUserReviewData: " + userReviewData)
userReviewData = flow {
emit(Resource.loading(true))
repository.getUserReviewData().collect {
emit(it)
}
}
}
EDIT in ViewModel:
// var userReviewData = MutableStateFlow<Resource<ReviewResponse>>(Resource.loading(false))
var userReviewData = MutableSharedFlow<Resource<ReviewResponse>>()
fun fetchUserReviewData() {
viewModelScope.launch {
userReviewData.emit(Resource.loading(true))
repository.getUserReviewData().collect {
userReviewData.emit(it)
}
}
}
override fun onCreate() {}
}
EDIT in Activity:
private fun setObservers() {
lifecycleScope.launchWhenStarted {
viewModel.userReviewData.collect {
setLoadingState(it.state)
when (it.status) {
Resource.Status.SUCCESS ->
if (it.data != null) {
val reviewResponse: ReviewResponse = it.data
if (!AppUtils.isNull(reviewResponse)) {
setReviewData(reviewResponse.data)
}
}
Resource.Status.ERROR -> showErrorSnackBarLayout(it.code, it.message) {
viewModel.fetchUserReviewData()
}
}
}
}
}
Now, I have only single doubt, should I use state one or shared one? I saw Phillip Lackener video and understood the difference, but still thinking what to use!
The thing is we only support Portrait orientation, but what in future requirement comes? In that case I think I have to use state one so that it can survive configuration changes! Don't know what to do!
Because of the single responsibility principle, the ViewModel alone should be updating its flow to show the latest requested data, rather than having to cancel the ongoing request and resubscribe to a new one from the Fragment side.
Here is one way you could do it. Use a MutableSharedFlow for triggering fetch requests and flatMapLatest to restart the downstream flow on a new request.
A Channel could also be used as a trigger, but it's a little more concise with MutableSharedFlow.
//In ViewModel
private val fetchRequest = MutableSharedFlow<Unit>(replay = 1, BufferOverflow.DROP_OLDEST)
var userReviewData = fetchRequest.flatMapLatest {
flow {
emit(Resource.loading(true))
emitAll(repository.getUserReviewData())
}
}.shareIn(viewModelScope, SharingStarted.WhlieSubscribed(5000), 1)
fun fetchUserReviewData() {
LogUtils.d("Hello fetchUserReviewData: " + userReviewData)
fetchRequest.tryEmit(Unit)
}
Your existing Fragment code above should work with this, but you no longer need the ?. null-safe call since the flow is not nullable.
However, if the coroutine does anything to views, you should use viewLifecycle.lifecycleScope instead of just lifecycleScope.
I am having this issue where I have to navigate when given state gets updated after an asynchronous task gets executed. I am doing it like this:
At ViewModel.kt
fun executeRandomTask() {
viewModelScope.launch {
runAsyncTask()
state = Success
}
}
At Composable.kt
LaunchedEffect(viewModel.state) {
if(viewModel.state is Success) {
navController.navigate("nextScreen")
}
}
Then in the next screen, I click the back navigation button (onBackPressed) and what happens, is that the effect gets launched again. So I end up again in "nextScreen".
When I do this next workaround:
DisposableEffect(viewModel.state) {
if(viewModel.state is Success) {
navController.navigate("nextScreen")
}
onDispose {
viewModel.state = null
}
}
Like this, the viewmodel state gets cleared and it also proves that what is happening is that the navigation controller destroys the previous screen (not sure if it is the intended behavior).
I am not sure about what I should be doing to avoid this, since this is a pretty common scenario and having to clear the state after a certain state is reached looks dirty.
I use SharedFlow for emitting one-time events like this
class MyViewModel : ViewModel() {
private val _eventFlow = MutableSharedFlow<OneTimeEvent>()
val eventFlow = _eventFlow.asSharedFlow()
private fun emitEvent(event: OneTimeEvent) {
viewModelScope.launch { _eventFlow.emit(event) }
}
}
The sealed class for defining events
sealed class OneTimeEvent {
object SomeEvent: OneTimeEvent()
}
And finally in the Screen collect the flow like this
fun MyScreen(viewModel: MyViewModel = hiltViewModel()) {
LaunchedEffect(Unit) {
viewModel.eventFlow.collect { event ->
when(event){
SomeEvent -> {
//Do Something
}
}
}
}
}
So whenever your state changes you can emit some event and take action against it in your Screen.
how can I avoid the collect{} code execute again when navigate back to the fragment.
ViewModel class
private val _commitResult = MutableStateFlow<Map<String, Any>>(mapOf())
val commitResult: StateFlow<Map<String, Any>> = _commitResult
Fragment code like this:
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED){
viewModel.commitResult.collect { data ->
Logger.i("commitResult $data")
//navigate to another fragment
}
}
}
when I change the _commitResult value in viewModel first, jump to another fragment works fine.
unfortunately, when I go back to the fragment. collect{ // navigate to another fragment} will
excute again.
I know when back to the fragment. onCreateView excute again and viewModel will emit the data store
before, so thecollect { // navigate to another fragment} excute. How can I avoid this?
same as LiveData, I use Event to fix this with LiveData.
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
how can I handle this with stateflow? actually I don't like Event<.> to handle this,
am I use the stateflow in a wrong way? how I can fix this?
If anyone who can help, thanks in advance.
StateFlow keeps it's state, so I'd suggest either:
A) Use SharedFlow. https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-shared-flow/
B) Use a pattern where you handle the dismissal of events
class Vm: ViewModel() {
private val mEvent = MutableStateFlow<MyResult?>(null)
val event = mEvent.asStateFlow()
fun dismissEvent() {
mEvent.value = null
}
}
class Frag: Fragment() {
override fun onViewCreated() {
vm.event.collect {
navigate()
vm.dismissEvent()
}
}
}
Scenario
Hi,
I have an Activity with a ViewPager. In the ViewPagerAdapter, I create instances of a same fragment with different data.
And in each instance I initialize a ViewModel
val dataViewModelFactory = this.activity?.let { DataViewModelFactory(it) }
mainViewModel = ViewModelProviders.of(this, dataViewModelFactory).get(MainViewModel::class.java)
In my fragment, I observe two MutableLiveData when I call APIs
mainViewModel.isResponseSuccessful.observe(this, Observer { it ->
if(it) {
//do Something
}else{
Toast.makeText(activity, "Error in Sending Request", Toast.LENGTH_SHORT).show()
}
})
mainViewModel.isLoading.observe(this, Observer {
if (it) {
println("show progress")
} else {
println("dismiss progress")
}
})
In each fragment, on a button click I load another fragment. And if required call and API to fetch data.
PROBLEM
The code comes to the observe block multiple times in my fragment. When I comeback from one fragment to previous fragment, even though no API is called, the code on observe block is executed.
What I tried
I tried using an activity instance in the ViewModel initialization
mainViewModel = ViewModelProviders.of(activity,dataViewModelFactory).get(MainViewModel::class.java)
But it did not work.
Please help,
If you want to prevent multiple calls of your observer u can just change MutableLiveData to SingleLiveEvent. Read this
It might help you:
import java.util.concurrent.atomic.AtomicBoolean
class OneTimeEvent<T>(
private val value: T
) {
private val isConsumed = AtomicBoolean(false)
private fun getValue(): T? =
if (isConsumed.compareAndSet(false, true)) value
else null
fun consume(block: (T) -> Unit): T? =
getValue()?.also(block)
}
fun <T> T.toOneTimeEvent() =
OneTimeEvent(this)
First, when you want to post a value on LiveData, use toOneTimeEvent() extension function to wrap it in a OneTimeEvent:
liveData.postValue(yourObject.toOneTimeEvent())
Second, when you are observing on the LiveData, use consume { } function on the delivered value to gain the feature of OneTimeEvent. You'll be sure that the block of consume { } will be executed only once.
viewModel.liveData.observe(this, Observer {
it.consume { yourObject ->
// TODO: do whatever with 'yourObject'
}
})
In this case, when the fragment resumes, your block of code does not execute again.
I have a Fragment that I want to do a fetch once on its data, I have used distinctUntilChanged() to fetch just once because my location is not changing during this fragment.
Fragment
private val viewModel by viewModels<LandingViewModel> {
VMLandingFactory(
LandingRepoImpl(
LandingDataSource()
)
)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val sharedPref = requireContext().getSharedPreferences("LOCATION", Context.MODE_PRIVATE)
val nombre = sharedPref.getString("name", null)
location = name!!
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
fetchShops(location)
}
private fun fetchShops(localidad: String) {
viewModel.setLocation(location.toLowerCase(Locale.ROOT).trim())
viewModel.fetchShopList
.observe(viewLifecycleOwner, Observer {
when (it) {
is Resource.Loading -> {
showProgress()
}
is Resource.Success -> {
hideProgress()
myAdapter.setItems(it.data)
}
is Resource.Failure -> {
hideProgress()
Toast.makeText(
requireContext(),
"There was an error loading the shops.",
Toast.LENGTH_SHORT
).show()
}
}
})
}
Viewmodel
private val locationQuery = MutableLiveData<String>()
fun setLocation(location: String) {
locationQuery.value = location
}
val fetchShopList = locationQuery.distinctUntilChanged().switchMap { location ->
liveData(viewModelScope.coroutineContext + Dispatchers.IO) {
emit(Resource.Loading())
try{
emit(repo.getShopList(location))
}catch (e:Exception){
emit(Resource.Failure(e))
}
}
}
Now, if I go to the next fragment and press back, this fires again, I know that maybe this is because the fragment is recreating and then passing a new instance of viewmodel and thats why the location is not retained, but if I put activityViewModels as the instance of the viewmodel, it also happends the same, the data is loaded again on backpress, this is not acceptable since going back will get the data each time and this is not server efficient for me, I need to just fetch this data when the user is in this fragment and if they press back to not fetch it again.
Any clues ?
I'm using navigation components, so I cant use .add or do fragment transactions, I want to just fetch once on this fragment when creating it first time and not refetching on backpress of the next fragment
TL;DR
You need to use a LiveData that emits its event only once, even if the ui re-subscribe to it. for more info and explanation and ways to fix, continue reading.
When you go from Fragment 1 -> Fragment 2, Fragment 1 is not actually destroyed right away, it just un-subscribe from your ViewModel LiveData.
Now when you go back from F2 to F1, the fragment will re-subscribe back to ViewModel LiveData, and since the LiveData is - by nature - state holder, then it will re-emit its latest value right away, causing the ui to rebind.
What you need is some sort of LiveData that won't emit an event that has been emitted before.
This is common use case with LiveData, there's a pretty nice article talking about this need for a similar LiveData for different types of use cases, you can read it here.
Although the article proposed a couple of solutions but those can be a bit of an overkill sometimes, so a simpler solution would be using the following ActionLiveView
// First extend the MutableLiveData class
class ActionLiveData<T> : MutableLiveData<T>() {
#MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<T?>) {
// Being strict about the observer numbers is up to you
// I thought it made sense to only allow one to handle the event
if (hasObservers()) {
throw Throwable("Only one observer at a time may subscribe to a ActionLiveData")
}
super.observe(owner, Observer { data ->
// We ignore any null values and early return
if (data == null) return
observer.onChanged(data)
// We set the value to null straight after emitting the change to the observer
value = null
// This means that the state of the data will always be null / non existent
// It will only be available to the observer in its callback and since we do not emit null values
// the observer never receives a null value and any observers resuming do not receive the last event.
// Therefore it only emits to the observer the single action so you are free to show messages over and over again
// Or launch an activity/dialog or anything that should only happen once per action / click :).
})
}
// Just a nicely named method that wraps setting the value
#MainThread
fun sendAction(data: T) {
value = data
}
}
You can find more explainiation for ActionLiveData in this link if you want.
I would advise using the ActionLiveData class, I've been using it for small to medium project size and it's working alright so far, but again, you know your use cases better than me. :)