Build a set of views to support signup/login and navigation, Incorporate suitable models. Explore how flow-synchronisation works in this context.
This is v2 of the pacemaker-android project from the last android lab:
You can import this for reference purposes, or keep working with your own version.
Design a new activity to look like this:
You may end up with new entries in the strings.xml file:
<string name="title_activity_welcome">Pacemaker</string>
<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="title_activity_signup">Signup</string>
<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=".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="title_activity_login">Login</string>
<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=".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"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="18"
android:targetSdkVersion="18" />
<application
android:name="org.pacemaker.main.PacemakerApp"
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="org.pacemaker.controllers.Welcome"
android:label="@string/title_activity_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"
android:label="@string/title_activity_signup" >
</activity>
<activity
android:name="org.pacemaker.controllers.Login"
android:label="@string/title_activity_login">
android:windowSoftInputMode="adjustResize|stateVisible" >
</activity>
<activity
android:name="org.pacemaker.controllers.CreateActivity"
android:label="@string/app_name" >
</activity>
<activity
android:name="org.pacemaker.controllers.ActivitiesList"
android:label="@string/title_activity_activities_list" >
</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 simplify the Activity Model class to just this version:
{
public Long id;
public String type;
public String location;
public double distance;
public Activity()
{}
public Activity(String type, String location, double distance)
{
this.type = type;
this.location = location;
this.distance = distance;
}
}
... and introduce a new User class, also kept very simple:
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<Activity> actvities = new ArrayList<Activity>();
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<Activity> activities = new ArrayList<Activity>();
private 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 (Activity activity)
{
activities.add(activity);
}
public List<Activity> 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();
Activity activity = new Activity (activityType.getText().toString(), activityLocation.getText().toString(), distance);
app.createActivity(activity);
}
~~java protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activities_list);
app = (PacemakerApp) getApplication();
activitiesListView = (ListView) findViewById(R.id.activitiesListView);
List<Activity> activities = app.getActivities();
ActivityAdapter activitiesAdapter = new ActivityAdapter(this, activities);
activitiesListView.setAdapter(activitiesAdapter);
activitiesAdapter.notifyDataSetChanged();
} ~~~
This should work now as expected.
..of the app so far:
Currenty, once logged in, there is no way for a user to log out again. Introduce this facility.
There is only a single activities list - and all activities are inserted into this list. Refactor such that we are seeing the activities associated with the logged in user.
As we are using a Facade, this should not require extensive changes on any of the controllers.