Francesco Galgani
Francesco Galgani

Reputation: 6249

Codename One - errorMessages by the Validator shown with valid inputs (UPDATE)

--- UPDATED QUESTION WITH MORE DETAILS ---

I'm updating this question to add more details. I'm sorry to report multiple bugs in this question, but there are multiple issues that I experienced with the following code.

First of all, this is my (new) code to test the TextModeLayout, that is easy to full copy and test (I changed the code from the previous version of this question):

package ...;

import com.codename1.io.Log;
import static com.codename1.ui.CN.addNetworkErrorListener;
import static com.codename1.ui.CN.getCurrentForm;
import static com.codename1.ui.CN.updateNetworkThreadCount;
import com.codename1.ui.Container;
import com.codename1.ui.Dialog;
import com.codename1.ui.Form;
import com.codename1.ui.PickerComponent;
import com.codename1.ui.TextComponent;
import com.codename1.ui.Toolbar;
import com.codename1.ui.layouts.BoxLayout;
import com.codename1.ui.layouts.TextModeLayout;
import com.codename1.ui.plaf.UIManager;
import com.codename1.ui.util.Resources;
import com.codename1.ui.validation.Constraint;
import com.codename1.ui.validation.LengthConstraint;
import com.codename1.ui.validation.Validator;
import java.util.Date;
import java.util.Calendar;

public class MyApplication {

    private Form current;
    private Resources theme;

    public void init(Object context) {
        // use two network threads instead of one
        updateNetworkThreadCount(2);

        theme = UIManager.initFirstTheme("/theme");

        // Enable Toolbar on all Forms by default
        Toolbar.setGlobalToolbar(true);

        // Pro only feature
        Log.bindCrashProtection(true);

        addNetworkErrorListener(err -> {
            // prevent the event from propagating
            err.consume();
            if (err.getError() != null) {
                Log.e(err.getError());
            }
            Log.sendLogAsync();
            Dialog.show("Connection Error", "There was a networking error in the connection to " + err.getConnectionRequest().getUrl(), "OK", null);
        });
    }

    public void start() {
        if (current != null) {
            current.show();
            return;
        }

        MyForm test = new MyForm();
        test.show();

    }

    public void stop() {
        current = getCurrentForm();
        if (current instanceof Dialog) {
            ((Dialog) current).dispose();
            current = getCurrentForm();
        }
    }

    public void destroy() {
    }

}

class MyForm extends Form {

    public MyForm() {
        super("Anagrafica", BoxLayout.y());
    }

    /**
     * Creates and shows the Registry Form
     */
    @Override
    public void show() {

        // More info: https://www.codenameone.com/blog/pixel-perfect-text-input-part-2.html
        TextModeLayout textModeLayout = new TextModeLayout(4, 1);
        Container inputPersonData = new Container(textModeLayout);

        TextComponent name = new TextComponent().labelAndHint("Name").errorMessage("Insert your name");
        TextComponent surname = new TextComponent().labelAndHint("Surname").errorMessage("Insert your surname");
        PickerComponent gender = PickerComponent.createStrings("Male", "Female", "Other", "Not specified").label("Gender").errorMessage("Please choose an option");
        PickerComponent date = PickerComponent.createDate(null).label("Birthday").errorMessage("Are you at least 13 years old?");

        Validator validator = new Validator();
        validator.setShowErrorMessageForFocusedComponent(false);
        validator.addConstraint(name, new LengthConstraint(2));
        validator.addConstraint(surname, new LengthConstraint(2));
        validator.addConstraint(gender, new Constraint() {
            @Override
            public boolean isValid(Object value) {
                boolean res = false;
                if (value != null && value instanceof String) {
                    res = true;
                }
                return res;
            }

            @Override
            public String getDefaultFailMessage() {
                return "Please choose an option";
            }

        });
        validator.addConstraint(date, new Constraint() {
            @Override
            public boolean isValid(Object value) {
                boolean res = false;
                if (value != null && value instanceof Date) {
                    Calendar birthday = Calendar.getInstance();
                    birthday.setTime((Date) value);
                    Calendar calendar = Calendar.getInstance();
                    calendar.setTime(new Date());
                    calendar.add(Calendar.YEAR, -13);
                    if (birthday.before(calendar)) {
                        res = true;
                    }
                }
                return res;
            }

            @Override
            public String getDefaultFailMessage() {
                return "You must be at least 13 years old";
            }
        });

        inputPersonData.add(name);
        inputPersonData.add(surname);
        inputPersonData.add(gender);
        inputPersonData.add(date);

        add(inputPersonData);
        //setEditOnShow(name.getField());
        super.show();

        Log.p("Registry Form shown correctly");

    }

}

I generated the apk and the ipa and I tested them on real devices in my hand (not in a device farm). To better understand the behaviors, I recorded two videos.

Android 7: As you can see in the video Android.mp4, the error message of the name only (but not of the surname) is always shown, regardless I provided a valid input. Moreover, the app crashes when I tap the birthday picker with a java.lang.NullPointerException. The full log is here: android_date_picker_crash.txt

iPhoneX (iOS 11.2.2): As you can see in the video iPhone.mp4, the red crosses on the right of the fields are correctly shown only when the inputs are not valid (as I expected), but the errorMessages texts are always shown also with valid inputs. There is also a problem on selection the gender, because if I tap the gender and then press OK (without scrolling the options), the first option (that is "male") is not selected (in the video I tried two times).

So the same code produces a crash on Android but not in iOS. Moreover, the errorTexts are shown incorrectly on both OSes, but with different behaviors.

On iOS there is also a strange bug: if you block the screen and then unlock it (tried using the right side button of iPhone X, and then unlock using FaceID), the input fields duplicate, like in this screenshot: error.jpg. This bug can be easily replicated.

Thank you for the support.

--- OLD QUESTION ---

Please note the following screenshots (that refers to the same code): on Android the errorMessage of TextComponent name is shown with a valid input (while the errorMessage of TextComponent surname is not shown with a valid input, as expected); on iPhoneX the errorMessages of TextComponent name and TextComponent surname are shown with a valid input. Why? What's wrong?

Of course, the expected behavior is that no errorMessage is shown with a valid input.

Below the screenshots, I copy the full code of the Form.

Screenshot of a real Android 7 (in my hand): Android 7

Screenshot of a real iPhoneX iOS11 (in a device farm): iPhoneX

Full Code:

import com.codename1.io.Log;
import com.codename1.ui.Container;
import com.codename1.ui.Display;
import com.codename1.ui.Form;
import com.codename1.ui.PickerComponent;
import com.codename1.ui.TextComponent;
import com.codename1.ui.layouts.BoxLayout;
import com.codename1.ui.layouts.TextModeLayout;
import com.codename1.ui.validation.Constraint;
import com.codename1.ui.validation.LengthConstraint;
import com.codename1.ui.validation.Validator;
import static com.myproject.registration.Registrazione.getArrowBack;
import java.util.Date;

public class Registry extends Form {

    public Registry() {
        super("Anagrafica", BoxLayout.y());
    }

    /**
     * Creates and shows the Registry Form
     *
     * @param backForm the previous Form to show tapping the back arrow
     */
    public void show(Form backForm) {

        // Back command (linked to the left arrow in the toolbar and to back hardware button)
        setBackCommand(getToolbar().addCommandToLeftBar("", getArrowBack(), (e) -> {
            Log.p("Invoked setBackCommand to go to the previous form");
            Display.getInstance().callSerially(() -> {
                backForm.showBack();
                Log.p("Previous form shown correctly");
            });
        }
        ));

        // More info: https://www.codenameone.com/blog/pixel-perfect-text-input-part-2.html
        TextModeLayout textModeLayout = new TextModeLayout(4, 1);
        Container inputPersonData = new Container(textModeLayout);

        TextComponent name = new TextComponent().label("Nome").errorMessage("Inserisci il tuo nome");
        TextComponent surname = new TextComponent().label("Cognome").errorMessage("Inserisci il tuo cognome");
        PickerComponent gender = PickerComponent.createStrings("Maschio", "Femmina", "altro").label("Genere");
        pickerComponentSetUnselectedText(gender, "Seleziona genere");
        PickerComponent date = PickerComponent.createDate(new Date()).label("Data di nascita").errorMessage("Hai almeno 13 anni?");
        pickerComponentSetUnselectedText(date, "Seleziona data");

        Validator validator = new Validator();
        // to make better
        // Codename One - Validate only one input at a time
        // https://stackoverflow.com/questions/50249453/codename-one-validate-only-one-input-at-a-time
        validator.setShowErrorMessageForFocusedComponent(false);
        validator.addConstraint(name, new LengthConstraint(2));
        validator.addConstraint(surname, new LengthConstraint(2));
        validator.addConstraint(date, new Constraint() {
            @Override
            public boolean isValid(Object value) {
                boolean res = false;
                // to do
                // Codename One - addConstraint to PickerComponent date
                // https://stackoverflow.com/questions/50249148/codename-one-addconstraint-to-pickercomponent-date
                return res;
            }

            @Override
            public String getDefaultFailMessage() {
                return "You must be at least 13 years old";
            }
        });

        inputPersonData.add(name);
        inputPersonData.add(surname);
        inputPersonData.add(gender);
        inputPersonData.add(date);

        add(inputPersonData);
        setEditOnShow(name.getField());
        super.show();

        Log.p("Registry Form shown correctly");

    }

    /**
     * Set a custom text for an unselected PickerComponent placed in a
     * TextModeLayout
     *
     * @param picker
     * @param text
     */
    private void pickerComponentSetUnselectedText(PickerComponent picker, String text) {
        if (picker.isOnTopMode()) {
            picker.getPicker().setText(text);
        } else {
            picker.getPicker().setText("");
        }
        picker.getPicker().setUIID("TextHint");
        picker.getPicker().addActionListener(l -> {
            l.getComponent().setUIID("TextField");
        });
    }
}

Upvotes: 1

Views: 327

Answers (2)

Francesco Galgani
Francesco Galgani

Reputation: 6249

Thank you Shai. About the other issues, I found these solutions:

  1. Don't use the PickerComponent.createDate(null).label("Birthday");, because the "null" argument caused the reported Android crash (see the log reported in the question). If I remeber correctly, you used createDate(null) in a lesson of the Facebook clone: it's ok only if you don't use the Validator (this problem affects only Android, instead the code works fine on iOS).
  2. Don't use a class that extends Form inside the same Java file of the main class (as in the code at the top of this question): it causes that after pausing and restoring the app, all the Form content is duplicated (I don't know why).

There are still minor issues that don't affect functionality, such as the fact that the Validator doesn't disable the submit button after the user invalidate an already validated input.

Finally, this is a (enough) working code:

import com.codename1.io.Log;
import com.codename1.ui.Button;
import static com.codename1.ui.CN.addNetworkErrorListener;
import static com.codename1.ui.CN.getCurrentForm;
import static com.codename1.ui.CN.updateNetworkThreadCount;
import com.codename1.ui.Container;
import com.codename1.ui.Dialog;
import com.codename1.ui.Form;
import com.codename1.ui.PickerComponent;
import com.codename1.ui.TextComponent;
import com.codename1.ui.Toolbar;
import com.codename1.ui.layouts.BoxLayout;
import com.codename1.ui.layouts.TextModeLayout;
import com.codename1.ui.plaf.UIManager;
import com.codename1.ui.util.Resources;
import com.codename1.ui.validation.Constraint;
import com.codename1.ui.validation.LengthConstraint;
import com.codename1.ui.validation.Validator;
import java.util.Date;
import java.util.Calendar;

public class MainClass {

    private Form current;
    private Resources theme;

    public void init(Object context) {
        // use two network threads instead of one
        updateNetworkThreadCount(2);

        theme = UIManager.initFirstTheme("/theme");

        // Enable Toolbar on all Forms by default
        Toolbar.setGlobalToolbar(true);

        // Pro only feature
        Log.bindCrashProtection(true);

        addNetworkErrorListener(err -> {
            // prevent the event from propagating
            err.consume();
            if (err.getError() != null) {
                Log.e(err.getError());
            }
            Log.sendLogAsync();
            Dialog.show("Connection Error", "There was a networking error in the connection to " + err.getConnectionRequest().getUrl(), "OK", null);
        });
    }

    public void start() {
        if (current != null) {
            current.show();
            return;
        }

        // More info:
        // https://www.codenameone.com/blog/pixel-perfect-text-input-part-2.html
        // https://stackoverflow.com/questions/50250572/codename-one-errormessages-by-the-validator-shown-with-valid-inputs-update
        Form hi = new Form("Anagrafica", BoxLayout.y());
        TextModeLayout textModeLayout = new TextModeLayout(4, 1);
        Container inputPersonData = new Container(textModeLayout);

        TextComponent name = new TextComponent().labelAndHint("Name");
        TextComponent surname = new TextComponent().labelAndHint("Surname");
        PickerComponent gender = PickerComponent.createStrings("Male", "Female", "Other", "Not specified").label("Gender");
        gender.getPicker().setSelectedString("Not specified");
        PickerComponent date = PickerComponent.createDate(new Date()).label("Birthday");

        Validator validator = new Validator();
        validator.setShowErrorMessageForFocusedComponent(true);
        validator.addConstraint(name, new LengthConstraint(2, "Insert your name"));
        validator.addConstraint(surname, new LengthConstraint(2, "Insert your surname"));

        validator.addConstraint(date, new Constraint() {
            @Override
            public boolean isValid(Object value) {
                boolean res = false;
                if (value != null && value instanceof Date) {
                    Calendar birthday = Calendar.getInstance();
                    birthday.setTime((Date) value);
                    Calendar calendar = Calendar.getInstance();
                    calendar.setTime(new Date());
                    calendar.add(Calendar.YEAR, -13);
                    if (birthday.before(calendar)) {
                        res = true;
                    }
                }
                return res;
            }

            @Override
            public String getDefaultFailMessage() {
                return "Are you at least 13 years old?";
            }
        });

        inputPersonData.add(name);
        inputPersonData.add(surname);
        inputPersonData.add(gender);
        inputPersonData.add(date);

        hi.add(inputPersonData);

        Button button = new Button("Send");
        hi.add(button);
        validator.addSubmitButtons(button);

        hi.setEditOnShow(name.getField());
        hi.show();

    }

    public void stop() {
        current = getCurrentForm();
        if (current instanceof Dialog) {
            ((Dialog) current).dispose();
            current = getCurrentForm();
        }
    }

    public void destroy() {
    }

}

Upvotes: 1

Shai Almog
Shai Almog

Reputation: 52770

You shouldn't use this with a validator:

TextComponent name = new TextComponent().labelAndHint("Name").errorMessage("Insert your name");

As you are effectively hardcoding the error message into the component and it will be set to null. You should place the message in the validator instead:

validator.addConstraint(name, new LengthConstraint(2, "Insert your name"));

Upvotes: 1

Related Questions