1

I have the following issue.I'm having an activity,part of an animal shelter app, where an user has to enter or edit pet-related data like weight,name and breed.What I want to achieve is to show a dialog asking if the user wants to keep editing or leave the activity,depending on whether he actually changed any of the text in the EditText views. To do so I have created a boolean variable ,which should turn to true if the text was edited /trigger the dialog/ or remain false/do nothing/ if the user didn't edit anything. I have attached TextWatcher to my EditText fields and tried to change the variable to true by doing that in onTextChangedor in beforeTextChanged.I tried comparing the hash or string values of the EditText fields to the CharSequence charSequence in the onTextChanged method but it only works for one of the EditText fields/meaning it triggers the dialog when the user changed the text/.Whenever I try to apply similar logic to the rest of the EditText fields as well the functionality breaks and the boolean variable stays "true" no matter what/meaning user sees dialog no matter if they changed the text or not/. I tried various comparisson,if-logic,switch statement in an inner class and nothing seems to work.Kindly see code below.Thank you.

/**
 * Allows user to create a new pet or edit an existing one.
 */
public class EditorActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor>, TextWatcher {


    /**
     * EditText field to enter the pet's name
     */
    private EditText mNameEditText;

    /**
     * EditText field to enter the pet's breed
     */
    private EditText mBreedEditText;

    /**
     * EditText field to enter the pet's weight
     */
    private EditText mWeightEditText;

    /**
     * EditText field to enter the pet's gender
     */
    private Spinner mGenderSpinner;

    /**
     * Gender of the pet. The possible values are:
     * 0 for unknown gender, 1 for male, 2 for female.
     */
    public static int mGender;

    public static String mPetName;

    public static String mPetBreed;

    public static String mPetWeight;

    private static ArrayAdapter mGenderSpinnerAdapter;

    private static Uri mSinglePetUri;

    private static ContentValues mContentValues;
    // we will show warning dialog to the user,if the below variable is true
    private boolean mPetHasChanged;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_editor);
        // checks if we are about to edit the information about an existing pet or add
        // a new pet record , adjusts the activity title accordingly and initializes/
        // activates Loader only if we are updating an existing pet
        mSinglePetUri = getIntent().getData();
        if (mSinglePetUri != null) {
            setTitle(R.string.edit_pet_activity_title);
            getSupportLoaderManager().initLoader(0, null, this);
        } else {
            setTitle(getString(R.string.add_a_pet_activity_title));

        }
        // Find all relevant views that we will need to read user input from
        mNameEditText = findViewById(R.id.edit_pet_name);
        mBreedEditText = findViewById(R.id.edit_pet_breed);
        mWeightEditText = findViewById(R.id.edit_pet_weight);
        mGenderSpinner = findViewById(R.id.spinner_gender);
        setupSpinner();
        // watch for text changes
        mNameEditText.addTextChangedListener(this);
        mBreedEditText.addTextChangedListener(this);
        mWeightEditText.addTextChangedListener(this);

}


@Override
    public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {

    }



  @Override
    public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {


        int nameTextHashCode = mNameEditText.getText().hashCode();
        int breedTextHashCode = mBreedEditText.getText().hashCode();
        int weightTextHashCode = mWeightEditText.getText().hashCode();
        boolean nameChanged = nameTextHashCode == charSequence.hashCode();
        boolean breedChanged = breedTextHashCode == charSequence.hashCode();
        boolean weightChanged = weightTextHashCode == charSequence.hashCode();
        //this works-mPetHasChanged properly changes value
        mPetHasChanged = nameChanged ;
        //this doesn't work - the value is always true even when user didn't change a thing
        mPetHasChanged = nameChanged||breedChanged||weightChanged;
    }

    @Override
    public void afterTextChanged(Editable editable) {

    }
}

Then in another method the boolean value is tested in order to show or not the dialog

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;

XML of the layout:

<?xml version="1.0" encoding="utf-8"?>

<!-- Copyright (C) 2016 The Android Open Source Project
     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at
          http://www.apache.org/licenses/LICENSE-2.0
     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an "AS IS" BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     See the License for the specific language governing permissions and
     limitations under the License.
-->
<!-- Layout for the editor -->
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="@dimen/activity_margin"
    tools:context=".EditorActivity">

    <!-- Overview category -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <!-- Label -->
        <TextView
            android:text="@string/category_overview"
            style="@style/CategoryStyle" />

        <!-- Input fields -->
        <LinearLayout
            android:layout_height="wrap_content"
            android:layout_width="0dp"
            android:layout_weight="2"
            android:paddingLeft="4dp"
            android:orientation="vertical">

            <!-- Name field -->
            <EditText
                android:id="@+id/edit_pet_name"
                android:hint="@string/hint_pet_name"
                android:inputType="textCapWords"
                style="@style/EditorFieldStyle" />

            <!-- Breed field -->
            <EditText
                android:id="@+id/edit_pet_breed"
                android:hint="@string/hint_pet_breed"
                android:inputType="textCapWords"
                style="@style/EditorFieldStyle" />
        </LinearLayout>
    </LinearLayout>

    <!-- Gender category -->
    <LinearLayout
        android:id="@+id/container_gender"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <!-- Label -->
        <TextView
            android:text="@string/category_gender"
            style="@style/CategoryStyle" />

        <!-- Input field -->
        <LinearLayout
            android:layout_height="wrap_content"
            android:layout_width="0dp"
            android:layout_weight="2"
            android:orientation="vertical">

            <!-- Gender drop-down spinner -->
            <Spinner
                android:id="@+id/spinner_gender"
                android:layout_height="48dp"
                android:layout_width="wrap_content"
                android:paddingRight="16dp"
                android:spinnerMode="dropdown"/>
        </LinearLayout>
    </LinearLayout>

    <!-- Measurement category -->
    <LinearLayout
        android:id="@+id/container_measurement"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <!-- Label -->
        <TextView
            android:text="@string/category_measurement"
            style="@style/CategoryStyle" />

        <!-- Input fields -->
        <RelativeLayout
            android:layout_height="wrap_content"
            android:layout_width="0dp"
            android:layout_weight="2"
            android:paddingLeft="4dp">

            <!-- Weight field -->
            <EditText
                android:id="@+id/edit_pet_weight"
                android:hint="@string/hint_pet_weight"
                android:inputType="number"
                style="@style/EditorFieldStyle" />

            <!-- Units for weight (kg) -->
            <TextView
                android:id="@+id/label_weight_units"
                android:text="@string/unit_pet_weight"
                style="@style/EditorUnitsStyle"/>
        </RelativeLayout>
    </LinearLayout>
</LinearLayout>
  • As you probably noticed there is no `View` reference getting back on `TextWatcher`'s callbacks. It's probably because it doesn't support what you want to achieve. I had a similar solution and I wanted to have View's reference. I ended up creating multiple `TextWatcher`. – Shynline May 28 '20 at 18:46
  • I think you can accomplish it without using `TextWatcher` and don't use `hashCode`. You can keep and old value and match against a new value. – OhhhThatVarun May 28 '20 at 18:52
  • No, Multiple `TextWatcher` are not needed, what you actually need is to crosscheck whether the current `EditText` is empty or not. I also use same `TextWatcher` for 4 `EditText`and I had to use empty check because it returns true even if the `EditText` is empty, it's a [common issue](https://stackoverflow.com/a/38566929/8244632). – Lalit Fauzdar May 28 '20 at 18:54
  • try like this -https://stackoverflow.com/questions/4283062/textwatcher-for-more-than-one-edittext/4283532 – Thirumalai May 28 '20 at 18:55
  • @OhhhThatVarun -could you explain further? I'm not quite getting your point. – Crimson Sun May 31 '20 at 14:08
  • @CrimsonSun why are you not using a `Model` class for your pet? – OhhhThatVarun May 31 '20 at 14:11

2 Answers2

0

When all that you need is mPetChanged, why are you even matching the HashCodes?

See this:

@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
    mPetHasChanged = !mNameEditText.getText().toString().trim().equals("") || !mBreedEditText.getText().toString().trim().equals("") || !mWeightEditText.getText().toString().trim().equals("");
}

You can use If for each EditText to check if they're empty but this seems easy. You can also use isBlank() but that comes in StringUtils() with Apache library, you can use String.isEmpty() which is available in Java but it returns true if it the EditText contains only spaces.

Update: you can compare start and before in onTextChanged(CharSequence charSequence, int start, int before, int count), if the values of start and before are not same that means text has changed. As provided below:

@Override
public void onTextChanged(CharSequence charSequence, int start, int before, int i2) {
    mPetHasChanged = start!= before;
}
Lalit Fauzdar
  • 5,953
  • 2
  • 26
  • 50
  • Thank you for the input Lalit.However,this doesn't work for me as I need to check if the user has changed existing text and not if the EditText is empty. – Crimson Sun May 31 '20 at 14:05
  • But that you can do by storing values of editTexts in strings and then comparing them inside `onTextChanged` if any of that changed or you can compare `start` and `before` in `onTextChanged(CharSequence charSequence, int start, int before, int count)`, if the values of strings and current text or values of start and before are not same that means text has changed. You've to go for this way because `onTextChanged()` gets called even when `EditText` is empty or is focused without changing a character. This is a pretty common issue or feature I don't know. – Lalit Fauzdar May 31 '20 at 14:42
  • And what you're doing in the code posted in the question is you're checking which EditText has called the onTextChanged() and then you're putting a boolean based on that, matching hashcodes gives the current editText, it doesn't guarantee text change. – Lalit Fauzdar May 31 '20 at 14:43
  • thanks so much for your help - the comparisson between start and before did work like a charm.PS.I'm quite new to android development and still figuring it out - here's the single line of code that was enough.. @Override public void onTextChanged(CharSequence charSequence, int start, int before, int i2) { mPetHasChanged = start!=before; } How can I upvote you despite the fact tha first suggested answer by you didn't work? – Crimson Sun May 31 '20 at 15:14
  • @CrimsonSun you can leave the upvote thing, I'm glad it helped. Although, I've updated the answer in case it helps someone else someday. – Lalit Fauzdar May 31 '20 at 17:05
  • Im sorry to bother further but now another issue occured,which I noticed after more testing - whenever I update the EditText with just one character -e.g. pet weight became 5kg and was 4 before, the logic breaks.It appears when I remove the old value to input the new one ,the onTextChanged method gets called and then when I input the new value,it doesn't count it.I tried to implement the correct logic by comparing if the string is empty ,using different logical operators/statements but to no avail. – Crimson Sun May 31 '20 at 18:08
  • I just checked your full code to clearly understand what you want. And I understood that you have an activity which opens with blank `EditTexts` and if user is leaving the activity without saving then you show the `Dialog` as I didn't find any setText for any EditText which means the values of EditText solely comes from user input which means if he/she leaves the activity without saving, why don't you just check if any of the EditTexts contain any value and if yes, then that means user is leaving unsaved changes. This way you won't have to mess with anything else. Have I missed something? – Lalit Fauzdar May 31 '20 at 18:18
  • And if you're setting up the values somewhere in the `EditTexts` then just compare those values with editText.getText() and match them in the `AfterTextChanged()`, if they don't match means values are changed. – Lalit Fauzdar May 31 '20 at 18:20
  • The EditText is being populated by the user indeed but also there is another Activity ,which has a ListView , which contains a list of pets.When the user clicks a pet the EditText's are being fired up and they're already pre-populated with the pet's data ,which is coming from a Cursor.I tried comparing the data from the Cursor with the EditText's input in the afterTextChanged method but it didn't work: – Crimson Sun Jun 01 '20 at 15:28
0

It took me quite some time in trial and error but I managed to make the code work in correct way. @LalitFauzdar - thank you very much for helping me find out a part of the EditText - related solution.:

@Override
public void onTextChanged(CharSequence charSequence, int start, int before, int i2) {
    mPetHasChanged = start!= before;
}

The other part turned out to be listening to delete key strokes when the user modifies the data. Then I also had to notify the user of pending changes, if he modified his selection in the spinner,showing the pet's gender in my activity. https://stackoverflow.com/a/24399683/10632237 this answer is the one that gave me idea how to listen for "real" changes, which the user made to the spinner. Here are the parts of the code,related to the initial question:

 /**
     * Allows user to create a new pet or edit an existing one.
     */
    public class EditorActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor>, TextWatcher, View.OnKeyListener, View.OnTouchListener {


        /**
         * EditText field to enter the pet's name
         */
        private EditText mNameEditText;

        /**
         * EditText field to enter the pet's breed
         */
        private EditText mBreedEditText;

        /**
         * EditText field to enter the pet's weight
         */
        private EditText mWeightEditText;

        /**
         * EditText field to enter the pet's gender
         */
        private Spinner mGenderSpinner;

        /**
         * Gender of the pet. The possible values are:
         * 0 for unknown gender, 1 for male, 2 for female.
         */
        public static int mGender;

        public static String mPetName;

        public static String mPetBreed;

        public static String mPetWeight;

        private static ArrayAdapter mGenderSpinnerAdapter;

        private static Uri mSinglePetUri;
        /**we will show warning dialog to the user,if the below variable is true*/
        private boolean mPetHasChanged;
        /**This flag turns to true if the spinner was actually touched by the user */
        private boolean spinnerActivated;


        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_editor);
            // checks if we are about to edit the information about an existing pet or add
            // a new pet record , adjusts the activity title accordingly and initializes/
            // activates Loader only if we are updating an existing pet
            mSinglePetUri = getIntent().getData();
            if (mSinglePetUri != null) {
                setTitle(R.string.edit_pet_activity_title);
                getSupportLoaderManager().initLoader(0, null, this);
            } else {
                setTitle(getString(R.string.add_a_pet_activity_title));

            }
            // Find all relevant views that we will need to read user input from
            mNameEditText = findViewById(R.id.edit_pet_name);
            mBreedEditText = findViewById(R.id.edit_pet_breed);
            mWeightEditText = findViewById(R.id.edit_pet_weight);
            mGenderSpinner = findViewById(R.id.spinner_gender);
            setupSpinner();
            mNameEditText.addTextChangedListener(this);
            mBreedEditText.addTextChangedListener(this);
            mWeightEditText.addTextChangedListener(this);
            mNameEditText.setOnKeyListener(this);
            mBreedEditText.setOnKeyListener(this);
            mWeightEditText.setOnKeyListener(this);
            mGenderSpinner.setOnTouchListener(this);



        }

/**
     * Setup the dropdown spinner that allows the user to select the gender of the pet.
     */
    private void setupSpinner() {
        // Create adapter for spinner. The list options are from the String array it will use
        // the spinner will use the default layout
        mGenderSpinnerAdapter = ArrayAdapter.createFromResource(this,
                R.array.array_gender_options, android.R.layout.simple_spinner_item);

        // Specify dropdown layout style - simple list view with 1 item per line
        mGenderSpinnerAdapter.setDropDownViewResource(android.R.layout.simple_dropdown_item_1line);

        // Apply the adapter to the spinner
        mGenderSpinner.setAdapter(mGenderSpinnerAdapter);

        // attach listener to the spinner to handle user selection from the pet's gender list.
        mGenderSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {

            @Override
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {


                String selection = (String) parent.getItemAtPosition(position);

                if (selection.equals(getString(R.string.gender_male))) {
                    mGender = PetsEntry.PET_GENDER_MALE;



                } else if (selection.equals(getString(R.string.gender_female))) {
                    mGender = PetsEntry.PET_GENDER_FEMALE;


                } else {
                    mGender = PetsEntry.PET_GENDER_UNKNOWN;
                }
                // if the Spinner was not actually touched by the user, the 
                //spinnerActivated flag is
                //false and the method is exited earlier as there was not actual selection 
                 //change,
                //made by the user.This manages the behaviour of onItemSelected ,where the 
               //  method is
                //called twice - once when the spinner is initialized and once again if 
                // the user changes
                //selection.
                if (!spinnerActivated){
                    return;
                }
                mPetHasChanged=true;


            }


            // Because onItemSelectedListener is an interface, onNothingSelected must be 
            // defined
            @Override
            public void onNothingSelected(AdapterView<?> parent) {
                mGender = PetsEntry.PET_GENDER_UNKNOWN;
            }
        });
    }
//more code between,unrelated to topic

    @Override
    public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {

    }
    /**The logic in this method gets triggered if the user changes existing pet's data by 
      using
     * any key except the delete one*/
    @Override
    public void onTextChanged(CharSequence charSequence, int start, int before, int count) {

        if (start != before){
            mPetHasChanged=true;
        }

    }

    @Override
    public void afterTextChanged(Editable editable) {


    }
   /** The logic in this method gets triggered if the user presses the delete button,
    *  while editing  pet's data, so we won't miss any type of editing action*/

    @Override
    public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
        if (keyCode== KeyEvent.KEYCODE_DEL){
            mPetHasChanged=true;
        }

        return false;
    }

   /**This method checks if the spinner was actually touched by the user and turns the
    * flag variable to true and based on the flag's value we trigger the further logic in
    * {#onItemSelected}*/
    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        spinnerActivated=true;
        return false;
    }
}