Ruby on Rails and Android Authentication Part Three
A Complete Android App with a Rails API Backend
In the previous two parts of the tutorial (first part and second part), you have been guided through the coding of a complete Ruby on Rails backend in part one, that can be used to register and login users through a JSON API that can be consumed by the Android native app we coded in part two.
In this third part of the tutorial I want to further extend the functionalities of our Android app and its Rails backend in order to allow the actual creation and editing of tasks through the exposed API.
Let’s start by securing a bit more the API, then we will proceed to enable the creation of new tasks
Security
Since I posted the tutorial I got some good advices on how to improve the API’s security, for example not passing the auth_token
as a GET parameter but in the HTTP Headers.
According to this response to a similar question on the official Devise Support Group, we need to override the params_auth_hash
of the TokenAuthenticatable
module.
Add the following code to the devise.rb
initializer in the config/initializers/
directory, just inside the Devise.setup
block (for example at the end, before the closing “end”).
# file: config/initializers/devise.rb
require 'devise/strategies/token_authenticatable'
module Devise
module Strategies
class TokenAuthenticatable < Authenticatable
def params_auth_hash
return_params = if params[scope].kind_of?(Hash) && params[scope].has_key?(authentication_keys.first)
params[scope]
else
params
end
token = ActionController::HttpAuthentication::Token.token_and_options(request)
return_params.merge!(:auth_token => token[0]) if token
return_params
end
end
end
end
Having done so it’s now possible to request the tasks to the API passing the auth_token inside the headers. Restart your Rails application and use curl from the command line to retrieve our (fake) tasks:
curl http://localhost:3000/api/v1/tasks.json -H 'Authorization: Token token="N8N5MPqFNdDz3G1jRsC9"'
You should receive the hard-coded tasks as usual.
It’s now the turn of our Android app to be modified to send the auth_token as a header when asking the tasks to the API.
To do this I decided to slightly modify the UrlJsonAsyncTask
and JsonHelper
from the library we used created by Tony Lukasavage.
I forked the original project on GitHub and made the modifications to the code. You can download the forked library here.
I added to the JsonHelper
a new parameter - authToken
to be passed to the get*FromUrl()
methods that will be added to the HTTP Headers in the getStringFromUrl()
method:
// file: com.savagelook.android/JsonHelper.java
private static String getStringFromUrl(String url, int connectTimeout, int readTimeout, String authToken)
throws MalformedURLException, JSONException, IOException {
URL urlObject = new URL(url);
HttpURLConnection urlConn = (HttpURLConnection)urlObject.openConnection();
urlConn.addRequestProperty("Authorization", "Token token=" + authToken);
// rest of the code ...
}
The auth_token
will be passed to the JsonHelper
from the modified UrlJsonAsyncTask
that has gained a new attribute authToken
, with its own getter and setter. We will use the latter in our HomeActivity
to pass the saved token our app received and saved in the properties after the login.
To finish up all this changes made to our app, just modify the loadTasksFromAPI()
method in the HomeActivity
// file: HomeActivity.java
private void loadTasksFromAPI(String url) {
GetTasksTask getTasksTask = new GetTasksTask(HomeActivity.this);
getTasksTask.setMessageLoading("Loading tasks...");
getTasksTask.setAuthToken(mPreferences.getString("AuthToken", ""));
getTasksTask.execute(url);
}
As you can see we are not passing the token as a GET parameter in the url but passing it to the GetTasksTask
(that is extended from UrlJsonAsyncTask
) thanks to the new setter method.
It’s now time to compile the app and test it. Everything should run as before, but now it will be more secure!
A real task
First of all we need to get rid of our fake tasks we setup in the previous tutorial. Let’s start creating a real model for the Task. We just need a title
and a completed
attributes. I also associate every task to the user who creates it.
rails g model task user_id:integer title:string completed:boolean
Generate the new model and the migration:
# file: db/migrate/<timestamp>_create_tasks.rb
class CreateTasks < ActiveRecord::Migration
def change
create_table :tasks do |t|
t.integer :user_id
t.string :title
t.boolean :completed, :default => false
t.timestamps
end
end
end
# file: app/models/task.rb
class Task < ActiveRecord::Base
belongs_to :user
attr_accessible :title
validates_presence_of :title
default_scope order('completed ASC, created_at DESC')
end
I added a default scope to order tasks based on the completion status and how old are they. In this way, when we receive them from the API, they are ordered as not completed first, done last and newer on top.
# file: app/models/user.rb
class User < ActiveRecord::Base
has_many :tasks
# rest of the code ...
end
Don’t forget to add the has_many
association to the User model.
rake db:migrate
Finally migrate the database.
Tasks API
To generate the API from our controller we will use the Rabl gem. It’s basically a builder to render JSON views and you can watch a nice screencast by Ryan Bates to learn more about it.
Let’s just add the gem to our Gemfile:
# file: Gemfile
gem 'rabl'
bundle install
And run the bundle install
command to install it. You also should add an initializer to the app to specify that you don’t want to add a root element to the JSON response and the children elements of the API. More info about that can be found in the Railscast linked above and in the official documentation of the Rabl gem.
# file: config/initializers/rabl_config.rb
Rabl.configure do |config|
config.include_json_root = false
config.include_child_root = false
end
Index action
It’s now time to remove our fake index action JSON response from the TasksController and provide some real data to our Android app.
# file: app/controllers/api/v1/tasks_controller.rb
class Api::V1::TasksController < ApplicationController
skip_before_filter :verify_authenticity_token,
:if => Proc.new { |c| c.request.format == 'application/json' }
before_filter :authenticate_user!
def index
@tasks = current_user.tasks
end
end
The index action is really simple! We just find the tasks associated with the current user, authenticated through the auth_token provided with each request to the API. All the work done so far is finally paying off.
# file: app/views/api/v1/tasks/index.json.rabl
object false
node (:success) { true }
node (:info) { 'ok' }
child :data do
node (:tasks_count) { @tasks.size }
child @tasks do
attributes :id, :title, :created_at, :completed
end
end
To use Rabl we need to create a new file in the views directory with the “.json.rabl” extension: the first to specify the format that it will provide and the second to use the correct builder.
Rabl, in my opinion, is not so easy to understand at first; but using the documentation and via a trial and error process you can wrap your head around it.
In this view I wanted to provide the same JSON format we faked before and use the same fields of the other part of the API, such as the user registration and authentication.
The object false
tells to the Rabl builder we don’t want to map the root-level of the response directly to any object and we construct the nodes freely.
I added two nodes with a success and info information that will be used by the Android app to check if everything is ok with the response and provide a message.
I also added a node with the tasks count that can come in handy.
You can test the real index action through the API with a curl command (the tasks will be empty if you haven’t created any).
curl -v -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Authorization: Token token="N8N5MPqFNdDz3G1jRsC9"' http://localhost:3000/api/v1/tasks.json
Create action
We are now able to requests the list of the tasks for a given user but we still need a way to create a new one. Let’s add the create action to the TasksController.
# file: app/controllers/api/v1/tasks_controller.rb
class Api::V1::TasksController < ApplicationController
# rest of the code ...
def create
@task = current_user.tasks.build(params[:task])
if @task.save
@task
else
render :status => :unprocessable_entity,
:json => { :success => false,
:info => @task.errors,
:data => {} }
end
end
end
As you can see we are building the new tasks against the current user tasks and rendering the created object with a Rabl JSON view. If something goes wrong the controller will render directly a response with an error message.
# file: app/views/api/v1/tasks/create.json.rabl
object false
node (:success) { true }
node (:info) { 'Task created!' }
child :data do
child @task do
attributes :id, :title, :created_at, :completed
end
end
The Rabl view is similar to the one we used for the index action: we just render the attributes for a single task and the additional nodes to give some more information about the response.
To finish up this last step, don’t forget to add the new create route to the app routes (the index action was already mapped before).
# file: config/routes.rb
AuthexampleWebapp::Application.routes.draw do
devise_for :users
namespace :api do
namespace :v1 do
devise_scope :user do
post 'registrations' => 'registrations#create', :as => 'register'
post 'sessions' => 'sessions#create', :as => 'login'
delete 'sessions' => 'sessions#destroy', :as => 'logout'
end
get 'tasks' => 'tasks#index', :as => 'tasks'
post 'tasks' => 'tasks#create'
end
end
ActiveAdmin.routes(self)
devise_for :admin_users, ActiveAdmin::Devise.config
end
To test the creation of a new task, just use this curl command and watch the response back from the API.
Remember that the auth_token used in these examples are random and provided by my own instance of the app. Use the ones created for your own users, as already specified in the previous part of the tutorial.
curl -v -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Authorization: Token token="N8N5MPqFNdDz3G1jRsC9"' -X POST http://localhost:3000/api/v1/tasks.json -d "{\"task\":{\"title\":\"A wild new task appears!\"}}"
{"success":true,"info":"Task created!","data":{"task":{"id":3,"title":"A wild new task appears!","created_at":"2012-12-02T19:52:22Z","done":false}}}
NewTaskActivity
Having completed the coding of the backend end point of the API, it’s now time to add the new feature to the Android app. In order to create a new task, the app should provide a new Activity with a text field and a save button.
This new Activity would be launched from the app action bar/menu, so we need to add a new item to it and catch the click on the new task button. Add the new menu_new_task
string to the strings.xml resource file.
<!-- file: res/menu/activity_home.xml -->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/menu_new_task"
android:title="@string/menu_new_task"
android:orderInCategory="100"
android:showAsAction="ifRoom" />
<item android:id="@+id/menu_refresh"
android:title="@string/menu_refresh"
android:orderInCategory="100"
android:showAsAction="ifRoom" />
</menu>
In order to launch the new task Activity, add a new id to the switch case in the onOptionsItemSelected()
method of the HomeActivity
class. When a user will click on the “New Task”, the NewTaskActivity
will be launched.
// file: HomeActivity.java
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle item selection
switch (item.getItemId()) {
case R.id.menu_new_task:
Intent intent = new Intent(HomeActivity.this, NewTaskActivity.class);
startActivityForResult(intent, 0);
return true;
case R.id.menu_refresh:
loadTasksFromAPI(TASKS_URL);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
Having added the additional code to manage the launch of the NewTaskActivity
, it’s time to actually create this new activity:
Open the menu File > New > Other ... > Android Activity
and name the new file NewTaskActivity
.
You can see the code for this new class here:
// file: NewTaskActivity.java
public class NewTaskActivity extends SherlockActivity {
private final static String CREATE_TASK_ENDPOINT_URL = "http://10.0.2.2:3000/api/v1/tasks.json";
private SharedPreferences mPreferences;
private String mTaskTitle;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_new_task);
mPreferences = getSharedPreferences("CurrentUser", MODE_PRIVATE);
}
public void saveTask(View button) {
EditText taskTitlelField = (EditText) findViewById(R.id.taskTitle);
mTaskTitle = taskTitlelField.getText().toString();
if (mTaskTitle.length() == 0) {
// input fields are empty
Toast.makeText(this, "Please write something as a title for this task",
Toast.LENGTH_LONG).show();
return;
} else {
// everything is ok!
CreateTaskTask createTask = new CreateTaskTask(NewTaskActivity.this);
createTask.setMessageLoading("Creating new task...");
createTask.execute(CREATE_TASK_ENDPOINT_URL);
}
}
private class CreateTaskTask extends UrlJsonAsyncTask {
public CreateTaskTask(Context context) {
super(context);
}
@Override
protected JSONObject doInBackground(String... urls) {
DefaultHttpClient client = new DefaultHttpClient();
HttpPost post = new HttpPost(urls[0]);
JSONObject holder = new JSONObject();
JSONObject taskObj = new JSONObject();
String response = null;
JSONObject json = new JSONObject();
try {
try {
json.put("success", false);
json.put("info", "Something went wrong. Retry!");
taskObj.put("title", mTaskTitle);
holder.put("task", taskObj);
StringEntity se = new StringEntity(holder.toString());
post.setEntity(se);
post.setHeader("Accept", "application/json");
post.setHeader("Content-Type", "application/json");
post.setHeader("Authorization", "Token token=" + mPreferences.getString("AuthToken", ""));
ResponseHandler<String> responseHandler = new BasicResponseHandler();
response = client.execute(post, responseHandler);
json = new JSONObject(response);
} catch (HttpResponseException e) {
e.printStackTrace();
Log.e("ClientProtocol", "" + e);
} catch (IOException e) {
e.printStackTrace();
Log.e("IO", "" + e);
}
} catch (JSONException e) {
e.printStackTrace();
Log.e("JSON", "" + e);
}
return json;
}
@Override
protected void onPostExecute(JSONObject json) {
try {
if (json.getBoolean("success")) {
Intent intent = new Intent(getApplicationContext(), HomeActivity.class);
startActivity(intent);
finish();
}
Toast.makeText(context, json.getString("info"), Toast.LENGTH_LONG).show();
} catch (Exception e) {
Toast.makeText(context, e.getMessage(), Toast.LENGTH_LONG).show();
} finally {
super.onPostExecute(json);
}
}
}
}
The Activity is very similar to the RegisterActivity
and as you can see the code is almost copied verbatim.
The differences are mainly in the CreateTaskTask
class extend from UrlJsonAsyncTask
that is called by the saveTask()
method when the user click on the save button.
The CreateTaskStak doInBackground()
method sets up the JSON request with all the right attributes (the auth_token taken from the SharedPreferences
and the value of the text field) and sends them to the API end point.
The onPostExecute
method instead close the activity in case of success and launch the HomeActivity
again.
The following code is the actual layout used for the NewTaskActivity
: it’s a simple EditText
field and a Button
. Remember to add all the necessary strings to the strings.xml resource file.
<!-- file: res/layout/activity_new_task.xml -->
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="15dp" >
<EditText
android:id="@+id/taskTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ems="10"
android:hint="@string/task_title"
android:inputType="textMultiLine" >
<requestFocus />
</EditText>
<Button
android:id="@+id/saveTaskButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="saveTask"
android:text="@string/save" />
</LinearLayout>
</ScrollView>
Finally, before compiling the Android app with the new feature, don’t forget to add to the manifest file the new activity:
<!-- file: AndroidManifest.xml -->
<activity
android:name=".NewTaskActivity"
android:label="@string/title_activity_new_task" >
</activity>
If everything goes right in the compiling process, you should see this new shiny activity that will allow you to create new tasks from the Android app directly to the Rails backend, using the improved API we coded earlier.
The NewTaskActivity layout (GingerBread 2.3 left and Jelly Bean 4.1 right, with the Sherlock Holo theme)
Completing Tasks: the Rails part
This is the last stretch of this long tutorial, bear with me just for a few lines of code more.
To actually do something with our tasks, we need a way to check them as “completed”. We already have an handy attribute to mark the task but it lacks a method to do so.
Reopen the Rails application and add these two new methods to the Task
model.
# file: app/models/task.rb
class Task < ActiveRecord::Base
# rest of the code ...
def open!
update_column(:completed, false)
end
def complete!
update_column(:completed, true)
end
end
I added non only a method to complete the task but also one to (re)open it: the user of our Android app should be able to check/uncheck his or her tasks.
# file: app/controller/tasks_controller.rb
class Api::V1::TasksController < ApplicationController
# rest of the code ...
def open
@task = current_user.tasks.find(params[:id])
@task.open!
rescue ActiveRecord::RecordNotFound
render :status => 404,
:json => { :success => false,
:info => 'Not Found',
:data => {} }
end
def complete
@task = current_user.tasks.find(params[:id])
@task.complete!
rescue ActiveRecord::RecordNotFound
render :status => 404,
:json => { :success => false,
:info => 'Not Found',
:data => {} }
end
end
The two new actions will update the task and render the JSON response back if everything is ok. I added a rescue for the RecordNotFound exception in order to render a proper response for this situation within the API.
Here are the two new views for the open and complete actions.
# file: app/views/api/v1/tasks/open.json.rabl
object false
node (:success) { true }
node (:info) { 'Task opened!' }
child :data do
child @task do
attributes :id, :title, :created_at, :completed
end
end
# file: app/views/api/v1/tasks/complete.json.rabl
object false
node (:success) { true }
node (:info) { 'Task completed!' }
child :data do
child @task do
attributes :id, :title, :created_at, :completed
end
end
I’m sure there is room for some refactoring and removal of duplicated code, but I leave this task to you.
# file: config/routes.rb
put 'tasks/:id/open' => 'tasks#open', :as => 'open_task'
put 'tasks/:id/complete' => 'tasks#complete', :as => 'complete_task'
Finally add these two new actions to the routes, inside the :api
namespace, after the tasks index route.
You can try the complete action via the API with the usual curl command (provided you have a task already created):
curl -v -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Authorization: Token token="9DLsDhFhqE8mnRbsPmXw"' -X PUT http://localhost:3000/api/v1/tasks/1/complete.json
{"success":true,"info":"Task completed!","data":{"task":{"id":1,"title":"question ","created_at":"2012-12-04T23:17:38Z","completed":true}}}
Completing tasks: the Android part
For the last time in this series of tutorials, we need to open Eclipse and make some changes to the code of the Android app.
Open the HomeActivity
file and start coding the last lines of code. I add another constant with the url of the tasks “toggle” endpoint of the API. We will add the task id and the required method (open or complete) when we will send the request in another method.
// file: HomeActivity.java
private static final String TOGGLE_TASKS_URL = "http://10.0.2.2:3000/api/v1/tasks/";
Before starting to add the code to actually toggle the completion of a task, we need to refactor some existing methods. When the GetTasksTask
class populates the tasks list with the objects retrieved from the API, instead of pushing just the titles of the tasks from the JSON response in an array, the methods populates an ArrayList
of Task
objects.
The for loop creates the new tasks to be added to the array with the given attributes extracted from the JSON response.
I also changed the way we handle the ListView
items, creating a custom Adapter
instead of using a normal ArrayAdapter
.
Note that I changed the layout for the list items from simple_list_item1
to simple_list_item_checked
that uses the CheckedTextView
instead of a simple TextView
to render the items.
// file: HomeActivity.java
private class GetTasksTask extends UrlJsonAsyncTask {
public GetTasksTask(Context context) {
super(context);
}
@Override
protected void onPostExecute(JSONObject json) {
try {
JSONArray jsonTasks = json.getJSONObject("data").getJSONArray("tasks");
JSONObject jsonTask = new JSONObject();
int length = jsonTasks.length();
final ArrayList<Task> tasksArray = new ArrayList<Task>(length);
for (int i = 0; i < length; i++) {
jsonTask = jsonTasks.getJSONObject(i);
tasksArray.add(new Task(jsonTask.getLong("id"), jsonTask.getString("title"), jsonTask.getBoolean("completed")));
}
ListView tasksListView = (ListView) findViewById (R.id.tasks_list_view);
if (tasksListView != null) {
tasksListView.setAdapter(new TaskAdapter(HomeActivity.this,
android.R.layout.simple_list_item_checked, tasksArray));
}
} catch (Exception e) {
Toast.makeText(context, e.getMessage(), Toast.LENGTH_LONG).show();
} finally {
super.onPostExecute(json);
}
}
}
The actual code of the Task
class is as follow. It’s basically a Java implementation of the Ruby class, with the id
, title
and completed
attributes and their getters and setters.
Just create a new class from the menu File > New > Class
and name it Task
.
// file: Task.java
public class Task {
private long id;
private String title;
private boolean completed;
public Task(long id, String title, boolean completed) {
this.id = id;
this.title = title;
this.completed = completed;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public boolean getCompleted() {
return completed;
}
public void setCompleted(boolean completed) {
this.completed = completed;
}
}
The code for the custom adapter could be added inside the HomeActivity
class file at the end before the closing braket.
More info about adapters can be found in the official documentation.
Particular interest in this code should be noted to the getView()
method where it sets the CheckedTextView
attributes from the Task
:
- the
setText()
method uses the task’s title - the
setChecked()
method sets the “checked” mark based on the state of the completed attribute of the task - a
onClickListener()
is set for the list item (see theonClick()
method) - the
setTag()
method assigns the task’s id to the tag to grab it when it’s clicked and use it to call the API
The onClick()
method simply calls the API based on the checked status of the clicked list item: if it’s checked (e.g. completed) send the request to the “open” action, otherwise to the “complete” one. The url is composed of the constant we set earlier, the task’s id extracted with the getTag()
method from the list item and the correct action to call (open or complete).
// file: HomeActivity.java
private class TaskAdapter extends ArrayAdapter<Task> implements OnClickListener {
private ArrayList<Task> items;
private int layoutResourceId;
public TaskAdapter(Context context, int layoutResourceId, ArrayList<Task> items) {
super(context, layoutResourceId, items);
this.layoutResourceId = layoutResourceId;
this.items = items;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = convertView;
if (view == null) {
LayoutInflater layoutInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = (CheckedTextView) layoutInflater.inflate(layoutResourceId, null);
}
Task task = items.get(position);
if (task != null) {
CheckedTextView taskCheckedTextView = (CheckedTextView) view.findViewById(android.R.id.text1);
if (taskCheckedTextView != null) {
taskCheckedTextView.setText(task.getTitle());
taskCheckedTextView.setChecked(task.getCompleted());
taskCheckedTextView.setOnClickListener(this);
}
view.setTag(task.getId());
}
return view;
}
@Override
public void onClick(View view) {
CheckedTextView taskCheckedTextView = (CheckedTextView) view.findViewById(android.R.id.text1);
if (taskCheckedTextView.isChecked()) {
taskCheckedTextView.setChecked(false);
toggleTasksWithAPI(TOGGLE_TASKS_URL + view.getTag() + "/open.json");
} else {
taskCheckedTextView.setChecked(true);
toggleTasksWithAPI(TOGGLE_TASKS_URL + view.getTag() + "/complete.json");
}
}
}
The toggleTasksWithAPI()
method is very simple and it just instantiates another class extended from the UrlJsonAsyncTask
and sets the title for the dialog and calls the execute method with the provided url as we have seen above.
// file: HomeActivity.java
private void toggleTasksWithAPI(String url) {
ToggleTaskTask completeTasksTask = new ToggleTaskTask(HomeActivity.this);
completeTasksTask.setMessageLoading("Updating task...");
completeTasksTask.execute(url);
}
We are almost there! The last class we need to add to the HomeActivity
is the aforementioned ToggleTaskTask
. As you can see it’s very similar to the RegisterTask
we coded in the previous part: it setups the json request with the correct headers with the auth token and executes.
Note that we are using the HttpPut
class instead of the HttpPost
one because our API responds to the PUT http verb for the open and complete actions.
The onPostExecute()
method just shows a Toast
with the success or error message coming from the API.
// file: HomeActivity.java
private class ToggleTaskTask extends UrlJsonAsyncTask {
public ToggleTaskTask(Context context) {
super(context);
}
@Override
protected JSONObject doInBackground(String... urls) {
DefaultHttpClient client = new DefaultHttpClient();
HttpPut put = new HttpPut(urls[0]);
String response = null;
JSONObject json = new JSONObject();
try {
try {
json.put("success", false);
json.put("info", "Something went wrong. Retry!");
put.setHeader("Accept", "application/json");
put.setHeader("Content-Type", "application/json");
put.setHeader("Authorization", "Token token=" + mPreferences.getString("AuthToken", ""));
ResponseHandler<String> responseHandler = new BasicResponseHandler();
response = client.execute(put, responseHandler);
json = new JSONObject(response);
} catch (HttpResponseException e) {
e.printStackTrace();
Log.e("ClientProtocol", "" + e);
} catch (IOException e) {
e.printStackTrace();
Log.e("IO", "" + e);
}
} catch (JSONException e) {
e.printStackTrace();
Log.e("JSON", "" + e);
}
return json;
}
@Override
protected void onPostExecute(JSONObject json) {
try {
Toast.makeText(context, json.getString("info"), Toast.LENGTH_LONG).show();
} catch (Exception e) {
Toast.makeText(context, e.getMessage(), Toast.LENGTH_LONG).show();
} finally {
super.onPostExecute(json);
}
}
}
}
The HomeActivity with some real tasks and checked marks (GingerBread 2.3 left and Jelly Bean 4.1 right, with the Sherlock Holo theme)
Conclusion
We made it!
It’s time to compile the app, fire up the Rails app (or push it to your server or to Heroku, if you followed the previous tutorial advice) and try it out. If everything went fine you should be able to register and login a user, create new tasks and mark them as completed, all by using a secure API exposed from your Rails application.
I think that by following every steps in this series of tutorials you will be able to build both the Rails backend and the Android app, but I know that it’s easier to look at the code when it’s already completed and not split in snippets. So in the next few days I will push the two applications to GitHub so you can read the code from there or directly clone it on your local development machine.
I know that the code isn’t the best you could write, but it’s working and I believe that it’s a good starting point for a newbie. I wrote these tutorials as the tutorials I wished I had when I started coding these kind of topics, so I hope you will find them useful.
For every question or doubt, just leave a comment below. If you need to contact me directly, you can do it by sending an email to luca.tironi@gmail.com
Thanks for all the patience and enjoy!
Luca
^Back to top