unsaved warning on back pressed in fragment - android

I want to show dialog when user press back or quit from fragment if there are some data unsaved. I am trying to override onbackpressed but unfortunately I got error lateinit property barcodeList has not been initialized. how to solve it?
here is my script on activity:
override fun onBackPressed() {
val theFragment = supportFragmentManager.fragments
for(i in 0 until theFragment.size)
{
if(theFragment[i].tag == "stocker_fragment")
{
StockerFragment().onBackPressed()
}
}
}
and this is in fragment:
fun onBackPressed() {
var check = false
// this barcodeList variable error.
for(i in 0 until barcodeList.size)
{
if(barcodeList[i].barcode.trim()=="")
{
check = true
break
}
}
if (check)
{
AlertHelper(context).onBackPressedAlert()
}
}
FYI: I have initialized barcodeList on onCreateView and everything is fine. only error in onBackPressed.
And my last question is, how do i know if user quit from fragment without pressing back button?

I think the problem is in your onBackPressed() implementation in the Activity. With the line StockerFragment().onBackPressed() you are creating a new instance of the StockerFragment and calling onBackPressed() on it, rather than calling it on the instance that is actively being used.
You should be able to adjust your Activity onBackPressed() like so:
override fun onBackPressed() {
val theFragment = supportFragmentManager.fragments
for(i in 0 until theFragment.size)
{
if(theFragment[i].tag == "stocker_fragment")
{
(theFragment[i] as StockerFragment).onBackPressed()
}
}
}
You can also make this a bit more kotliny like so:
override fun onBackPressed() {
supportFragmentManager.fragments.forEach { fragment ->
if (fragment is StockerFragment) {
fragment.onBackPressed()
}
}
}
You'll probably also want to figure out a way to decide whether the fragment's onBackPressed has determined that the Activity should stick around or not. Then, if the fragment is happy, you should call super.onBackPressed() in the Activity so that the expected back behavior (leave the Activity) happens.

Related

custom back button - Android Navigation

I have a form and want to get a confirmation message from the user before the user leaves it.
i want provide custom back button when user touch this button:
i try this:
val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
}
}
requireActivity().onBackPressedDispatcher.addCallback(this,onBackPressedCallback)
but only seems to work for providing custom back behavior to the built-in software/hardware back button and not the back arrow button
How can I do this?
Use onSupportNavigateUp, and replace yourCurrentFragmentID to your current fragment id. All these should be done in MainActivity.
navController = findNavController(R.id.nav_host_fragment)
setupActionBarWithNavController(navController!!)
override fun onSupportNavigateUp(): Boolean {
return when(navController?.currentDestination?.id) {
R.id.yourCurrentFragmentID -> {
showDialog()
true
}
else -> navController?.navigateUp()!!
}
}
Edit
If your fragment already use onOptionsItemSelected, you can handle the logic by checking itemId.
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val id = item.itemId
if (id == R.id.save) {
// your save button code logic
}else if(id ==android.R.id.home){
// display confirmation message
}
return super.onOptionsItemSelected(item)
}
Use this code to set Activity on backpress.
override fun onBackPressed() {
if (isDiscardChanges) {
discardDialog()
} else {
super.onBackPressed()
}
}
If you only want to go back from Activity, then you can add within AndroidManifest.xml child class Activity.
android:parentActivityName="Parent_Activity_Name

Application Crash/Navigation issue with observer when clicking "back" from fragment

I have a fragment A which sends a search query to the network, and if the result is positive uses Android navigation component to navigate to fragment B, and its done using observers.
After navigation to fragment B, i click on "<-" arrow on the top of the screen, but instead of navigating back to fragment A it reloads fragment B again. And if using the native "back" button on the device, the app crashes with "illegalArgumentException navigation destination unknown" error.
I check the internet for clues on this issue, but all i learned is that this happens because i am using .observe in onViewCreated() and when i go back, it gets called again, and because livedata has something in it already, it just navigates me back to B.
I have tried observing in onActivityCreated(), and using getViewLifeCycleOwner, but no success... the only thing that helped is checking if livedata has observers and returning if true, before using .observe, but it seems incorrect.
This is the viewModel:
private val getAssetResult = MutableLiveData<GeneralResponse<Asset>>()
private val updateAssetResult = MutableLiveData<GeneralResponse<Int>>()
private val deleteAssetResult = MutableLiveData<GeneralResponse<Int>>()
init {
state.value = ViewState(false)
Log.d(TAG, "State in init: $state")
}
fun getAssetResult(): LiveData<GeneralResponse<Asset>>{
return getAssetResult
}
fun findAsset(req: GetAssetRequest) {
scope.launch {
setProgressIndicator(true)
val result = repository.getAsset(req)
getAssetResult.postValue(result)
setProgressIndicator(false)
}
}
This is the fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this).get(EditAssetViewModel::class.java)
setupViewModel()
initFields()
}
private fun setupViewModel() {
if (viewModel.getAssetResult().hasObservers()) // <- This is the part that prevents the app from crashing.
return
viewModel.getAssetResult().observe(this, Observer {
if (it == null) return#Observer
handleSearchResult(it)
})
if (viewModel.getState().hasObservers())
return
viewModel.getState().observe(this, Observer { handleState(it) })
}
private fun handleSearchResult(response: GeneralResponse<Asset>) {
if (response.singleValue == null) {
Toast.makeText(context!!, response.errorMessage, Toast.LENGTH_SHORT).show()
return
}
targetFragment?.let { it ->
val bundle = bundleOf("asset" to response.singleValue)
when(it) {
"UpdateLocation" ->
Navigation.findNavController(view!!).navigate(R.id.updateLocation, bundle)
"EditAsset" -> {
Navigation.findNavController(view!!).navigate(R.id.editAsset, bundle)
}
}
}
}
if i remove this part from the setupViewModel function:
if (viewModel.getAssetResult().hasObservers())
return
the app will either crash when clicked "back" using the device button or go back to fragment A, just to be navigated back to fragment B because of the .observe function.
Override the method onBackPressed() to handle the "<-" arrow
Seems like the LiveData that you use to signal to fragment A that it should navigate to fragment B is actually an event. An event happens only once and once it is consumed (navigation event is done), it is gone. Therefore, after navigating you need to send a message to the viewmodel that the navigation took place and that the corresponding data holder should be (e.g.) null again. In Fragment A you check that the new value is unequal to null, and only if this is the case, you issue the navigation event. This would prevent fragment A to immediatelly switch to B again in the back scenario.
If you want to learn more about ways to use live data for events, please refer to this article.

OnBackPressedCallback is being called, but app is not going back

I recently updated my dependencies to include the OnBackPressedCallback change from an interface into an abstract class.
I have set things up according to the new documentation here but I feel like things are not working as they should.
My fragment's OnCreate looks a lot like the documentation:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requireActivity().onBackPressedDispatcher.addCallback(this) {
backPressed()
}
}
When I press the back button, the code in backPressed() is run, but nothing more happens.
I have tried calling handleBackPressed() and requireActivity().onBackPressedDispatcher.onBackPressed() and requireActivity().onBackPressed() from inside the callback, but those all cause a StackOverflowError because it seems to run that callback recursively.
There has got to be something really obvious I am missing...
There has got to be something really obvious I am missing...
You forget to disable your custom callback in you fragment before asking Activity to handle back pressed.
My solutiuon suitable for me:
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final OnBackPressedCallback callback = new OnBackPressedCallback(true) {
#Override
public void handleOnBackPressed() {
if (/*situation to handle back pressing*/){
//here handle your backPress in your fragment
} else {
setEnabled(false); //this is important line
requireActivity().onBackPressed();
}
}
};
requireActivity().getOnBackPressedDispatcher().addCallback(this, callback);
}
When you register an OnBackPressedCallback, you are taking on the responsibility for handling the back button. That means that no other on back pressed behavior is going to occur when you get a callback.
If you're using Navigation, you can use your NavController to pop the back stack:
requireActivity().onBackPressedDispatcher.addCallback(this) {
backPressed()
// Now actually go back
findNavController().popBackStack()
}
This works for me in androidx.appcompat:appcompat:1.1.0
requireActivity().onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
Log.d(TAG, "Fragment back pressed invoked")
// Do custom work here
// if you want onBackPressed() to be called as normal afterwards
if (isEnabled) {
isEnabled = false
requireActivity().onBackPressed()
}
}
}
)
You can also remove callback instead of setting enabled if it's no longer needed. I use it with nested graph like this because when you touch back in a nested nav graph with it's NavHostFragment, it removes it from main fragment back stack instead of opening last fragment in nested nav graph.
// Get NavHostFragment
val navHostFragment =
childFragmentManager.findFragmentById(R.id.nested_nav_host_fragment)
// ChildFragmentManager of the current NavHostFragment
val navHostChildFragmentManager = navHostFragment?.childFragmentManager
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
val backStackEntryCount = navHostChildFragmentManager!!.backStackEntryCount
if (backStackEntryCount == 1) {
// We are at the root of nested navigation, remove this callback
remove()
requireActivity().onBackPressed()
} else {
navController?.navigateUp()
}
}
}
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)

How to change the view of the previous fragment after returning to it using the back button?

I have an activity with two fragments.
The second one is called when I click on something to the first.
What I want is this : if i click on "back" button, I want to go back to the first fragment (that is working), but I want to set the visibility to VISIBLE on an element (if the first fragment is called with back press only)
How do I do that ?
I tried something like this (in my main fragment), I've found the idea in another topic, but this is trigger always in my main activity :
override fun onResume() {
super.onResume()
view?.isFocusableInTouchMode = true
view?.requestFocus()
view?.setOnKeyListener { v, keyCode, event ->
if(event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK){
Log.i(TAG, "backpress pressed")
return#setOnKeyListener true
}
return#setOnKeyListener false
}
}
Temporary solution :
I've created a companion object with a value true or false and I change it everytime I need it, but it's temporary only.
Assuming your second Fragment replaces the first (i.e. using FragmentTransaction#replace), your first Fragment (we'll call them FragmentA and FragmentB) will be paused (i.e. onPause() will be called on FragmentA).
When you press the back button, the backstack will be popped, and FragmentA will be resumed (i.e. onResume() will be called).
What I would recommend, is to save a boolean flag in FragmentA, and set it to true when you show FragmentB. Then, in FragmentA#onResume, you can check if the flag is set to true, and set it back to false while handing the case that you wanted.
For example, something like:
private const val STATE_WAITING_FOR_FRAGMENT_B = "state_waiting_b"
class FragmentA : Fragment() {
private var isWaitingForFragmentB: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState != null) {
isWaitingForFragmentB = savedInstanceState.getBoolean(STATE_WAITING_FOR_FRAGMENT_B)
}
}
override fun onResume() {
super.onResume()
if (isWaitingForFragmentB) {
isWaitingForFragmentB = false
// handle your view state here
}
}
override fun onSaveInstanceState(savedInstanceState: Bundle) {
super.onSaveInstanceState(savedInstanceState)
savedInstanceState.putBoolean(
STATE_WAITING_FOR_FRAGMENT_B,
isWaitingForFragmentB
)
}
private fun showFragmentB() {
isWaitingForFragmentB = true
// do fragment transaction here
}
}
I'm not good at grammar.
First fragment do not call resume function when returning.
You must create callback with interface.
A good approach should be passing some flag, on the second fragment, by activity intent and to capture it on the first Fragment on onResume()
If you need extra info, just let me know

In Android Navigation Architecture, how can I check if current Fragment is the last one?

I need to display custom AlertDialog, but only when there are no more fragments after calling NavController.navigateUp(). My current code does something similar, but there is a bug to it:
override fun onBackPressed() {
if (navController.navigateUp()) {
return
}
showQuitDialog()
}
This somewhat works, but if I cancel the AlertDialog and don't quit the app, navController already navigated up to NavGraph root, which is not the fragment in which I was when AlertDialog appeared. So, if I try to use any navigation action from that fragment, I get error:
java.lang.IllegalArgumentException: navigation destination
com.example.test/actionNavigateToNextFragment is unknown to this NavController
This could be resolved, if I had access to mBackStack field in NavController, but it's package private, and I can't use reflection in my current project. But if it was public, I would use it the following way:
override fun onBackPressed() {
// 2 because first one is NavGraph, second one is last fragment on the stack
if(navController.mBackStack.size > 2) {
navController.navigateUp()
return
}
showQuitDialog()
}
Is there a way to do this without reflection?
You can compare the ID of the start destination with the ID of the current destination. Something like:
override fun onBackPressed() = when {
navController.graph.startDestination == navController.currentDestination?.id -> showQuitDialog()
else -> super.onBackPressed()
}
Hope it helps.
Try this for any destination (you can find your destination id in the navigation graph):
private fun isDesiredDestination(): Boolean {
return with(navController) {
currentDestination == graph[R.id.yourDestinationId]
}
}
Comparing IDs of destination may not be the best solution because you may have multiple screens with the same ID but different arguments on the backstack.
Here's an alternative:
val isRootScreen = navController.previousBackStackEntry == null
if you want this is a button so to speak you can have this
yourbutton.setOnClickListener {
val currentFragment = navController.currentDestination?.id
when (navController.graph.startDestination == currentFragment) {
true -> { //Go here}
// Go to the app home
else -> onBackPressed()
}
}

Categories

Resources