Skip to content

Commit

Permalink
1.25 Warn user about losing unsaved changes
Browse files Browse the repository at this point in the history
  • Loading branch information
Beginning Android committed Aug 23, 2016
1 parent 6a82f79 commit bea7d90
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 2 deletions.
104 changes: 102 additions & 2 deletions app/src/main/java/com/example/android/pets/EditorActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
*/
package com.example.android.pets;

import android.app.AlertDialog;
import android.app.LoaderManager;
import android.content.ContentValues;
import android.content.CursorLoader;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.Loader;
import android.database.Cursor;
Expand All @@ -28,6 +30,7 @@
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
Expand Down Expand Up @@ -68,6 +71,21 @@ public class EditorActivity extends AppCompatActivity implements
*/
private int mGender = PetEntry.GENDER_UNKNOWN;

/** Boolean flag that keeps track of whether the pet has been edited (true) or not (false) */
private boolean mPetHasChanged = false;

/**
* OnTouchListener that listens for any user touches on a View, implying that they are modifying
* the view, and we change the mPetHasChanged boolean to true.
*/
private View.OnTouchListener mTouchListener = new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
mPetHasChanged = true;
return false;
}
};

This comment has been minimized.

Copy link
@n-abdelmaksoud

n-abdelmaksoud Nov 22, 2017

why " return false" not "true"???

This comment has been minimized.

Copy link
@megandelrosario

megandelrosario Nov 27, 2017

This comment has been minimized.

Copy link
@LuizPelegrini

LuizPelegrini Dec 9, 2018

After reading what those links say about it, I purposely change the return statement to return true. After making this change, I could not be able to click and edit any field.

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Expand Down Expand Up @@ -98,6 +116,14 @@ protected void onCreate(Bundle savedInstanceState) {
mWeightEditText = (EditText) findViewById(R.id.edit_pet_weight);
mGenderSpinner = (Spinner) findViewById(R.id.spinner_gender);

// Setup OnTouchListeners on all the input fields, so we can determine if the user
// has touched or modified them. This will let us know if there are unsaved changes
// or not, if the user tries to leave the editor without saving.
mNameEditText.setOnTouchListener(mTouchListener);

This comment has been minimized.

Copy link
@darwinyip

darwinyip Jul 23, 2018

I'm getting custom view has setOnTouchListener does not override performClick.

This comment has been minimized.

Copy link
@rami-alloush

rami-alloush Jul 24, 2018

It's not mandatory to overwrite it, it's just better to do so for accessibility compatibility.

This comment has been minimized.

Copy link
@xht418

xht418 Nov 15, 2018

I'm getting custom view has setOnTouchListener does not override performClick.

You can add "@SuppressLint("ClickableViewAccessibility")" to eliminate the "accessibility" hint.

mBreedEditText.setOnTouchListener(mTouchListener);
mWeightEditText.setOnTouchListener(mTouchListener);
mGenderSpinner.setOnTouchListener(mTouchListener);

setupSpinner();
}

Expand Down Expand Up @@ -235,13 +261,58 @@ public boolean onOptionsItemSelected(MenuItem item) {
return true;
// Respond to a click on the "Up" arrow button in the app bar
case android.R.id.home:
// Navigate back to parent activity (CatalogActivity)
NavUtils.navigateUpFromSameTask(this);
// If the pet hasn't changed, continue with navigating up to parent activity
// which is the {@link CatalogActivity}.
if (!mPetHasChanged) {
NavUtils.navigateUpFromSameTask(EditorActivity.this);
return true;
}

// Otherwise if there are unsaved changes, setup a dialog to warn the user.
// Create a click listener to handle the user confirming that
// changes should be discarded.
DialogInterface.OnClickListener discardButtonClickListener =
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
// User clicked "Discard" button, navigate to parent activity.
NavUtils.navigateUpFromSameTask(EditorActivity.this);
}
};

// Show a dialog that notifies the user they have unsaved changes
showUnsavedChangesDialog(discardButtonClickListener);
return true;
}
return super.onOptionsItemSelected(item);
}

/**
* This method is called when the back button is pressed.
*/
@Override
public void onBackPressed() {
// If the pet hasn't changed, continue with handling back button press
if (!mPetHasChanged) {
super.onBackPressed();
return;
}

// Otherwise if there are unsaved changes, setup a dialog to warn the user.
// Create a click listener to handle the user confirming that changes should be discarded.
DialogInterface.OnClickListener discardButtonClickListener =
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
// User clicked "Discard" button, close the current activity.
finish();
}
};

// Show dialog that there are unsaved changes
showUnsavedChangesDialog(discardButtonClickListener);
}

@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
// Since the editor shows all pet attributes, define a projection that contains
Expand Down Expand Up @@ -314,4 +385,33 @@ public void onLoaderReset(Loader<Cursor> loader) {
mWeightEditText.setText("");
mGenderSpinner.setSelection(0); // Select "Unknown" gender
}

/**
* Show a dialog that warns the user there are unsaved changes that will be lost
* if they continue leaving the editor.
*
* @param discardButtonClickListener is the click listener for what to do when
* the user confirms they want to discard their changes
*/
private void showUnsavedChangesDialog(
DialogInterface.OnClickListener discardButtonClickListener) {
// Create an AlertDialog.Builder and set the message, and click listeners
// for the postivie and negative buttons on the dialog.
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(R.string.unsaved_changes_dialog_msg);
builder.setPositiveButton(R.string.discard, discardButtonClickListener);
builder.setNegativeButton(R.string.keep_editing, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
// User clicked the "Keep editing" button, so dismiss the dialog
// and continue editing the pet.
if (dialog != null) {
dialog.dismiss();
}
}

This comment has been minimized.

Copy link
@n-abdelmaksoud

n-abdelmaksoud Nov 22, 2017

what is the use of checking dialog if it equals to null before dismissing?
in all cases, the user cannot press NegativeButton unless the dialog is displayed on the screen!!

This comment has been minimized.

Copy link
@n-abdelmaksoud

n-abdelmaksoud Dec 5, 2017

And how it could be null while its negative button is available to be clicked. Is there a way that a dialog is shown and it equals to null in the same time!!

This comment has been minimized.

Copy link
@pkhruasu-ui

pkhruasu-ui Dec 23, 2017

From my limited knowledge in web dev. Sometimes unexpected things could happen like alert popup, user click outside the modal(prematurely closing it), api error, etc. It is a good measure to check just to be sure that there is something to close before you actually close it. A library developer can facilitate checking it for you but inconsistencies can happen among libraries.

This comment has been minimized.

Copy link
@n-abdelmaksoud

n-abdelmaksoud Dec 25, 2017

Thanks, that makes sense.

This comment has been minimized.

Copy link
@danishbhatia58

danishbhatia58 Jul 13, 2018

THANKS.

This comment has been minimized.

Copy link
@xht418

xht418 Nov 15, 2018

From my limited knowledge in web dev. Sometimes unexpected things could happen like alert popup, user click outside the modal(prematurely closing it), api error, etc. It is a good measure to check just to be sure that there is something to close before you actually close it. A library developer can facilitate checking it for you but inconsistencies can happen among libraries.

For outside clicking you can handle it by this:

builder.setCancelable(false);

});

// Create and show the AlertDialog
AlertDialog alertDialog = builder.create();
alertDialog.show();
}
}
9 changes: 9 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@
<!-- Toast message in editor when current pet has failed to be updated [CHAR LIMIT=NONE] -->
<string name="editor_update_pet_failed">Error with updating pet</string>

<!-- Dialog message when user is leaving editor but hasn't saved changes [CHAR LIMIT=NONE] -->
<string name="unsaved_changes_dialog_msg">Discard your changes and quit editing?</string>

<!-- Dialog button text for the option to discard a user's changes [CHAR LIMIT=20] -->
<string name="discard">Discard</string>

<!-- Dialog button text for the option to keep editing the current pet [CHAR LIMIT=20] -->
<string name="keep_editing">Keep Editing</string>

<!-- Label for overview category of attributes in the editor [CHAR LIMIT=30] -->
<string name="category_overview">Overview</string>

Expand Down

28 comments on commit bea7d90

@hpols
Copy link

@hpols hpols commented on bea7d90 Mar 22, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oddly enough, this code does nothing when I return to the catalog activity without saving. It happily closes the editor activity (without saving).

I am working with an emulator (Api22) – might there be something that the emulator ignores or is not supported by it?

@xingped
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@atschpe You're probably missing the final return true; in the android.R.id.home switch case in onOptionsItemSelected(...)

@my-jabin
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we just edit the name and quit(back or up) without saving, It won't show the alertDialog.
OnTouchListener is not appropriate for listening to the changes. Since we didn't touch any other Views, the variable mPetHasChanged won't be True, but we still can edit the name.

Any suggestions?

@hpols
Copy link

@hpols hpols commented on bea7d90 Apr 21, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xingped I've doublechecked and return true; is set on each switch case. So sadly that is not what is causing it.

@dmetree
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To fix the mistake change "int id" => "int which" in:

builder.setNegativeButton(R.string.keep_editing, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if (dialog != null){ dialog.dismiss(); } }

@namclu
Copy link

@namclu namclu commented on bea7d90 Jun 3, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For myself, I found it simpler to set up showUnsavedChangesDialog() with no argument and then set the .setPositiveButton() as below:

private void showUnsavedChangesDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setMessage(R.string.unsaved_changes_dialog_msg)
                .setPositiveButton(R.string.discard, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        finish();
                    }
                })

This way, when I'm calling showUnsavedChangesDialog(), I simply call the alert dialog w/o having to define the onClick() method in both onBackPressed() and onOptionsItemSelected().

case android.R.id.home:
                if (!mPetHasChanged) {
                    NavUtils.navigateUpFromSameTask(this);
                    return true;
                }
                showUnsavedChangesDialog();
                return true;

Seems to behave correctly. Am I missing something by not implementing code as written by Udacity?

@namclu
Copy link

@namclu namclu commented on bea7d90 Jun 3, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, not sure why we have to do if (dialog != null) here

.setNegativeButton(R.string.keep_editing, new DialogInterface.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialog, int id) {
                 // User clicked the "Keep editing" button, so dismiss the dialog
                 // and continue editing the pet.
                 if (dialog != null) {
                     dialog.dismiss();
                 }
             }

Isn't the act of clicking on the "Keep editing" button means that we're calling the .setNegativeButton()? So we can just do:

.setNegativeButton(R.string.keep_editing, new DialogInterface.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialog, int which) {
                        // User clicked the "Keep editing" button, so dismiss the dialog
                        // and continue editing the pet.
                        dialog.dismiss();
                    }

@linucksrox
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding navigation, there's a difference between calling finish() and NavUtils.navigateUpFromSameTask(EditorActivity.this) See the training documentation https://developer.android.com/training/design-navigation/index.html
You don't really see it in such a small app with only two activities, but when you start designing more complicated navigation structures, or make it possible to get to a certain activity from outside the app, the behavior of these two methods will be different. Either way is fine for this app.

I don't know why it's necessary to check if (dialog != null) When I get a chance I'll review the video explaining that one to see if they give any hints about why they added that check.

@namclu
Copy link

@namclu namclu commented on bea7d90 Jun 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@linucksrox
For if (dialog != null), one explanation I was told was that it was good practice to check if the dialog window was present or not before dismissing it.

I'm not entirely sure I understand that explanation. Maybe in this case, the dialog is pretty cut and dry but in other cases, there may be an instance where a dialog is dismissed and you're responding to it via another dialog or activity...

@benji0988
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @my-jabin ,

I think the logic is if the user click/touch on a View then the onTouchListener will work.
Since when the first time we go into EditorActivity the cursor is directly on mNameEditText, it doesn't count as if we click/touch on mNameEditText. So when we pressed on back button or Up button then the Dialog won't pop up.

Try to click/touch on the mNameEditText first then edit the pet name. it'll pop up the Dialog when you pressed on back / up button.

CMIIW

@Arthzil
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't overthink it, swap
if (!mPetHasChanged)
to
if (!mPetHasChanged && TextUtils.isEmpty(mNameEditText.getText().toString()))

in both cases.

Will work even if user doesn't trigger onTouchListener by staying in Name edit box. The only problem is when user actually uses Tab on attached physical keyboard to move between fields... but if you want to cover such cases, just add remaining fields in same manner.

@alexmiretskiy
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To fix it add in root LinearLayout:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:descendantFocusability="beforeDescendants"
    android:focusableInTouchMode="true" >

@SachinGarg10
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After edit or touch any field and then rotate the device, the dialog box doesn't pop up.

@alexmiretskiy
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no problem

@Override
  protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putBoolean("petHasChanged", mPetHasChanged);
  }

  @Override
  protected void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    mPetHasChanged = savedInstanceState.getBoolean("petHasChanged");
  }

@n-abdelmaksoud
Copy link

@n-abdelmaksoud n-abdelmaksoud commented on bea7d90 Nov 22, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should add a line of code before dismissing the dialog:

  mPetsHasChanges=false;
  dialog.dismiss();

to reset its value so if the user presses back button again but this time without changes, the EditorActivity finishes without displaying the dialog again.

@jakubosiak
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nerrydanna You are smart bunny πŸ‘―

@Sakshamgupta20
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nerrydanna no you cannot do that because if you click back button before clickiing at the save button then your data will not be saved.

@bhavya-arora
Copy link

@bhavya-arora bhavya-arora commented on bea7d90 Jan 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if User Touch any Edit Field, so 'OnTouchListener' get Triggered and 'mPetHasChanged' value will change to 'true' and then if user Rotate the screen, so Editor activity will again created with Default value of 'mPetHasChanged' value to 'false', so it's not good.

SOLUTION: In 'EditorActivity.java'

@Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putBoolean("mPetHasChanged", mPetHasChanged);
    }

Now in onCreate():

     if(savedInstanceState != null){
               mPetHasChanged = savedInstanceState.getBoolean("mPetHasChanged");
            }

@Babadzhanov
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why they are spitting so much code and changes without explanation i don't understand...

@iammohdzaki
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a problem in this one ,when i press back button it did not display the dialog box and there is no back button showing at the top of the activity.Help!

@trigal2012
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found a test case where the dialog box was not appearing when the user added details for a new pet but then clicking the back button before saving the details.

Steps to Reproduce:

  1. run the code on an emulator
  2. click on the FAB to add a new Pet (using your computer keyboard mouse)
  • the add Pet screen opens with Focus on the Name field (* Note - the soft keyboard does not appear in the emulator)
  1. start typing - whatever you type should appear in the name field
  2. click the back button - Here the dialog box is not appearing
    I believe this is because no touch event has been detected because you never actually clicked anywhere within the emulator with your mouse.
  • if, at step 3 above, you use your mouse and click into the Name field, two things happen, the touch is detected and the soft keyboard appears.

How I resolved it:

To resolve this test case I added an onKeylistener like so to the EditorActivity.java, just below the View.OnTouchListener.
private View.OnKeyListener mKeyListener = new View.OnKeyListener(){ @Override public boolean onKey(View v, int keyCode, KeyEvent event){ mPetHasChanged = true; return false; } };

Then set the onkeylistners in line with the setOnTouchListeners:
mNameEditText.setOnTouchListener(mTouchListener); mNameEditText.setOnKeyListener(mKeyListener); mBreedEditText.setOnTouchListener(mTouchListener); mBreedEditText.setOnKeyListener(mKeyListener); mWeightEditText.setOnTouchListener(mTouchListener); mWeightEditText.setOnKeyListener(mKeyListener); mGenderSpinner.setOnTouchListener(mTouchListener);

While this approach seems to work ok, Is this a good way to deal with the issue?

@EvenTheSheepSays
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a bug, but it would be really nice to set the boolean variable back to "FALSE" when we successfully update the pet.

@xht418
Copy link

@xht418 xht418 commented on bea7d90 Nov 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After edit or touch any field and then rotate the device, the dialog box doesn't pop up.

Try this when you're setting the AlertDialog:

builder.setCancelable(false);

This will prevent from clicking outside to close the dialog window.

@xht418
Copy link

@xht418 xht418 commented on bea7d90 Nov 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a bug, but it would be really nice to set the boolean variable back to "FALSE" when we successfully update the pet.

No need. When you successfully update the pet means that you have quit the Editor Activity, and next time you enter Editor Activity everything will be reset.

@SayantanBanerjee16
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Home Button going from Editor activity to Catalog activity, we are writing:
NavUtils.navigateUpFromSameTask(EditorActivity.this);

Why not we are writing
Intent intent = new Intent(EditorActivity.this, CatalogActivity.class);
startActivity(intent);

What's the difference

@xht418
Copy link

@xht418 xht418 commented on bea7d90 Jun 24, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SayantanBanerjee16 "Up" button which appears in the top left corner brings you to the upper level Activity (by specifying the "parent_activity" attribute in AndroidManifest file), whereas the universal "back" key brings you to the previous screen which can be the screen of another app.

Intent is always creating a new instance of the target Activity class, whereas "up" can resume the state of parent Activity which follows the Activity life cycle rule. Further discusstion: https://stackoverflow.com/questions/40629176/is-there-any-difference-between-using-the-up-button-and-using-intent-to-go-up-on.

By the way, "up" button will call "onCreate()", "onStart()" and "onResume()", which give you the chance to run some code on them (latter two). If you don't want the "onCretae()" get called you can set "launchMode" to "singleTop" in Activity attribute in AndroidManifest file.

Good luck.

@monaASamra
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The upper left back arrow, does not wait me to choose, Edit or Discard, it closes the activity it self.

@Hantar2000
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (cursor.moveToFirst()) {
//Toast.makeText(this, cursorPositoin,Toast.LENGTH_LONG).show();
// Find the columns of pet attributes that we're interested in
int nameColumnIndex = cursor.getColumnIndex(PetContract.PetEntry.COLUMN_NAME);
int breedColumnIndex = cursor.getColumnIndex(PetContract.PetEntry.COLUMN_BREED);
int genderColumnIndex = cursor.getColumnIndex(PetContract.PetEntry.COLUMN_GENDER);
int weightColumnIndex = cursor.getColumnIndex(PetContract.PetEntry.COLUMN_WEIGHT);
...etc
this method always return the first table record instead of readable the record according to pet id. Any idea how to fix it plz ?

Please sign in to comment.