RemoveObservers not working on onDestroyView? - android

I'm calling the ViewModel from my RecyclerView Adapter to delete an item.
And from my Fragment, I'm observing the LiveData:
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
fragmentUserPaymentMethodsListBinding = FragmentUserPaymentMethodsListBinding.inflate(layoutInflater, container, false)
observeNetworkResult(userPaymentMethodsListViewModel.euLiveData)
observeNetworkResult(userPaymentMethodsListViewModel.usLiveData)
observeNetworkResult(userPaymentMethodsListViewModel.ukLiveData)
observeNetworkResult(userPaymentMethodsListViewModel.eccpLiveData)
observeNetworkResult(userPaymentMethodsListViewModel.localLiveData)
observeNetworkResult(userPaymentMethodsListViewModel.skrillLiveData)
return fragmentUserPaymentMethodsListBinding.root
}
private fun <T> observeNetworkResult(liveData: LiveData<NetworkResult<T>>) {
liveData.observe(viewLifecycleOwner, Observer { result ->
when (result) {
is NetworkResult.Success -> Snackbar.make(
requireView(),
"Payment method has been removed",
Toast.LENGTH_SHORT
).show()
is NetworkResult.Error -> view?.let {
Snackbar.make(
it,
"We couldn't remove this payment method, please try again.",
Snackbar.LENGTH_SHORT
).show()
}
is NetworkResult.Loading -> Unit // do nothing, or show a progress indicator
}
})
}
Even after using the following on the onDestroyView:
override fun onDestroyView() {
super.onDestroyView()
Log.d("onDestroyView","Called")
userPaymentMethodsListViewModel.euLiveData.removeObservers(viewLifecycleOwner)
userPaymentMethodsListViewModel.usLiveData.removeObservers(viewLifecycleOwner)
userPaymentMethodsListViewModel.ukLiveData.removeObservers(viewLifecycleOwner)
userPaymentMethodsListViewModel.eccpLiveData.removeObservers(viewLifecycleOwner)
userPaymentMethodsListViewModel.localLiveData.removeObservers(viewLifecycleOwner)
userPaymentMethodsListViewModel.skrillLiveData.removeObservers(viewLifecycleOwner)
}
I still have the same problem:
The Snackbar should only be triggered on the NetworkResult.Success. But for some reason, when I delete an item, go to the previous fragment, and then go back to this one... the Snackbar popup again. What could be causing this issue? especially when I'm removing the observers.

Related

How to display Toast.makeText result in another page textView using kotlin

//This is my scanner barcode code using kotlin
override fun receiveDetections(detections: Detector.Detections) {
val barcodes = detections.detectedItems
if (barcodes.size() == 1) {
scannedValue = barcodes.valueAt(0).rawValue
runOnUiThread {
cameraSource.stop()
Toast.makeText(this#InsertStockInActivity, "value- $scannedValue", Toast.LENGTH_SHORT).show()
finish()
}
}else
{
Toast.makeText(this#InsertStockInActivity, "value- else", Toast.LENGTH_SHORT).show()
}
}
//This is my input page
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentInputStockInBinding.inflate(inflater, container, false)
binding.btnScanBarcode.setOnClickListener{ nav.navigate(R.id.insertStockInActivity)}
return binding.root
}[enter image description here][1]
i thnk you should extract your toast text as a variable and pass it to another page ( i assume that your mean are to another fragment/activity or to another screen)
private lateinit var toastText:String
...
if (barcodes.size() == 1) {
...
toastText = scannedValue
...
} else {
toastText = "value- else"
}
Toast.makeText(this#InsertStockInActivity, toastText , Toast.LENGTH_SHORT).show()
}
and pass toastText to another page via Intent or safe-args if you are using Jetpack Navigation
You could use a view model, live data, or another (static) object to hold your results in a variable. Along the same lines, you can create a show toast function in another class and just pass the context of the fragment or activity that you are in. For example a fragment context could be requireContext().
fun showToast(context: Context?, message: String) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}

Kotlin App Crash if back button pressed during fetching API

So i have the problem that the App will crash if the Back button is pressed during fetching service in a fragment. Currently I'm using Retrofit Library to do the service calling tasks. Below is the code snippet of the fragment :
ProductStockOutletListFragment.kt
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
(activity as AppCompatActivity).supportActionBar?.title = "Product List"
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_product_stock_outlet_list, container, false)
//Show Progressbar While loading data
binding.progressBar.visibility = View.VISIBLE
//Apply layout manager
binding.rvOutletList.layoutManager = LinearLayoutManager((activity as AppCompatActivity))
NetworkConfig().getOutletListService()
.getOutlets()
.enqueue(object : Callback<OutletListPOJODataClasses> {
override fun onFailure(call: Call<OutletListPOJODataClasses>, t: Throwable) {
if(call.isCanceled){
Toast.makeText((activity as AppCompatActivity), "Request Aborted", Toast.LENGTH_SHORT).show()
}else{
Toast.makeText((activity as AppCompatActivity), t.localizedMessage, Toast.LENGTH_SHORT).show()
}
}
override fun onResponse(
call: Call<OutletListPOJODataClasses>,
response: Response<OutletListPOJODataClasses>
) {
binding.progressBar.visibility = View.GONE
binding.rvOutletList.adapter = response.body()?.let { OutletListAdapter(it, this#ProductStockOutletListFragment) }
Toast.makeText((activity as AppCompatActivity), "Data retrieved!", Toast.LENGTH_SHORT).show() //It points out this line. This is where the error happened.
}
})
// Declare that this fragment has menu
setHasOptionsMenu(true)
// Set action bar title to "Outlet List"
(activity as AppCompatActivity).supportActionBar?.title = "Outlet List"
return binding.root
}
If back button is pressed, it'll crash and return the error
kotlin.TypeCastException: null cannot be cast to non-null type androidx.appcompat.app.AppCompatActivity
The error pointed out at the below line at OnResponse()
Toast.makeText((activity as AppCompatActivity), "Data retrieved!", Toast.LENGTH_SHORT).show()
Am I missing something ? Or maybe this is a lifecycle-related problem ? Let me know if there's anything Unclear.
Edit : As requested, This is the full error log.
The exception is because the activity you used in Toast becomes null when you backpress (fragment is destroyed) but the api response is received later and toast is executed.
So just add a null check before toasting:
if(activity!=null){
Toast.makeText((activity as AppCompatActivity), "Data retrieved!", Toast.LENGTH_SHORT).show()
}
You should only handle the response in your Fragment if your fragment is isAdded.
When you exit the activity, context is null
your can use applicationContext
Toast.makeText(applicationContext, "Data retrieved!", Toast.LENGTH_SHORT).show()

Recyclerview Duplicate Values with LiveData onBackPressed in Fragment

I have a recyclerview with load more, and I can't store the values in the database. So when I load the data, everything works perfectly. The problem occurs when I navigate to another fragment, and click onBackPressed, the onChanged of the observer is being called again and it is giving me the last values called from the API. Then as you can see in the code below, they are automatically being added to the list and published in UI
productsViewModel.productsObject.observe(viewLifecycleOwner, Observer { result ->
if (result.status != Status.LOADING) {
(activity as MainActivity?)!!.dismissProgressDialog(getView())
if (result.status == Status.SUCCESS && result.data != null) {
val productsResult = parseObject(result.data.asJsonObject)
if (!productsResult.isNullOrEmpty()) {
products.addAll(productsResult)
productAdapter.submitList(products)
progressBar.visibility = View.GONE
numberSearch.text = products.size.toString() + "/" + total + " " + resources.getString(R.string.items_found)
} else if (result.status == Status.ERROR) {
if (result.message.isNullOrBlank())
Utils.showSnackBar(requireContext(), view, getString(R.string.something_wrong_try_again), true)
else
Utils.showSnackBar(requireContext(), view, result.message, true)
}
}
}
})
I think that you are not observing your result correctly.
For example, if the value of result is Status.LOADING what happens? After you enter that block, maybe there are other cases that you are not handling. We don't know because there is no other when or else if block.
Another way more "clean" would be:
productsViewModel.productsObject.observe(viewLifecycleOwner, Observer { result ->
when(result.status) {
Status.LOADING -> manageLoading()
Status.SUCCESS -> manageSuccess()
Status.ERROR -> manageError()
else -> manageBaseCase()
}
}
Thus, it is not all embedded in a unique if block making the management more understandable.
In your Fragment you should manually send an event to "reset" the actual state of the LiveData so that you will be sure that there won't be duplicate items.
Your Fragment may look something like this:
class ProductsFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.product_fragment, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
observe()
productsViewModel.send(Event.Load)
}
}

How to handle API calls in fragments while using navGraph?

I'm using Navigation component in one of project which is having bottom navigation view with three menu items(for example home,profile and about). Landing page of my app is home fragment(for example) at which one API is called(in onCreateView() method) to get user lists; its working fine but whenever user navigates to some other page like profile and come back means again API gets called in home fragment.
I referred this link - https://github.com/googlesamples/android-architecture-components.git
class Home : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_home, container, false)
view.findViewById<Button>(R.id.signup_btn).setOnClickListener {
findNavController().navigate(R.id.action_home_to_registered)
}
callUserListApi()
return view
}
private fun callUserListApi() {
val client = ServiceGenerator.getApiService(requireContext()).getJsonbyWholeUrl("http://dummy.restapiexample.com/api/v1/employees")
client.enqueue(object : Callback<JsonArray> {
override fun onFailure(call: Call<JsonArray>, t: Throwable) {
println("callUserListApi onFailure ${t.message}")
}
override fun onResponse(call: Call<JsonArray>, response: Response<JsonArray>) {
println("callUserListApi onResponse ${response.isSuccessful}")
}
})
}
}
Using navigation componentthe fragment were recreate every time when you select the tab. So here i've inflate fragment if my view is null when come back to those fragment at that moment view is not null so fragment should not recreate.
private var homeFragment: View? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
if (view == null){
homeFragment = inflater.inflate(R.layout.fragment_home, container, false)
callUserListApi()
}
return view
}
Using the Navigation component, the Fragment were recreated each time the it was selected on the navigation, meaning the onCreateView() is called that includes youre callUserListApi()
Since you are using AAC, you can create a ViewModel on your activity and initialize it, then re-initializing it from the Home(Fragment). Add a MutableLiveData() variable which you can check if you alread called callUserListApi().
Dont forget to call it inside onActivityCreated()
On Home
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
mainActivityViewModel.calledUserList.observe(viewLifecycleOwner, Observer {
if(it != true) {
callUserListApi()
}
})
}
private fun callUserListApi() {
val client = ServiceGenerator.getApiService(requireContext()).getJsonbyWholeUrl("http://dummy.restapiexample.com/api/v1/employees")
client.enqueue(object : Callback<JsonArray> {
override fun onFailure(call: Call<JsonArray>, t: Throwable) {
println("callUserListApi onFailure ${t.message}")
}
override fun onResponse(call: Call<JsonArray>, response: Response<JsonArray>){
println("callUserListApi onResponse ${response.isSuccessful}")
mainActivityViewModel.setCalledUserList(true)
}
})
}
Inside youre MainActivityViewModel
val _calledUserList = MutableLiveData<Boolean?>()
val calledUserList = _calledUserList
fun setCalledUserList(bool: Boolean?) {
_calledUserList.value = bool
}

Bottom Sheet Drawer in Material design

class BottomNavigationDrawerFragment: BottomSheetDialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_bottomsheet, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
navigation_view.setNavigationItemSelectedListener { menuItem ->
// Bottom Navigation Drawer menu item clicks
when (menuItem!!.itemId) {
R.id.nav1 -> context!!.toast("you clicked one")
R.id.nav2 -> context!!.toast("you clicked two")
R.id.nav3 -> context!!.toast("you clicked three")
}
true
}
}
// This is an extension method for easy Toast call
fun Context.toast(message: CharSequence) {
val toast = Toast.makeText(this, message, Toast.LENGTH_SHORT)
toast.setGravity(Gravity.BOTTOM, 0, 600)
toast.show()
}
}
What i want to achieve is after clicking navigation icon in bottom app bar a modal bottom sheet is created and and a navigation drawer is shown in it.In above code i kept three items in it.Up to here everything is okay but when comes to handling item clicking part then the line:
navigation_view.setNavigationItemSelectedListener { menuItem ->
shows error. It tells :
unresolved type:setNavigationItemSelectedListener
And unresolved type in:menuItem
Here is fragment_bottomsheet.xml :
<android.support.design.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.navigation.NavigationView
android:id="#+id/navigation_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:menu="#menu/bottom_nav_drawer_menu"/>
</android.support.design.widget.ConstraintLayout>
What is wrong in am doing here?
It seems like issue comes from Context.toast(message: CharSequence) which you could use anko for that:
implementation "org.jetbrains.anko:anko:0.10.6"
Anyways, you can check the output by:
Toast.makeText(context, "Clicked", Toast.LENGTH_LONG).show()
And you'll see that it will work.
UPDATE:
Try using this inside your BottomNavigationDrawerFragment:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
navigation_view.setNavigationItemSelectedListener { menuItem ->
// Bottom Navigation Drawer menu item clicks
when (menuItem.itemId) {
R.id.home ->{
Toast.makeText(context, "Clicked", Toast.LENGTH_LONG).show()
}
// R.id.nav1 -> context!!.toast(getString(R.string.nav1_clicked))
}
// Add code here to update the UI based on the item selected
// For example, swap UI fragments here
true
}
close_imageview.setOnClickListener {
this.dismiss()
}
disableNavigationViewScrollbars(navigation_view)
}
The below method is worked for me.
class BottomNavigationDrawerFragment : BottomSheetDialogFragment() {
private val TAG: String = ImageChooserDialog::class.simpleName.toString()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.bottom_sheet_image_chooser, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.findViewById<NavigationView>(R.id.navigation_view)
.setNavigationItemSelectedListener {
when(it.itemId){
R.id.action_camera -> Log.e(TAG, "action_camera")
R.id.action_gallery -> Log.e(TAG, "action_gallery")
}
return#setNavigationItemSelectedListener true
}
}
}

Categories

Resources