Fork us on GitHub

TIP: Activation UI and the Builder Pattern

Using the builder pattern to create a fluid API
TIP: Activation UI and the Builder Pattern

TIP: Activation UI and the Builder Pattern

I wrote two posts about the SMS activation process. In the first I discussed using the Twilio API via REST and in the second I discussed the native interfaces for SMS interception we can use in Android. Now it’s time to put this all together and create a single API that’s fluid. It should include the full UI process but be flexible enough to let you design your own experience.

The Builder

I wanted to create a UI experience that’s pretty typical and standard while hiding a lot of nuance. My first intuition was to just derive Form and build the UI. But that’s not a good approach.

When you derive a component you expose the entire API of the base class onward, that creates a confusing UI API so I decided to expose a builder API which looks something like this:

TwilioSMS smsAPI = TwilioSMS.create(accountSID, authToken, fromPhone);
ActivationForm.create("Signup").
        show(s -> Log.p(s), smsAPI);

There are several things going on here. I wrapped the code from the SMS rest calls in an API called TwilioSMS. This allows me to hide the details of sending an SMS from the ActivationForm UI class. You will notice I create an instance of this class using the create method which accepts the title of the form. I then have a show method which calls us back with the phone number when activation succeeds…​

Since this is a builder pattern you can customize all sorts of things:

ActivationForm.create("Signup").
        codeDigits(5). // number of digits in the activation code sent via SMS
        enterNumberLabel(string). // text of the label above the number input
        includeFab(true). // true if a fab should be shown, by default a fab button will appear in Android only
        includeTitleBarNext(true). // true if a next arrow should appear in the title, by default this would appear in non-Android platforms
        show(s -> Log.p(s), smsAPI);

The neat thing is that you can’t do all sorts of things you aren’t supposed to do as the ActivationForm class derives from Object and has a private constructor.

The UI for the activation form shows a form on top of the current form with a number entry UI.

The SMS activation UI/UX
Figure 1. The SMS activation UI/UX
When clicking on the country code you are prompted with a list of countries
Figure 2. When clicking on the country code you are prompted with a list of countries

Obtaining the list of Countries

I got the list of flags and countries from https://mledoze.github.io/countries/

I converted the flags SVG’s to PNG’s and added them all to the flags.res file. This is more efficient than opening multiple pngs from the system. I considered using the SVG files with our transcoder but decided against it due to the immaturity and possible performance issues of the tool. Flags are sensitive things and a bug in the transcoder might mean we do something offensive.

I couldn’t skip the flags as they provide a visual element that changes the perception of the UI completely.

I considered shipping the app with the JSON data file but it includes a lot of information I don’t need and weighs 500kb. So I wrote a quick app that just printed out the data as arrays and I pasted them into the source file. This is an important step as the list might change and we’d need to go thru it again…​ This is how I parsed the JSON, I just ran this in the simulator and pasted the output into the Java source file:

private static final CaseInsensitiveOrder cio = new CaseInsensitiveOrder();
class Country implements Comparable<Country> {
    public final String name;
    public final String code;
    public final String flag;
    public final String isoCode2;
    public final String isoCode3;

    public Country(String name, String code, String flag, String isoCode2, String isoCode3) {
        this.name = name;
        this.code = code;
        this.flag = flag;
        this.isoCode2 = isoCode2;
        this.isoCode3 = isoCode3;
    }

    @Override
    public int compareTo(Country o) {
        return cio.compare(name, o.name);
    }
}

ArrayList<Country> con = new ArrayList<>();
try {
    JSONParser p = new JSONParser();
    Map<String, Object> dat = p.parseJSON(new InputStreamReader(getResourceAsStream("/countries.json"))); (1)
    List<Map<String, Object>> l = (List<Map<String, Object>>)dat.get("root");
    for(Map<String, Object> m : l) { (2)
        List ll = ((List)m.get("callingCode"));
        if(ll != null && ll.size() > 0) {
            String name = (String)((Map)m.get("name")).get("common");
            String callingCode = (String)ll.get(0);
            String code = (String)m.get("cioc");
            String flag = null;
            if(code != null && code.length() > 0) {
                flag = code.toLowerCase();
            }
            String isoCode2 = (String)m.get("cca2");
            String isoCode3 = (String)m.get("cca3");
            con.add(new Country(name, callingCode, flag, isoCode2, isoCode3)); (3)
        }
    }
} catch(IOException err) {
    Log.e(err);
}

Collections.sort(con); (4)
System.out.println("private static final String[] COUNTRY_NAMES = {");
for(Country c : con) { (5)
    System.out.println("    \"" + c.name + "\",");
}
System.out.println("};");
System.out.println("private static final String[] COUNTRY_CODES= {");
for(Country c : con) {
    System.out.println("    \"" + c.code + "\",");
}
System.out.println("};");
System.out.println("private static final String[] COUNTRY_FLAGS = {");
for(Country c : con) {
    if(c.flag == null) {
        System.out.println("    null,");
    } else {
        System.out.println("    \"" + c.flag + "\",");
    }
}
System.out.println("};");
System.out.println("private static final String[] COUNTRY_ISO2 = {");
for(Country c : con) {
    System.out.println("    \"" + c.isoCode2 + "\",");
}
System.out.println("};");
System.out.println("private static final String[] COUNTRY_ISO3 = {");
for(Country c : con) {
    System.out.println("    \"" + c.isoCode3 + "\",");
}
System.out.println("};");

Most of that code is pretty simple:

1 I load the JSON file from the resources
2 I loop over the entries within the JSON file one by one
3 I construct a Country object based on the entry data
4 Country is sortable thanks to the Comparable interface so I can just sort the list
5 Now I can just create the 5 String arrays I want

Yes, I know I could have kept the Country object and worked with that instead of 5 String arrays…​ I might change to that in the future.

Packaging it into a CN1LIB

Diamond surprised me a couple of weeks ago when he packaged my last post as a cn1lib. I think that’s great if what you need is interception of SMS messages.

This cn1lib is pretty different in its goals. I wanted to solve a very specific and restricted use case of validating a user with an SMS. So while we have some things in common with our cn1libs the basic premise is pretty far apart and it shows.

Share this Post:

Posted by Shai Almog

Shai is the co-founder of Codename One. He's been a professional programmer for over 25 years. During that time he has worked with dozens of companies including Sun Microsystems.
For more follow Shai on Twitter & github.