Build a set of views to support signup/login and navigation. Incorporate suitable models. Introduce the Facade pattern to encapsulate Model access.
This is the archive of the las lab:
https://github.com/wit-design-patterns-2016/pacemaker-android/releases/tag/V2
The starting point for this lab is a small evolution on the last lab, can can be downloaded here:
https://github.com/wit-design-patterns-2016/pacemaker-android/releases/tag/V3
This version has a refactored package structure:
Also, the adapter is reconfigured to load a layout for each row:
Design a new activity to look like this:
You may end up with new entries in the strings.xml file:
<string name="welcomeLogin">Login</string>
<string name="welcomeSignup">Sign up</string>
And this layout:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/RelativeLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<Button
android:id="@+id/welcomeLogin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:layout_marginTop="102dp"
android:onClick="loginPressed"
android:text="@string/welcomeLogin" />
<Button
android:id="@+id/welcomeSignup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:onClick="signupPressed"
android:text="@string/welcomeSignup" />
</RelativeLayout>
Design a new activity to look like this:
You may end up with new entries in the strings.xml file:
<string name="signupTitle">Sign up for the Pacemaker</string>
<string name="signupSubtitle">Enter details below</string>
<string name="signupFirstname">First name</string>
<string name="signupLastName">Last Name</string>
<string name="signupEmail">Email</string>
<string name="signupPassword">Password</string>
<string name="signupRegister">Register</string>
And this layout:
<RelativeLayout 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:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="org.pacemaker.controllers.Signup" >
<TextView
android:id="@+id/signupTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="32dp"
android:layout_marginTop="28dp"
android:text="@string/signupTitle"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/signupSubtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/signupTitle"
android:layout_below="@+id/signupTitle"
android:layout_marginLeft="55dp"
android:layout_marginTop="30dp"
android:text="@string/signupSubtitle"
android:textAppearance="?android:attr/textAppearanceSmall" />
<EditText
android:id="@+id/firstName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/signupSubtitle"
android:layout_marginTop="40dp"
android:ems="10"
android:hint="@string/signupFirstname"
android:inputType="textPersonName"/>
<requestFocus />
<EditText
android:id="@+id/lastName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/firstName"
android:layout_alignParentRight="true"
android:layout_below="@+id/firstName"
android:ems="10"
android:hint="@string/signupLastName"
android:inputType="textPersonName" >
</EditText>
<EditText
android:id="@+id/Email"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/lastName"
android:layout_alignParentRight="true"
android:layout_below="@+id/lastName"
android:ems="10"
android:hint="@string/signupEmail"
android:inputType="textEmailAddress" />
<EditText
android:id="@+id/Password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/Email"
android:layout_alignParentRight="true"
android:layout_below="@+id/Email"
android:ems="10"
android:hint="@string/signupPassword"
android:inputType="textPassword" />
<Button
android:id="@+id/register"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="28dp"
android:onClick="registerPressed"
android:text="@string/signupRegister" />
</RelativeLayout>
Design a new activity to look like this:
You may end up with new entries in the strings.xml file:
<string name="loginTitle">Login to Donation</string>
<string name="loginSubtitle">You must be reigstered</string>
<string name="loginSignin">Sign in</string>
<string name="loginEmail">Email</string>
<string name="loginPassword">Password</string>
And this layout:
<RelativeLayout 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:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="org.pacemaker.controllers.Login" >
<TextView
android:id="@+id/loginTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_marginTop="18dp"
android:text="@string/loginTitle"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/loginSubtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/loginTitle"
android:layout_alignParentRight="true"
android:layout_below="@+id/loginTitle"
android:text="@string/loginSubtitle"
android:textAppearance="?android:attr/textAppearanceSmall" />
<EditText
android:id="@+id/loginEmail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/loginSubtitle"
android:layout_alignRight="@+id/loginSubtitle"
android:layout_below="@+id/loginSubtitle"
android:layout_marginTop="17dp"
android:ems="10"
android:hint="@string/loginEmail"
android:inputType="textEmailAddress" >
<requestFocus />
</EditText>
<EditText
android:id="@+id/loginPassword"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/loginEmail"
android:layout_alignRight="@+id/loginEmail"
android:layout_below="@+id/loginEmail"
android:ems="10"
android:hint="@string/loginPassword"
android:inputType="textPassword" />
<Button
android:id="@+id/login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:onClick="signinPressed"
android:text="@string/loginSignin" />
</RelativeLayout>
These three classes will load the views we have just created. Place them in the org.pacemaker.conrollers
package:
package org.pacemaker.controllers;
import org.pacemaker.R;
import org.pacemaker.main.PacemakerApp;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
public class Welcome extends Activity
{
PacemakerApp app;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_welcome);
app = (PacemakerApp) getApplication();
}
public void loginPressed (View view)
{
startActivity (new Intent(this, Login.class));
}
public void signupPressed (View view)
{
startActivity (new Intent(this, Signup.class));
}
}
package org.pacemaker.controllers;
import org.pacemaker.R;
import org.pacemaker.main.PacemakerApp;
import android.os.Bundle;
import android.app.Activity;
import android.content.Intent;
import android.view.View;
import android.widget.TextView;
public class Signup extends Activity
{
private PacemakerApp app;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_signup);
app = (PacemakerApp) getApplication();
}
public void registerPressed (View view)
{
TextView firstName = (TextView) findViewById(R.id.firstName);
TextView lastName = (TextView) findViewById(R.id.lastName);
TextView email = (TextView) findViewById(R.id.Email);
TextView password = (TextView) findViewById(R.id.Password);
startActivity (new Intent(this, Login.class));
}
}
package org.pacemaker.controllers;
import org.pacemaker.R;
import org.pacemaker.main.PacemakerApp;
import android.os.Bundle;
import android.app.Activity;
import android.content.Intent;
import android.view.View;
import android.widget.TextView;
public class Login extends Activity
{
PacemakerApp app;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
}
public void signinPressed (View view)
{
app = (PacemakerApp) getApplication();
TextView email = (TextView) findViewById(R.id.loginEmail);
TextView password = (TextView) findViewById(R.id.loginPassword);
startActivity (new Intent(this, CreateActivity.class));
}
}
These new activities must be registered in the AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.pacemaker">
<application
android:name=".main.PacemakerApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name="org.pacemaker.controllers.Welcome">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="org.pacemaker.controllers.Signup">
</activity>
<activity
android:name="org.pacemaker.controllers.Login">
</activity>
<activity
android:name="org.pacemaker.controllers.CreateActivity">
</activity>
<activity
android:name="org.pacemaker.controllers.ActivitiesList">
</activity>
</application>
</manifest>
Note that we have marked the Welcome
activity as the on with the intent-filter
MAIN
Run the application now. You should be able to navigate from view to view.
We can introduce a new User class :
package org.pacemaker.models;
public class User
{
public Long id;
public String firstname;
public String lastname;
public String email;
public String password;
public User()
{}
public User(String firstname, String lastname, String email, String password)
{
this.firstname = firstname;
this.lastname = lastname;
this.email = email;
this.password = password;
}
}
We already have a list of activities in the PacemakerApp class - we can introduce a map of users alongside it:
public List<MyActivity> activities = new ArrayList<MyActivity>();
public Map<String, User> users = new HashMap<String, User>();
In Signup, we can create a new entry in this map :
public void registerPressed (View view)
{
TextView firstName = (TextView) findViewById(R.id.firstName);
TextView lastName = (TextView) findViewById(R.id.lastName);
TextView email = (TextView) findViewById(R.id.Email);
TextView password = (TextView) findViewById(R.id.Password);
User user = new User(firstName.getText().toString(), lastName.getText().toString(), email.getText().toString(), password.getText().toString());
app.users.put(user.email, user);
startActivity (new Intent(this, Login.class));
}
... and in Login we can validate the user against these entries:
public void signinPressed (View view)
{
app = (PacemakerApp) getApplication();
TextView email = (TextView) findViewById(R.id.loginEmail);
TextView password = (TextView) findViewById(R.id.loginPassword);
String emailStr = email.getText().toString();
String passwordStr = password.getText().toString();
User user = app.users.get(emailStr);
if (user != null && user.password.equals(passwordStr))
{
startActivity (new Intent(this, CreateActivity.class));
}
else
{
Toast toast = Toast.makeText(this, "Invalid Credentials", Toast.LENGTH_SHORT);
toast.show();
}
}
We should be able to register and log in now. And if we provide invalid credentials, we should be kept out.
There are, of course, some serious problems with our app. The list of activities is not associated with any specific user, the user and activities themselves are not persisted.
These issues could be tackled piecemeal - in each of the respective views for instance, but we might be more advised strengthen some of the abstractions around user/activity management first.
This pattern here may be appropriate:
Instead of a very exposed set of data structures in PacemakerApp:
public class PacemakerApp extends Application
{
public List<Activity> actvities = new ArrayList<Activity>();
public Map<String, User> users = new HashMap<String, User>();
@Override
public void onCreate()
{
super.onCreate();
Log.v("Pacemaker", "Pacemaker App Started");
}
}
We could remodel this class to provide a encapsulated perspective - a Facade - to the these models:
public class PacemakerApp extends Application
{
private List<MyActivity> activities = new ArrayList<MyActivity>();
public Map<String, User> users = new HashMap<String, User>();
private User loggedInUser;
public void registerUser(User user)
{
users.put(user.email, user);
}
public boolean loginUser(String email, String password)
{
loggedInUser = users.get(email);
if (loggedInUser != null && !loggedInUser.password.equals(password))
{
loggedInUser = null;
}
return loggedInUser != null;
}
public void createActivity (MyActivity activity)
{
activities.add(activity);
}
public List<MyActivity> getActivities()
{
return activities;
}
@Override
public void onCreate()
{
super.onCreate();
Log.v("Pacemaker", "Pacemaker App Started");
}
}
This will require a refactoring of most of the controllers:
public void registerPressed (View view)
{
TextView firstName = (TextView) findViewById(R.id.firstName);
TextView lastName = (TextView) findViewById(R.id.lastName);
TextView email = (TextView) findViewById(R.id.Email);
TextView password = (TextView) findViewById(R.id.Password);
User user = new User(firstName.getText().toString(), lastName.getText().toString(), email.getText().toString(), password.getText().toString());
app.registerUser(user);
startActivity (new Intent(this, Login.class));
}
public void signinPressed (View view)
{
app = (PacemakerApp) getApplication();
TextView email = (TextView) findViewById(R.id.loginEmail);
TextView password = (TextView) findViewById(R.id.loginPassword);
boolean loggedIn = app.loginUser(email.getText().toString(), password.getText().toString());
if (loggedIn)
{
startActivity (new Intent(this, CreateActivity.class));
}
else
{
Toast toast = Toast.makeText(this, "Invalid Credentials", Toast.LENGTH_SHORT);
toast.show();
}
public void createActivityButtonPressed (View view)
{
double distance = distancePicker.getValue();
MyActivity activity = new MyActivity (activityType.getText().toString(), activityLocation.getText().toString(), distance);
app.createActivity(activity);
Log.v("Pacemaker", "CreateActivity Button Pressed with " + distance);
}
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_activities_list);
app = (PacemakerApp) getApplication();
activitiesListView = (ListView) findViewById(R.id.activitiesListView);
List<MyActivity> activities = app.getActivities();
ActivityAdapter activitiesAdapter = new ActivityAdapter(this, activities);
activitiesListView.setAdapter(activitiesAdapter);
activitiesAdapter.notifyDataSetChanged();
}
}
This should work now as expected.
Archive of the app so far: