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()
Related
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.
//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()
}
newbie here! I'm having a hard time learning the life cycle of fragments and I'm stuck in this problem. If I run this on the emu, the fragment is showing on the activity but the button inside the fragment needs to be clicked twice to run the destination activity.
FragmentSetting.kt:
class FragmentSetting : Fragment(), View.OnClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view: View = inflater!!.inflate(R.layout.fragment_main_setting, container, false)
val btnLogout: Button = view.findViewById(R.id.btn_logout)
val btnArticle: Button = view.findViewById(R.id.btn_art)
btnLogout.setOnClickListener(this)
btnArticle.setOnClickListener(this)
return view
}
companion object {
fun newInstance(): FragmentSetting {
return FragmentSetting()
}
}
override fun onClick(v: View?) {
when (v?.id) {
R.id.btn_logout -> {
btn_logout.setOnClickListener {
Toast.makeText(getContext(), "Signed Out.", Toast.LENGTH_SHORT).show()
FirebaseAuth.getInstance().signOut()
val intent = Intent(activity, SignInActivity::class.java)
startActivity(intent)
}
}
R.id.btn_art -> {
btn_art.setOnClickListener {
Toast.makeText(getContext(), "Hello World", Toast.LENGTH_SHORT).show()
val intent = Intent(activity, ArticleActivity::class.java)
startActivity(intent)
}
}
}
}
}
You are setting the Fragment class itself as the listener to both buttons when the view is created. That would be an acceptable place to do it.
However, your listener function doesn't perform your desired action of your button. Instead, it is setting a new listener for each button. So the first time they are clicked, they get their new listener. It is only in that inner secondary listener that you are going to another activity, so that's why it's taking two clicks.
You need to directly do your action instead of wrapping the action inside setting another listener. By the way, the view passed to a click listener is never null, so you can remove the ?'s.
override fun onClick(v: View) {
when (v.id) {
R.id.btn_logout -> {
Toast.makeText(getContext(), "Signed Out.", Toast.LENGTH_SHORT).show()
FirebaseAuth.getInstance().signOut()
val intent = Intent(activity, SignInActivity::class.java)
startActivity(intent)
}
R.id.btn_art -> {
Toast.makeText(getContext(), "Hello World", Toast.LENGTH_SHORT).show()
val intent = Intent(activity, ArticleActivity::class.java)
startActivity(intent)
}
}
}
there are two different things number one if you are implement View.OnClickListener interface so defiantly you use onclick call back method they will fire every time when ever you click it mean in onclick method execute piece code write in onclick method so in your current code first click they will set setOnclicklistener and then next time you click then it will work as per expected.
so simple is that you directly execute code with in curls braces don't need to set setOnClickListener.
For Example :
R.id.btn_art -> {
Toast.makeText(getContext(), "Hello World", Toast.LENGTH_SHORT).show()
val intent = Intent(activity, ArticleActivity::class.java)
startActivity(intent)
}
second thing is that you set setOnClickOnClickListenre in your onCreateView and don't implement clicklistenere.
For Example:
btn_logout.setOnClickListener {
Toast.makeText(getContext(), "Signed Out.", Toast.LENGTH_SHORT).show()
FirebaseAuth.getInstance().signOut()
val intent = Intent(activity, SignInActivity::class.java)
startActivity(intent)
}
but they best practice you can handle click listener in onclick method as per view architecture.
I am in an activity fragment whereby i want to display a toast widget after the commands for the submit button have been met and completed.
the code:
class HomeFragment : Fragment() {
private val currentUserDocRef = Firebase.firestore.collection("users")
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.apply {
submitbutton.setOnClickListener {
FirestoreUtil.updateCurrentUser(
edittextPersonname.text.toString(),
editTextBio.text.toString(),
editTextTextEmailAddress.text.toString(),
edittextage.text.toString()
)
}
return view
}
}
no error is present in my code however on trying to declare a toast widget i get an error. code:
Toast.makeText(this#HomeFragment, "saving", Toast.LENGTH_SHORT).show()
the error:
You need a context to show toast, here is the code :
Toast.makeText(this#HomeFragment.requireActivity(), "saving", Toast.LENGTH_SHORT).show()
Thanks
The context should not be nullable type. The error shows that it is a type mismatch.
Option 1:
Toast.makeText(context!!, "saving", Toast.LENGTH_SHORT).show()
The !! (not-null assertion operator) is used to denote the variable is not null.
Option 2:
Using let and safe calls
context?.let{ context->
Toast.makeText(context, "saving", Toast.LENGTH_SHORT).show()
}
Refer: https://kotlinlang.org/docs/reference/null-safety.html for more details
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
}