Human Factors of XR: Using Human Factors to Design XR Systems
Creating a Facebook Clone - Part XXVIII - Transcript.pdf
1. Creating a Facebook Clone - Part XXVIII
The sensible place to start is with the login & signup processes and the way they tie to the server.
2. Container logoContainer = BorderLayout.centerAbsolute(logo);
logoContainer.setUIID("LoginTitle");
signUp.addActionListener(e -> UIController.showSignup());
login.addActionListener(e -> {
User u = new User().password.set(password.getText());
if(user.getText().contains("@")) {
u.email.set(user.getText());
} else {
u.phone.set(user.getText());
}
Dialog d = new InfiniteProgress().showInifiniteBlocking();
ServerAPI.login(u, new Callback<User>() {
@Override
public void onSucess(User value) {
d.dispose();
UIController.showMainUI();
}
@Override
public void onError(Object sender, Throwable err,
int errorCode, String errorMessage) {
d.dispose();
ToastBar.showErrorMessage("Login Failed");
}
LoginForm
The login change is trivial, we need to replace the action listener of the login button with this new one
3. Container logoContainer = BorderLayout.centerAbsolute(logo);
logoContainer.setUIID("LoginTitle");
signUp.addActionListener(e -> UIController.showSignup());
login.addActionListener(e -> {
User u = new User().password.set(password.getText());
if(user.getText().contains("@")) {
u.email.set(user.getText());
} else {
u.phone.set(user.getText());
}
Dialog d = new InfiniteProgress().showInifiniteBlocking();
ServerAPI.login(u, new Callback<User>() {
@Override
public void onSucess(User value) {
d.dispose();
UIController.showMainUI();
}
@Override
public void onError(Object sender, Throwable err,
int errorCode, String errorMessage) {
d.dispose();
ToastBar.showErrorMessage("Login Failed");
}
LoginForm
We use the User object as a data transfer object, we set the password and phone/email to send them to the server
4. Container logoContainer = BorderLayout.centerAbsolute(logo);
logoContainer.setUIID("LoginTitle");
signUp.addActionListener(e -> UIController.showSignup());
login.addActionListener(e -> {
User u = new User().password.set(password.getText());
if(user.getText().contains("@")) {
u.email.set(user.getText());
} else {
u.phone.set(user.getText());
}
Dialog d = new InfiniteProgress().showInifiniteBlocking();
ServerAPI.login(u, new Callback<User>() {
@Override
public void onSucess(User value) {
d.dispose();
UIController.showMainUI();
}
@Override
public void onError(Object sender, Throwable err,
int errorCode, String errorMessage) {
d.dispose();
ToastBar.showErrorMessage("Login Failed");
}
LoginForm
The infinite progress UI is a bit of a shortcut but for a feature like login it makes some sense as we can't avoid a delay
5. login.addActionListener(e -> {
User u = new User().password.set(password.getText());
if(user.getText().contains("@")) {
u.email.set(user.getText());
} else {
u.phone.set(user.getText());
}
Dialog d = new InfiniteProgress().showInifiniteBlocking();
ServerAPI.login(u, new Callback<User>() {
@Override
public void onSucess(User value) {
d.dispose();
UIController.showMainUI();
}
@Override
public void onError(Object sender, Throwable err,
int errorCode, String errorMessage) {
d.dispose();
ToastBar.showErrorMessage("Login Failed");
}
});
});
add(NORTH, logoContainer);
LoginForm
A successful login shows the main UI whereas an error just shows the error message. Both dispose the infinite progress dialog
That's pretty simple and should work right away if we had a User object in place... To add a new user we need to overhaul the signup form.
6. public class SignupForm extends Form {
Container content = new Container(BoxLayout.y(), "PaddedContainer");
Container south = new Container(BoxLayout.y());
protected SignupForm(String title, String backLabel, Form previous) {
super(title, new BorderLayout());
setUIID("SignupForm");
content.setScrollableY(true);
add(CENTER, content);
getToolbar().setBackCommand(backLabel,
Toolbar.BackCommandPolicy.WHEN_USES_TITLE_OTHERWISE_ARROW,
e -> previous.showBack());
getToolbar().getTitleComponent().setUIID("Title", "TitleLandscape");
Button problem = new Button("Report a Problem", "BlueLink");
south.add(problem);
problem.addActionListener(e ->
sendMessage("Problem with Facebook Clone",
new Message("Details..."),
"mark@facebook.com"));
add(SOUTH, south);
SignupForm
As you may recall the SignupForm has a series of methods to create the wizard flow UI. This series of methods should generate a User object that we can use for signup
7. next.addActionListener(al);
return next;
}
public static SignupForm createTerms() {
SignupForm s = new SignupForm("Create Account", "Sign In",
getCurrentForm());
Label title = new Label("Terms & Conditions", "SignupSubHeader");
RichTextView rt = new RichTextView(
"By signing up you agree to our " +
"<a href="terms">Facebook Terms</a> and that you have " +
"read our <a href="data-policy">Data Policy</a>, including " +
"our <a href="cookie-use">Cookie Use</a>.");
rt.setAlignment(CENTER);
rt.setUIID("PaddedContainer");
Button next = s.createNextButton(e -> createName().show());
next.setText("I Agree");
rt.addLinkListener(e -> {
String link = (String) e.getSource();
execute("https://www.codenameone.com/");
});
s.content.addAll(title, rt, next);
return s;
}
SignupForm
The first in the series is the createTerms() method, since that method doesn't include any data there is no need for a User object there
8. s.content.addAll(title, rt, next);
return s;
}
public static SignupForm createName() {
SignupForm s = new SignupForm("Name", "Terms",
getCurrentForm());
Label title = new Label("What's Your Name?", "SignupSubHeader");
TextComponent first = new TextComponent().
label("First Name").columns(12);
TextComponent last = new TextComponent().
label("Last Name").columns(12);
User currentUser = new User();
UiBinding uib = new UiBinding();
uib.bind(currentUser.firstName, first);
uib.bind(currentUser.familyName, last);
TextModeLayout layout = new TextModeLayout(1, 2);
Container textContainer = new Container(layout, "PaddedContainer");
textContainer.add(layout.createConstraint().
widthPercentage(50), first);
textContainer.add(layout.createConstraint().
widthPercentage(50), last);
last.getField().setDoneListener(e ->
SignupForm
The second method is createName() and there we already have some work
9. s.content.addAll(title, rt, next);
return s;
}
public static SignupForm createName() {
SignupForm s = new SignupForm("Name", "Terms",
getCurrentForm());
Label title = new Label("What's Your Name?", "SignupSubHeader");
TextComponent first = new TextComponent().
label("First Name").columns(12);
TextComponent last = new TextComponent().
label("Last Name").columns(12);
User currentUser = new User();
UiBinding uib = new UiBinding();
uib.bind(currentUser.firstName, first);
uib.bind(currentUser.familyName, last);
TextModeLayout layout = new TextModeLayout(1, 2);
Container textContainer = new Container(layout, "PaddedContainer");
textContainer.add(layout.createConstraint().
widthPercentage(50), first);
textContainer.add(layout.createConstraint().
widthPercentage(50), last);
last.getField().setDoneListener(e ->
SignupForm
Since this is the first stage in the wizard we create the User & binding instances
10. s.content.addAll(title, rt, next);
return s;
}
public static SignupForm createName() {
SignupForm s = new SignupForm("Name", "Terms",
getCurrentForm());
Label title = new Label("What's Your Name?", "SignupSubHeader");
TextComponent first = new TextComponent().
label("First Name").columns(12);
TextComponent last = new TextComponent().
label("Last Name").columns(12);
User currentUser = new User();
UiBinding uib = new UiBinding();
uib.bind(currentUser.firstName, first);
uib.bind(currentUser.familyName, last);
TextModeLayout layout = new TextModeLayout(1, 2);
Container textContainer = new Container(layout, "PaddedContainer");
textContainer.add(layout.createConstraint().
widthPercentage(50), first);
textContainer.add(layout.createConstraint().
widthPercentage(50), last);
last.getField().setDoneListener(e ->
SignupForm
Binding means changes to the UI instantly change this field & visa versa. That's very convenient
11. TextModeLayout layout = new TextModeLayout(1, 2);
Container textContainer = new Container(layout, "PaddedContainer");
textContainer.add(layout.createConstraint().
widthPercentage(50), first);
textContainer.add(layout.createConstraint().
widthPercentage(50), last);
last.getField().setDoneListener(e ->
createBirthday(currentUser, uib).show());
s.content.addAll(title, textContainer,
s.createNextButton(e -> createBirthday(currentUser, uib).
show()));
return s;
}
public static SignupForm createBirthday(User currentUser, UiBinding uib) {
SignupForm s = new SignupForm("Birthday",
"Name",
getCurrentForm());
Label title = new Label("What's Your Birthday?", "SignupSubHeader");
Picker datePicker = new Picker();
datePicker.setType(PICKER_TYPE_DATE);
int twentyYears = 60000 * 60 * 24 * 365 * 20;
datePicker.setDate(
SignupForm
The createBirthday method now accepts the user and binding objects so we can fill them up with further details
12. s.content.addAll(title, textContainer,
s.createNextButton(e -> createBirthday(currentUser, uib).
show()));
return s;
}
public static SignupForm createBirthday(User currentUser, UiBinding uib) {
SignupForm s = new SignupForm("Birthday",
"Name",
getCurrentForm());
Label title = new Label("What's Your Birthday?", "SignupSubHeader");
Picker datePicker = new Picker();
datePicker.setType(PICKER_TYPE_DATE);
int twentyYears = 60000 * 60 * 24 * 365 * 20;
datePicker.setDate(
new Date(System.currentTimeMillis() - twentyYears));
uib.bind(currentUser.birthday, datePicker);
s.content.addAll(title, datePicker,
s.createNextButton(e -> createGender(currentUser, uib).show()));
return s;
}
private static RadioButton createGenderButton(ButtonGroup bg,
String label, String icon) {
SignupForm
That's simple enough, the binding implicitly updates the user objects with changes from the UI. Next we can proceed to the changes in the birthday method obviously
the first change in the method is the signature which now accepts the user and binding
13. s.content.addAll(title, textContainer,
s.createNextButton(e -> createBirthday(currentUser, uib).
show()));
return s;
}
public static SignupForm createBirthday(User currentUser, UiBinding uib) {
SignupForm s = new SignupForm("Birthday",
"Name",
getCurrentForm());
Label title = new Label("What's Your Birthday?", "SignupSubHeader");
Picker datePicker = new Picker();
datePicker.setType(PICKER_TYPE_DATE);
int twentyYears = 60000 * 60 * 24 * 365 * 20;
datePicker.setDate(
new Date(System.currentTimeMillis() - twentyYears));
uib.bind(currentUser.birthday, datePicker);
s.content.addAll(title, datePicker,
s.createNextButton(e -> createGender(currentUser, uib).show()));
return s;
}
private static RadioButton createGenderButton(ButtonGroup bg,
String label, String icon) {
SignupForm
Other than the method signature we added this line to bind the date picker to the birthday
14. s.content.addAll(title, textContainer,
s.createNextButton(e -> createBirthday(currentUser, uib).
show()));
return s;
}
public static SignupForm createBirthday(User currentUser, UiBinding uib) {
SignupForm s = new SignupForm("Birthday",
"Name",
getCurrentForm());
Label title = new Label("What's Your Birthday?", "SignupSubHeader");
Picker datePicker = new Picker();
datePicker.setType(PICKER_TYPE_DATE);
int twentyYears = 60000 * 60 * 24 * 365 * 20;
datePicker.setDate(
new Date(System.currentTimeMillis() - twentyYears));
uib.bind(currentUser.birthday, datePicker);
s.content.addAll(title, datePicker,
s.createNextButton(e -> createGender(currentUser, uib).show()));
return s;
}
private static RadioButton createGenderButton(ButtonGroup bg,
String label, String icon) {
SignupForm
And we added the arguments to the gender method too
15. rb.setPressedIcon(selIcon);
rb.setUIID("GenderToggle");
return rb;
}
public static SignupForm createGender(User currentUser, UiBinding uib) {
SignupForm s = new SignupForm("Gender",
"Birthday",
getCurrentForm());
Label title = new Label("What's Your Gender?", "SignupSubHeader");
ButtonGroup bg = new ButtonGroup();
RadioButton female = createGenderButton(bg, "Female", "ue800");
RadioButton male = createGenderButton(bg, "Male", "ue801");
uib.bindGroup(currentUser.gender,
new String[]{"Male", "Female"}, male, female);
Container buttons = GridLayout.encloseIn(2, female, male);
buttons.setUIID("PaddedContainer");
s.content.addAll(title, buttons,
s.createNextButton(e -> createNumber(currentUser, uib).show()));
return s;
}
SignupForm
This leads us to the changes in the createGender() method. Since most of the changes are repetitive I'll skip the method signature and the signature of the followup
createNumber method. Suffice to say we pass the user & binding onward…
This is the binding code for the gender that automatically sets Male/Female for the gender attribute based on the radio button selection. It's a special case because these
are buttons so we need to provide the values that match each button.
16. new String[]{"Male", "Female"}, male, female);
Container buttons = GridLayout.encloseIn(2, female, male);
buttons.setUIID("PaddedContainer");
s.content.addAll(title, buttons,
s.createNextButton(e -> createNumber(currentUser, uib).show()));
return s;
}
public static SignupForm createNumber(User currentUser, UiBinding uib) {
return createMobileOrEmail(currentUser, currentUser.phone, uib,
"Mobile Number",
"What's Your Mobile Number?",
"Sign Up With Email Address",
TextArea.PHONENUMBER,
e -> createEmail(currentUser, uib).show());
}
private static SignupForm createMobileOrEmail(User currentUser,
Property userProp, UiBinding uib, String formTitle,
String subtitle, String signUpWith, int constraint,
ActionListener goToOther) {
SignupForm s =
new SignupForm(formTitle, getCurrentForm().getTitle(),
getCurrentForm());
SignupForm
The createNumber changes are a bit different because of the symbiotic relationship to the createEmail method. I'll list more of the code but I'll skip the createEmail
method as the changes there are obvious.
The change itself is in the createMobileOrEmail method which is common to createNumber & createEmail
17. TextArea.PHONENUMBER,
e -> createEmail(currentUser, uib).show());
}
private static SignupForm createMobileOrEmail(User currentUser,
Property userProp, UiBinding uib, String formTitle,
String subtitle, String signUpWith, int constraint,
ActionListener goToOther) {
SignupForm s =
new SignupForm(formTitle, getCurrentForm().getTitle(),
getCurrentForm());
Label title = new Label(subtitle, "SignupSubHeader");
TextComponent textEntry = new TextComponent().
label(formTitle).
columns(20).
constraint(constraint);
uib.bind(userProp, textEntry);
Container textContainer = new Container(new TextModeLayout(1, 1),
"PaddedContainer");
textContainer.add(textEntry);
Button mobile = new Button(signUpWith, "BoldBlueLink");
mobile.addActionListener(goToOther);
SignupForm
This is the only change in this method. It's right under the declaration of textEntry but in effect it can be anywhere in the method. We bind the property to the text
component both of which are passed as arguments to this method.
18. TextArea.EMAILADDR,
e -> createNumber(currentUser, uib).show());
}
public static SignupForm createPassword(
User currentUser, UiBinding uib, boolean phone, String value) {
SignupForm s = new SignupForm("Password",
getCurrentForm().getTitle(),
getCurrentForm());
Label title = new Label("Choose a Password", "SignupSubHeader");
TextComponent password = new TextComponent().
label("Password").
columns(20);
uib.bind(currentUser.password, password);
Container textContainer = new Container(new TextModeLayout(1, 1),
"PaddedContainer");
textContainer.add(password);
s.content.addAll(title, textContainer,
s.createNextButton(e -> {
Dialog dlg = new Dialog("Signing Up...",
new BorderLayout(
BorderLayout.CENTER_BEHAVIOR_CENTER_ABSOLUTE));
SignupForm
The final piece of the wizard is the createPassword method, which follows up with a code change that's pretty similar to everything we saw so far at least initially…
The binding call is pretty standard as we had up until now
19. SignupForm s = new SignupForm("Password",
getCurrentForm().getTitle(),
getCurrentForm());
Label title = new Label("Choose a Password", "SignupSubHeader");
TextComponent password = new TextComponent().
label("Password").
columns(20);
uib.bind(currentUser.password, password);
Container textContainer = new Container(new TextModeLayout(1, 1),
"PaddedContainer");
textContainer.add(password);
s.content.addAll(title, textContainer,
s.createNextButton(e -> {
Dialog dlg = new Dialog("Signing Up...",
new BorderLayout(
BorderLayout.CENTER_BEHAVIOR_CENTER_ABSOLUTE));
dlg.add(CENTER, new InfiniteProgress());
dlg.showModeless();
ServerAPI.signup(currentUser, new Callback<User>() {
@Override
public void onSucess(User result) {
dlg.dispose();
createConfirmation(currentUser, phone, value).show();
SignupForm
We launch a progress dialog that runs while we wait for signup to complete
20. s.content.addAll(title, textContainer,
s.createNextButton(e -> {
Dialog dlg = new Dialog("Signing Up...",
new BorderLayout(
BorderLayout.CENTER_BEHAVIOR_CENTER_ABSOLUTE));
dlg.add(CENTER, new InfiniteProgress());
dlg.showModeless();
ServerAPI.signup(currentUser, new Callback<User>() {
@Override
public void onSucess(User result) {
dlg.dispose();
createConfirmation(currentUser, phone, value).show();
}
@Override
public void onError(Object sender, Throwable err,
int errorCode, String errorMessage) {
dlg.dispose();
ToastBar.showErrorMessage(
"Error in server connection");
}
});
}));
return s;
}
SignupForm
If signup was successful we go to the confirmation UI and wait for that
That effectively created a user on the server and triggered an email or SMS to us. We can now check the confirmation value to see if the user is indeed valid.
21. "Enter the code in the message sent to " + value,
"CenterLabel");
}
TextComponent confirm = new TextComponent().
label("Confirmation Code").
columns(20).
constraint(TextArea.NUMERIC);
Container textContainer = new Container(new TextModeLayout(1, 1),
"PaddedContainer");
textContainer.add(confirm);
Button done = s.createNextButton(e -> {
Dialog d = new InfiniteProgress().showInifiniteBlocking();
if(ServerAPI.verifyUser(confirm.getText(), !phone)) {
UIController.showMainUI();
} else {
d.dispose();
ToastBar.showErrorMessage("Verification code error!");
}
});
done.setText("Confirm");
s.content.addAll(title, line, textContainer, done);
return s;
}
SignupForm
We do that by adding this code to the createConfirmation method. If verification succeeded we show the main UI…
22. "Enter the code in the message sent to " + value,
"CenterLabel");
}
TextComponent confirm = new TextComponent().
label("Confirmation Code").
columns(20).
constraint(TextArea.NUMERIC);
Container textContainer = new Container(new TextModeLayout(1, 1),
"PaddedContainer");
textContainer.add(confirm);
Button done = s.createNextButton(e -> {
Dialog d = new InfiniteProgress().showInifiniteBlocking();
if(ServerAPI.verifyUser(confirm.getText(), !phone)) {
UIController.showMainUI();
} else {
d.dispose();
ToastBar.showErrorMessage("Verification code error!");
}
});
done.setText("Confirm");
s.content.addAll(title, line, textContainer, done);
return s;
}
SignupForm
Otherwise we show an error message and remain in the current Form.
23. public static void showSplashScreen() {
Form splash = new Form(new BorderLayout(
BorderLayout.CENTER_BEHAVIOR_CENTER_ABSOLUTE));
splash.setUIID("SplashForm");
Label logo = new Label("uf308", "IconFont");
logo.setName("Logo");
splash.add(CENTER, logo);
splash.setTransitionOutAnimator(
MorphTransition.
create(1200).
morph("Logo"));
final Motion anim = Motion.createLinearMotion(0, 127, 1000);
anim.start();
UITimer.timer(20, true, splash, () -> {
if(anim.isFinished()) {
if(!ServerAPI.isLoggedIn()) {
showLoginForm();
} else {
showMainUI();
}
} else {
logo.getUnselectedStyle().setOpacity(anim.getValue() + 127);
logo.repaint();
}
});
splash.show();
UIController
We still have one more change to make to the UIController class. Up until now we always started with the LoginForm but we should only start there when we are logged
out. We can just change the call to showMainUI() to this. If we aren't logged in correctly we behave as before after the splash screen. However, if we are logged in we go
to the main UI…
With that a new User is in place and should allow us to login to the app!