Update: Parse is shutting down. So you should start migrating.
In this article we’ll build a login screen which will be somewhat similar to whatsapp’s login screen. So the login and registration screens will actually be the same and the unique identifier for each user will be their phone numbers. So to quickly summarize, here are the fields we’ll have:
What's the one thing every developer wants? More screens! Enhance your coding experience with an external monitor to increase screen real estate.
- First Name – This will accept and store the user’s first name.
- Last Name – This will accept and store the user’s last name.
- Country – This will be a simple dropdown holding the user’s country.
- Country Phone Code – Store the country’s phone number code.
- Phone Number – This will accept and store the user’s phone number which will be the unique identifier for each user.
As the backend, we won’t build our custom solution from scratch but leverage Parse.com to store all the user details. Parse’s SDK will help us with logging in and signing up the user as well as with other features like logout by maintaining the session on disk.
Installing and Setting Up Parse
Let’s quickly install and setup parse for our application with the following steps:
- Signup for a new account on Parse.com.
- Create a new app with your app’s name. All your application keys will be accessible here (Application Keys tab under Settings).
- Next go to the Quickstart wizard from where after making your selections through the subsequent steps you’ll end up at the SDK Installation guide. Download the latest SDK from the link given there – https://parse.com/downloads/android/Parse/latest
- Extract the zip file downloaded and move the
Parse-*.jar
file to the `libs` directory of your project. - Before using the Parse library, we must call
Parse.initialize(context, PARSE_APPLICATION_ID, PARSE_CLIENT_KEY)
once to set our application ID and client key. We can do this in theonCreate
method of our Application class. Here’s a really good article that
explains what an Application class is and how to set it up. - Installation complete!
Note: We’ll need the INTERNET
and ACCESS_NETWORK_STATE
permissions, hence add this in the AndroidManifest.xml
file before the <application>
tag:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
You can test the library with a simple piece of code:
ParseObject testObject = new ParseObject("TestObject"); testObject.put("foo", "bar"); testObject.saveInBackground();
This code creates a new object of class TestObject
(think of it as a Database Table or Collection) and saves a new row with the value of “bar” under the “foo” column. You can either click the “Test” button on the Parse SDK installation page or examine the data in the Parse Data Browser.
Coding into the Project
Parse is up and running so we should start coding the user interface and functionality into our app. We’ll start off with a very basic UI for our login cum registration/signup screen.
<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:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" tools:context="com.pycitup.pyc.LoginActivity"> <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/firstName" android:layout_alignParentTop="true" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:hint="First Name" android:inputType="textCapWords" /> <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/lastName" android:layout_below="@+id/firstName" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:hint="Last Name" android:inputType="textCapWords" /> <Spinner android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/country" android:layout_below="@+id/lastName" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="+" android:id="@+id/plusSign" android:layout_alignTop="@+id/countryCode" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:layout_alignBottom="@+id/countryCode" android:layout_marginTop="10dp" /> <EditText android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/countryCode" android:width="50dp" android:hint="1" android:layout_below="@+id/country" android:layout_toRightOf="@+id/plusSign" android:inputType="number" /> <EditText android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/phoneNumber" android:hint="phone number" android:layout_below="@+id/country" android:layout_alignRight="@+id/country" android:layout_alignEnd="@+id/country" android:layout_toRightOf="@+id/countryCode" android:inputType="phone" /> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Login" android:id="@+id/loginButton" android:layout_centerVertical="true" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" /> </RelativeLayout>
Nothing complicated, all very simple XML objects for our different views. One interesting thing to notice is the android:inputType
XML attribute. This attribute defines the content type of the text held by the Editable
object. It can hold multiple values separated by the bitwise OR (|) operator. For a comprehensive list of content types, check here.
Here are the different input types we’ve used:
- textCapWords: This leads to the soliciting of capitalization of the first character of each view. In effect, when you focus on the field and the keyboard slides up, the first character you type appears in uppercase and then the typing mode changes to lowercase.
- number: A numeric only field, hence the keyboard type will only contain numbers.
- phone: Phone number field, hence the keyboard type will appear like a dialer and contain characters pertinent to phone numbers.
Since we’re done with defining our views, we’ll move onto writing the code in our Activity
file that’ll deal with the functionality (including communication with Parse). So here’s the code that should go into a new Activity file called LoginActivity
.
import android.app.Activity; import android.app.AlertDialog; import android.content.Intent; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; import android.widget.Spinner; import com.parse.FindCallback; import com.parse.LogInCallback; import com.parse.ParseException; import com.parse.ParseQuery; import com.parse.ParseUser; import com.parse.SignUpCallback; import java.util.ArrayList; import java.util.List; public class LoginActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); // All the views from our login form final EditText firstNameView = (EditText) findViewById(R.id.firstName); final EditText lastNameView = (EditText) findViewById(R.id.lastName); final Spinner countryView = (Spinner) findViewById(R.id.country); final EditText countryCodeView = (EditText) findViewById(R.id.countryCode); final EditText phoneNumberView = (EditText) findViewById(R.id.phoneNumber); Button loginButtonView = (Button) findViewById(R.id.loginButton); // Set items for the Spinner dropdown ArrayList<String> countries = new ArrayList<String>(); countries.add("Australia"); countries.add("Brazil"); countries.add("China"); countries.add("Canada"); countries.add("India"); countries.add("Russia"); countries.add("Singapore"); countries.add("United States"); // Create the adapter for the spinner ArrayAdapter adapter = new ArrayAdapter(this, android.R.layout.simple_spinner_item, countries); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); // Attach the adapter to the spinner countryView.setAdapter(adapter); // On login button click loginButtonView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // Get the values of all the form fields final String phoneNumber = phoneNumberView.getText().toString().trim(); String firstName = firstNameView.getText().toString().trim(); String lastName = lastNameView.getText().toString().trim(); String countryCode = countryCodeView.getText().toString().trim(); String country = countryView.getSelectedItem().toString().trim(); // Simple validation: if any field is empty then don't let the form submit // and show an alert dialog with error message if (phoneNumber.isEmpty() || firstName.isEmpty() || lastName.isEmpty() || countryCode.isEmpty() || country.isEmpty()) { AlertDialog.Builder builder = new AlertDialog.Builder(LoginActivity.this); builder.setMessage("Please make sure you entered all the fields correctly.") .setTitle("Oops!") .setPositiveButton(android.R.string.ok, null); AlertDialog dialog = builder.create(); dialog.show(); return; } // Create a ParseUser object to create a new user final ParseUser user = new ParseUser(); user.setUsername(phoneNumber); user.setPassword("Fake Password"); user.put("firstName", firstName); user.put("lastName", lastName); user.put("country", country); user.put("countryCode", countryCode); // First query to check whether a ParseUser with // the given phone number already exists or not ParseQuery<ParseUser> query = ParseUser.getQuery(); query.whereEqualTo("username", phoneNumber); query.findInBackground(new FindCallback<ParseUser>() { @Override public void done(List<ParseUser> parseUsers, ParseException e) { if (e == null) { // Successful Query // User already exists ? then login if (parseUsers.size() > 0) { loginUser(phoneNumber, "Fake Password"); } else { // No user found, so signup signupUser(user); } } else { // Shit happened! AlertDialog.Builder builder = new AlertDialog.Builder(LoginActivity.this); builder.setMessage(e.getMessage()) .setTitle("Oops!") .setPositiveButton(android.R.string.ok, null); AlertDialog dialog = builder.create(); dialog.show(); } } }); } }); } private void loginUser(String username, String password) { ParseUser.logInInBackground(username, password, new LogInCallback() { public void done(ParseUser user, ParseException e) { if (user != null) { // Hooray! The user is logged in. navigateToHome(); } else { // Login failed! AlertDialog.Builder builder = new AlertDialog.Builder(LoginActivity.this); builder.setMessage(e.getMessage()) .setTitle("Oops!") .setPositiveButton(android.R.string.ok, null); AlertDialog dialog = builder.create(); dialog.show(); } } }); } private void signupUser(ParseUser user) { user.signUpInBackground(new SignUpCallback() { @Override public void done(ParseException e) { if (e == null) { // Signup successful! navigateToHome(); } else { // Fail! AlertDialog.Builder builder = new AlertDialog.Builder(LoginActivity.this); builder.setMessage(e.getMessage()) .setTitle("Oops!") .setPositiveButton(android.R.string.ok, null); AlertDialog dialog = builder.create(); dialog.show(); } } }); } private void navigateToHome() { // Let's go to the MainActivity Intent intent = new Intent(LoginActivity.this, MainActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); startActivity(intent); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.login, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); if (id == R.id.action_settings) { return true; } return super.onOptionsItemSelected(item); } }
In the onCreate
method the views objects are fetched, few countries are assigned to the spinner and an “onclick” listener is set on the login button. According to the code inside the onClick
method of the View.OnClickListener
interface implemented we take the values of each view and check if any of those is empty or not. If it is then an error is shown else the communication with Parse starts happening.
This is the flow of the queries made to Parse logically:
- A
ParseQuery
object is first created to check whether any user with the submitted phone number exists or not. Based on this value we’ll decide whether we need to login this user or signup (register). - If a user is found then the
loginUser
method is called where the handy class methodlogInInBackground
on theParseUser
class is passed the username (phone number) and password (a fake one) to sign in the user. - If a user is not found then the
signupUser
method is called where theParseUser
object passed to it is made to signup by calling thesignUpInBackground
method on it. - When the login or signup is successful the
navigateToHome
method is called that invokes theMainActivity
(home screen in our case) using an Intent.
If you notice, I’ve used 2 intent flags in the navigateToHome()
method:
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
Those flags help with eliminating the LoginActivity
from the tasks back stack, so that when you hit the back button after login, you won’t end up on the login screen again. Another way to achieve this behaviour is to use android:noHistory attribute of the relevant <activity>
tag in the AndroidManifest.xml
. When set to true, it won’t leave a historical trace in the activity stack for the task.
Let me enumerate a couple of strange decisions taken in the code or some optimization that could be done or standards followed for the better:
- We use phone number as the unique identifier and have no username or email. But at the same time, the username is required (and maintained as unique) on Parse’s side. So to work in accordance with their rules, I used phone number for the username field. This helps me to conform their required rule as well as maintain phone numbers in a unique field.
- We have no password, so I used a sample string called “Fake Password” that again complies with the Parse rules of having the password field as required.
- Instead of using strings directly as titles and messages for the Alert Builders, you’d want to set them in the strings resource file and reference them from the Activity’s code. I used strings directly to keep things short and to the point of the topic’s context.
- Instead of having so many
final
variables, we could probably set them as instance/member variables.
To make sure that the user is always logged in before using the app, we can add this piece of code to redirect the user to the login screen (when he launches the app) in the “main” activity’s onCreate
method.
// Get current user ParseUser currentUser = ParseUser.getCurrentUser(); if (currentUser == null) { // It's an anonymous user, hence show the login screen navigateToLogin(); } else { // The user is logged in, yay!! Log.i(TAG, currentUser.getUsername()); }
This is how the navigateToLogin()
method will look like:
private void navigateToLogin() { // Launch the login activity Intent intent = new Intent(this, LoginActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); startActivity(intent); }
We can add a logout button to the overflow menu that’ll execute this piece of code in the onOptionsItemSelected
method of the Activity
class:
ParseUser.logOut(); navigateToLogin();
Easy peasy!
Next Up
We discussed how to make a single screen to server login’s and registration’s purpose. It’s always good, safe and secure to validate the user on signup like sending him an email to activate his account. Since we don’t ask the user for his email but phone number, what we could do is send him an SMS with an activation code that he’ll need to put in the signup flow after submitting the form. I’ll discuss how to do that in the next post.