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.
Great tutorial thanx.
When are you gonna continue with the sms verification?
I’ve coded it already. Hopefully should write up an article soon!
Great tutorial
What happens if the user deletes and reinstalls the app to the same device without SMS verification.
I haven’t written about SMS verification yet (second part of this tutorial) but if you delete and re-install that won’t change the backend data. So you’ll still get automatically logged in if you’d registered earlier. Btw, till I write the second part, this article might be helpful in implementing SMS verification for you guys – http://codetheory.in/android-sms/
Hi,
Plz send me the complete code with sms verification process part also.
thanks in advance for your help…………….. plz i waiting for your response…
I haven’t worked on the SMS verification part (with Parse) yet. But this tutorial might help you find way – http://codetheory.in/android-sms/
plz send code on [email protected]
App is getting crashed on its launch.
And also, when there is no activity_main.xml, how can a MainActivity be opened up when its launched?!
Hi Rishabh
I have a one problem regarding to the Parse, when i change the password of Parse user it will successfully change and second time try to change the password it will not change. is that compulsory to logout from parse after changing password, help me…
Thanks.
Hey Rishabh, would you mind sending me the complete source code of this tutorial.
Parse.com is shutting down.
Here is my email: [email protected]
package com.login;
import java.io.Serializable;
/**
* Created by Jai on 6/10/2017.
*/
public class Pojo implements Serializable {
String name;
String loc;
String usernam;
String pass;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getLoc() {
return loc;
}
public void setLoc(String loc) {
this.loc = loc;
}
public String getUsernam() {
return usernam;
}
public void setUsernam(String usernam) {
this.usernam = usernam;
}
public String getPass() {
return pass;
}
public void setPass(String pass) {
this.pass = pass;
}
}
package com.login;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends Activity {
String username,password;
EditText user,pas;
TextView unam,pass;
Button lbt,sbt,fpas;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
user = (EditText)findViewById(R.id.urn);
pas=(EditText)findViewById(R.id.pass);
unam = (TextView)findViewById(R.id.ur);
pass = (TextView)findViewById(R.id.pa);
lbt = (Button)findViewById(R.id.lbt);
sbt =(Button)findViewById(R.id.sbtn);
fpas=(Button)findViewById(R.id.fpas);
sbt.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent si = new Intent(MainActivity.this,SignActivity.class);
startActivity(si);
}
});
lbt.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
username = user.getText().toString();
password = pas.getText().toString();
sqlite sq = new sqlite(MainActivity.this);
boolean b = sq.login(username,password);
if (b==true) {
Intent it = new Intent(MainActivity.this,list .class);
Toast.makeText(MainActivity.this,”welcome ” +username,Toast.LENGTH_LONG).show();
startActivity(it);
}
else {
Toast.makeText(MainActivity.this,”wrong username/password”,Toast.LENGTH_LONG).show();
}
}
});
fpas.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent fp = new Intent(MainActivity.this,Forgot.class);
startActivity(fp);
}
});
}
}
package com.login;
import android.app.Activity;
import android.content.ContentValues;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import static android.R.attr.name;
public class SignActivity extends Activity {
TextView snam,pass,loc,con,uname;
EditText enam,epass,email,econ,eunam;
Button subtn;
sqlite sq;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sign);
enam = (EditText) findViewById(R.id.sena);
epass = (EditText) findViewById(R.id.sepas);
email = (EditText) findViewById(R.id.semail);
econ = (EditText) findViewById(R.id.secpas);
eunam = (EditText) findViewById(R.id.seuna);
subtn = (Button) findViewById(R.id.subtn);
subtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (enam.getText().toString().equals(“”)|| email.getText().toString().equals(“”)) {
enam.setError(“field is empty”);
}
else {
ContentValues conValue = new ContentValues();
conValue.put(“name”, enam.getText().toString());
conValue.put(“loc”, email.getText().toString());
conValue.put(“unam”, eunam.getText().toString());
conValue.put(“pass”, epass.getText().toString());
sqlite sqli = new sqlite(SignActivity.this);
long i = sqli.insertAction(conValue, “login”);
Toast.makeText(SignActivity.this, “successfully entered”+i, Toast.LENGTH_LONG).show();
enam.setText(“”);
email.setText(“”);
epass.setText(“”);
econ.setText(“”);
eunam.setText(“”);
}
}
});
}
}
package com.login;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.widget.Toast;
import java.util.ArrayList;
/**
* Created by Jai on 6/10/2017.
*/
public class sqlite extends SQLiteOpenHelper {
private static final int Database_version = 1;
private static final String Dbname = “log.db”;
SQLiteDatabase db;
Context con;
public sqlite(Context context) {
super(context, Dbname, null, 1);
con = context;
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(“create table login (id integer primary key autoincrement,name varchar, unam varchar,loc varchar, pass varchar)”);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL(“drop table if exists” + db);
}
public long insertAction(ContentValues cValuea, String tablename) {
SQLiteDatabase db = this.getWritableDatabase();
long i = db.insert(tablename, null, cValuea);
db.close();
return i;
}
public boolean login(String usr, String pass) {
SQLiteDatabase dblogin = this.getReadableDatabase();
Cursor cursor = dblogin.rawQuery(“select * from login where unam=? and pass=?”, new String[]{usr, pass});
if (cursor.getCount() > 0) {
return true;
}
return false;
}
public ArrayList getall() {
SQLiteDatabase dball = this.getWritableDatabase();
Cursor all = dball.rawQuery(“select * from login”,null);
ArrayList li = new ArrayList();
while(all.moveToNext())
{
Pojo p =new Pojo();
p.setUsernam(all.getString(2));
p.setPass(all.getString(4));
li.add(p);
}
return li;
}
}