My goal is to allow only one single instance of the same dialog fragment in the fragment stack.
The current trigger condition is coming from a SharedFlow and can be triggered as often as 7ms apart between values.
Here's what I have tried:
Placing the code in a synchronized block
Checking whether existing fragment is in the stack by calling fm.findFragmentByTag
However, both the conditions are not enough to prevent the fragment from adding multiple times to the fragmentManager.
I tried with dialogFragment.showNow(fm, tag) but it's unstable and it's crashing
Appreciate for any helps.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.someSharedFlow
.flowWithLifecycle(viewLifecycleOwner.lifecycle)
.onEach { showMyFragmentDialog() }
.launchIn(viewLifecycleOwner.lifecycleScope)
}
private fun showMyFragmentDialog() {
synchronized(childFragmentManager) {
if (childFragmentManager.findFragmentByTag(MyFragment.TAG) == null) {
MyFragment.newInstance(fuelTypes)
.show(childFragmentManager, MyFragment.TAG)
}
}
}
Resorted with coroutine for now. Not ideal but at least it's working.
private var myLaunchJob: Job? = null
private fun showMyFragmentDialog() {
if (myLaunchJob?.isActive == true) return
myLaunchJob = viewLifecycleOwner.lifecycleScope.launch {
if (childFragmentManager.findFragmentByTag(MyFragment.TAG) == null) {
MyFragment.newInstance(fuelTypes)
.show(childFragmentManager, MyFragment.TAG)
}
// Act as debouncer
delay(1000)
}
}
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.
So, I would like to use StateFlow instead of LiveData, but I can not figure out what's the problem with my logic.
I have a flow, which has a default null value. When I open a dialog which contains a some datas, after that I select one data, I emit the new value to the flow.
In the first time, after the dialog closed, collectLatest called, and I get the null value (init), after the emit, I get the new value, it is good. But If I open the dialog again, and select value, and close the dialog, the collectLatest fun called 3-times, and I again open the dialog... and collectLatest called 4 times and so on.
So this is very bad behavior, and I'm sure , I did something wrong, but I don't see the bug.
In the liveData the expected behavior is after the dialog close, that the observer fun is called just once. I would like to achive this.
I also checked, that I emit the new value only once, so there is no reason why collectLatest fire multiple times.
ViewModel:
private val _previousManufacture = MutableStateFlow<PreviousManufactureView?>(null)
val previousManufacture = _previousManufacture.asStateFlow()
private suspend fun setPreviousManufactureByMachineId(machineId: String) {
val result = stateReportRepository.getPreviousManufactureByMachineId(machineId)
if (result is Result.Success) {
_previousManufacture.emit(result.data)
} else {
_previousManufacture.emit(null)
}
}
Fragment:
lifecycleScope.launchWhenCreated {
viewModel.previousManufacture.collectLatest {
var d = it
}
}
[Update]
Fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.vm = viewModel
initFlows()
}
private fun initFlows() {
lifecycleScope.launchWhenCreated {
viewModel.openStateOfWorkflowBrowser.collectLatest {
openStateOfWorkflowSelectionDialog()
}
}
...
}
Sorry, I missed this before in my comment, but I think the problem is that you are calling launchWhenCreated in the lifecycleScope of the Fragment, not in its viewLifecycle.lifecycleScope. So if the Fragment is reused (like after a dialog fragment has a appeared), the old collector is not cancelled and a new one is added, because the lifecycle of the Fragment has not ended, only the lifecycle of its previous view. You should almost always use viewLifecycle.lifecycleScope when you are using coroutines in a Fragment.
I am creating a calculator. But there are too many setOnClickListeners() making it harder to progress. This belongs to a fragment and it also has a ViewModel. I am using dataBinding here.
If there is any way I can write less code in the below mentioned context.
if there is any confusion about the question, please write in the comment. If my approach is wrong, share in the comments
MY code:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.calculatorViewModel = viewModel
binding.lifecycleOwner = viewLifecycleOwner
viewModel.currentExpression.value = "814×122" //temporary value
viewModel.currentResult.value = "99308" //temporary value
binding.etInput.doAfterTextChanged {
viewModel.currentExpression.value = it.toString()
binding.tvOutputPreview.text = viewModel.currentExpression.value
}
binding.apply {
// Extra operators - setOnClickListener
btnClear.setOnClickListener { viewModel.onClear() }
btnAllClear.setOnClickListener { viewModel.onAllClear() }
btnPlusMinus.setOnClickListener { }
btnEqual.setOnClickListener { }
// Operators - setOnClickListener
btnDivide.setOnClickListener {
viewModel.mountOperator(btnDivide.text) }
btnMultiply.setOnClickListener { viewModel.mountOperator(btnMultiply.text) }
btnMinus.setOnClickListener { viewModel.mountOperator(btnMinus.text) }
btnPlus.setOnClickListener { viewModel.mountOperator(btnPlus.text) }
//Secondary operators - setOnClickListener
btnPercent.setOnClickListener { }
btnDecimal.setOnClickListener { }
// Numbers - setOnClickListener
btn0Num.setOnClickListener { }
btn1Num.setOnClickListener { }
btn2Num.setOnClickListener { }
btn3Num.setOnClickListener { }
btn4Num.setOnClickListener { }
btn5Num.setOnClickListener { }
btn6Num.setOnClickListener { }
btn7Num.setOnClickListener { }
btn8Num.setOnClickListener { }
btn9Num.setOnClickListener { }
}
binding.btnClear.setOnClickListener { viewModel.onClear() }
binding.btnAllClear.setOnClickListener { viewModel.onAllClear() }
binding.btnPlusMinus.setOnClickListener { }
}
That's okay to have too many click listeners.
but what you can do to make it look cleaner is you can set the click listener to this fragment or activity.
for example:
btn0Num.setOnClickListener(this)
and then implement View.OnClickListener in your class
and override the onClick method.
override fun onClick(v: View?) {
v?.let {
when(it){
btn0Num -> {
//Todo do something when the button is clicked
}
btn1Num -> {
//Todo do something when the button is clicked
}
}
}
}
Calculator crew today is it
I just posted this on another question, but if you have repetitive code like that, use a loop!
listOf(btnDivide, btnMultiply, btnMinus, btnPlus).forEach {
it.setOnClickListener { //bla bla }
}
In this case, since your buttons are logically grouped and you might need to refer to a group again, you might want to keep those lists around as a top-level variable:
// lateinit so we don't have to assign it yet, just make sure it's set
// before it's read!
lateinit var digitButtons: List<Button>
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
with(binding) {
digitButtons = listOf(btn0Num, btn1Num...)
}
}
then you can refer to it with things like digitButtons.forEach, or if (button in digitButtons) etc
If you're worried about creating so many click listeners, you can reuse a function:
fun handleClick(view: View) {
// do the stuff
}
digitButtons.forEach {
setOnClickListener { handleClick(it) }
}
// or with a function reference instead of a lambda
digitButtons.forEach {
setOnClickListener(::handleClick)
}
And your handling code can use those lists from earlier too
fun handleClick(view: View) {
when {
view !is Button -> return
view in digitButtons -> whateverYouDoWithThose(view)
}
}
but personally I'd go for separate functions for each button type - for the digits, call a function that handles those. For an operator, call a different function. It's easier to read than one giant "a button was clicked here's how we handle each one" function, and it's more informative when you're assigning the click listeners too, since it reads like "when clicked, do this" and handleDigitPressed is better than handleClick, in my opinion anyway!
And setOnClickListener(::clear) is definitely better, you can immediately see what that button does without having to go look it up in the general click handler function. Having separate, well-named functions can make things a lot easier to parse
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!
Alright, so I have a fragment that displays user account information.
The way this works is it issues a request that may or may not touch the server, depending on what information is already cached.
When the information is available, there's a callback in which the fragment will update its textfields.
At that point, however, the UI needs be refreshed, so I'd like to issue some kind of invalidate...
Except that does perfectly nothing.
First, I noticed that this.view returns null. So rather than rely on that, I store the view I create in onCreateView explicitly.
Then after I updated the textfields, I call fragmentView.postInvalidate() ... which does nothing.
I also tried doAsync { uiThread { fragmentView.invalidate() } } ... which also does nothing.
Then I found this answer and tried
val fragment = this
doAsync { uiThread {
activity!!.supportFragmentManager.beginTransaction().detach(fragment).commit()
activity!!.supportFragmentManager.beginTransaction().attach(fragment).commit()
}}
... which is even WORSE than not doing anything, because the other two ways will at least update the view if you switch to a different fragment and back, whereas this will perpetually refuse to display any useful information.
So maybe I can "cheat" by delaying the call to super::setUserVisibleHint until after I updated the values ... no, I can't.
What I can do, though, is I can force a redraw of the ui by toasting something. So that's what I am currently doing. But still, that can't be the solution.
How do I get a proper ui refresh?
Oh, and this is an android.support.v4.app.Fragment and I'm using an android.support.v4.app.FragmentStatePagerAdapter to switch between fragments, if that matters.
Full fragment code:
class AccountFragment : Fragment() {
lateinit var fragmentView: View
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
super.onCreateView(inflater, container, savedInstanceState)
val view = inflater.inflate(R.layout.fragment_account, container, false)
/*for some reason [this.view] remains null when we later try to use it, so we're explicitly storing a reference*/
fragmentView = view
return view
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
if(isVisibleToUser)setAccountInformation()
super.setUserVisibleHint(isVisibleToUser)
}
private fun setAccountInformation(){
val um = DataConfig.getUserManager()
val au = um.getActiveUser()
um.getUserInfo(au, Callback(
onResponse = {when(it){
is SuccessfulAccountGetResponse -> {
val info = it.result
usernameText.text = info.name
uuidText.text = info.uuid
adminText.text = if(info.isAdmin) "YES" else "NO"
createdText.text = info.created.toString()
lastLoginText.text = info.lastLogin?.toString() ?: "-"
//now actually force the new information to show up
refreshUI()
}
else -> doAsync { uiThread{ activity?.longToast("could not get information") } }
}},
onError = {doAsync { uiThread { activity?.longToast("error: $it") } }}
))
}
/** forces a refresh, making changes visible*/
private fun refreshUI(){
/*hack: force a redraw by toasting */
doAsync { uiThread { activity?.toast("updating values") } }
//THIS does absolutely nothing: //TODO: figure out why
// fragmentView.postInvalidate()
}
}