By now, if you’ve been keeping up with my tutorials, you can probably put two-and-two together and determine that I’m really trying to get into the NoSQL world. Up until now I’ve brushed upon Google Firebase, Facebook Parse, and Apache CouchDB, most of which I’ve done some kind of todo list type application. Now of course, many of my previous tutorials were based on hybrid app development instead of native.
This time we’re going to take a look at Couchbase’s version of NoSQL in a mobile Android application. In particular a native Android application.
Couchbase actually ships several different products as part of its mobile arsenal, but in this particular tutorial we’re going to focus strictly on Couchbase Lite. If you’re unfamiliar with Couchbase Lite, it is actually the embedded NoSQL database that lives locally on the mobile device. It is going to offer us a convenient replacement to Android’s SQLite option.
Let’s start things off by creating a fresh Android project via the command prompt or terminal:
android create project --activity MainActivity --package com.nraboy.couchbaseapp --target android-21 --path ./CouchbaseApp --gradle --gradle-version 0.11.+
As you can see in the above command, we are going to be using Gradle as our build tool rather than Apache Ant.
Before we dive into our code, let’s crack open the build.gradle file and add a few dependencies:
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:0.11.+'
}
}
apply plugin: 'android'
repositories {
mavenCentral()
maven {
url "http://files.couchbase.com/maven2/"
}
}
dependencies {
compile 'com.couchbase.lite:couchbase-lite-android:1.0.4'
compile 'com.android.support:appcompat-v7:21.0.+'
compile 'com.shamanland:fab:0.0.8'
}
android {
compileSdkVersion 'android-21'
buildToolsVersion '21.0.2'
defaultConfig {
minSdkVersion 9
targetSdkVersion 21
}
buildTypes {
release {
runProguard false
proguardFile getDefaultProguardFile('proguard-android.txt')
}
}
packagingOptions {
exclude 'META-INF/ASL2.0'
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE'
}
}
We are adding the latest Couchbase Lite library from the Maven repository as well as a nifty floating action bar and compatibility library for material design.
Above is what we’re going to try to accomplish. It is a todo list with with the ability to both add and remove list items. We aren’t going to worry about changing list items in this tutorial.
We are going to take care of the UI before we start jumping into the Java code. Open your project’s src/main/res/layout/main.xml file and add the following code:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/todo_list"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<com.shamanland.fab.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginRight="16dp"
android:layout_marginBottom="20dp"
android:src="@drawable/ic_action_new"
app:floatingActionButtonSize="mini"
android:onClick="addTodo" />
</RelativeLayout>
</RelativeLayout>
The above XML is the only UI screen in our application. It is just a list view and floating action button to make it more material design like.
In our list view, keeping track of only the todo title data won’t cut it because we are going to perform transactions that require more. In particular we will need the Couchbase Document id, which in this case is a string. Because of this we need to create a custom adapter that will be used with our Android list view. Create src/main/java/com/nraboy/couchbaseapp/TodoAdapter.java and add the following source code:
package com.nraboy.couchbaseapp;
import android.app.*;
import android.os.*;
import android.widget.*;
import java.util.*;
import android.graphics.*;
import android.view.*;
import android.content.*;
import com.couchbase.lite.Document;
public class TodoAdapter extends BaseAdapter {
private Context context;
private ArrayList<Document> documentList;
public TodoAdapter(Context context, ArrayList<Document> documentList) {
this.context = context;
this.documentList = documentList;
}
public int getCount() {
return this.documentList.size();
}
public Object getItem(int position) {
return this.documentList.get(position);
}
public long getItemId(int position) {
return 0;
}
public int getIndexOf(String id) {
for(int i = 0; i < this.documentList.size(); i++) {
if(this.documentList.get(i).getId().equals(id)) {
return i;
}
}
return -1;
}
public View getView(int position, View convertView, ViewGroup parent) {
TextView todoTitle = null;
Document currentDocument;
try {
if(convertView == null) {
todoTitle = new TextView(this.context);
todoTitle.setPadding(10, 25, 10, 25);
} else {
todoTitle = (TextView) convertView;
}
currentDocument = this.documentList.get(position);
todoTitle.setText(String.valueOf(currentDocument.getProperty("todo")));
} catch (Exception e) {
e.printStackTrace();
}
return todoTitle;
}
}
Time to break down a few important pieces of the above code.
Much of the TodoAdapter class comes pretty standard with the exception of the getIndexOf(String id)
function and the logic inside of the getView
method. The getIndexOf
function will take in a Document id and return the index of the ArrayList
that it resides in. Note that a Document id, is an actual value determined by Couchbase Lite. This function will prove useful when deleting documents from our database. This brings us to the logic found in the getView
function. We’re actually picking a Document from the list and storing the value at the todo
key in a TextView
. The TextView
is actually a row in our list view.
Moving beyond the TodoAdapter
, let’s get deep into the stuff that matters. Open your project’s src/main/java/com/nraboy/couchbaseapp/MainActivity.java file and add the following source code:
package com.nraboy.couchbaseapp;
import android.app.*;
import android.os.*;
import com.couchbase.lite.*;
import com.couchbase.lite.android.*;
import android.util.*;
import java.util.*;
import com.shamanland.fab.*;
import android.support.v7.app.*;
import android.content.*;
import android.widget.*;
import android.view.*;
import android.widget.AdapterView.OnItemLongClickListener;
public class MainActivity extends ActionBarActivity {
private final String TAG = "Couchbase";
private Manager couchbaseManager;
private Database couchbaseDatabase;
private ListView todoList;
private ArrayList<Document> todoArray;
private TodoAdapter adapter;
private FloatingActionButton fab;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
this.todoList = (ListView) findViewById(R.id.todo_list);
this.todoArray = new ArrayList<Document>();
this.adapter = new TodoAdapter(this, this.todoArray);
this.todoList.setAdapter(this.adapter);
this.fab = (FloatingActionButton) findViewById(R.id.fab);
this.todoList.setOnTouchListener(new ShowHideOnScroll(this.fab));
try {
this.couchbaseManager = new Manager(new AndroidContext(this), Manager.DEFAULT_OPTIONS);
this.couchbaseDatabase = this.couchbaseManager.getDatabase("mydb");
Query allDocumentsQuery = couchbaseDatabase.createAllDocumentsQuery();
QueryEnumerator queryResult = allDocumentsQuery.run();
for (Iterator<QueryRow> it = queryResult; it.hasNext(); ) {
QueryRow row = it.next();
todoArray.add(row.getDocument());
adapter.notifyDataSetChanged();
}
} catch (Exception e) {
Log.e(TAG, "An error happened", e);
return;
}
if(this.couchbaseDatabase != null) {
this.couchbaseDatabase.addChangeListener(new Database.ChangeListener() {
public void changed(Database.ChangeEvent event) {
for(int i = 0; i < event.getChanges().size(); i++) {
Document retrievedDocument = couchbaseDatabase.getDocument(event.getChanges().get(i).getDocumentId());
if(retrievedDocument.isDeleted()) {
int documentIndex = adapter.getIndexOf(retrievedDocument.getId());
if(documentIndex > -1) {
todoArray.remove(documentIndex);
}
} else {
todoArray.add(retrievedDocument);
}
adapter.notifyDataSetChanged();
}
}
});
}
this.todoList.setOnItemLongClickListener(new OnItemLongClickListener() {
public boolean onItemLongClick(AdapterView<?> a, android.view.View v, int position, long id) {
final Document listItemDocument = (Document) todoList.getItemAtPosition(position);
final int listItemIndex = position;
final CharSequence[] items = {"Delete Item"};
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setTitle("Perform Action");
builder.setItems(items, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int item) {
if(item == 0) {
try {
listItemDocument.delete();
} catch (Exception e) {
Log.e(TAG, "An error happened", e);
}
}
}
});
AlertDialog alert = builder.show();
return true;
}
});
}
public void addTodo(android.view.View view) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("New TODO Item");
final EditText input = new EditText(this);
builder.setView(input);
builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Map<String, Object> docContent = new HashMap<String, Object>();
docContent.put("todo", input.getText().toString());
Document document = couchbaseDatabase.createDocument();
try {
document.putProperties(docContent);
} catch (CouchbaseLiteException e) {
Log.e(TAG, "Cannot write document to database", e);
}
}
});
builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
});
builder.show();
}
}
A lot is happening in the above code so before I break it down, I’m going to go through the logic behind it. We essentially have four parts of Couchbase code, where two of those parts are doing the heavy lifting. The goal here is to first fetch all data when the application loads and then monitor for changes in data while the application is open. Changes in data include insert, update, and delete.
try {
this.couchbaseManager = new Manager(new AndroidContext(this), Manager.DEFAULT_OPTIONS);
this.couchbaseDatabase = this.couchbaseManager.getDatabase("mydb");
Query allDocumentsQuery = couchbaseDatabase.createAllDocumentsQuery();
QueryEnumerator queryResult = allDocumentsQuery.run();
for (Iterator<QueryRow> it = queryResult; it.hasNext(); ) {
QueryRow row = it.next();
todoArray.add(row.getDocument());
adapter.notifyDataSetChanged();
}
} catch (Exception e) {
Log.e(TAG, "An error happened", e);
return;
}
The above snippet of code will create a new Couchbase database if it doesn’t already exist and then attempt to fetch all documents in the database. All documents that have been fetched will be added to the list view for presenting to the screen.
This brings us to monitoring for changes:
if(this.couchbaseDatabase != null) {
this.couchbaseDatabase.addChangeListener(new Database.ChangeListener() {
public void changed(Database.ChangeEvent event) {
for(int i = 0; i < event.getChanges().size(); i++) {
Document retrievedDocument = couchbaseDatabase.getDocument(event.getChanges().get(i).getDocumentId());
if(retrievedDocument.isDeleted()) {
int documentIndex = adapter.getIndexOf(retrievedDocument.getId());
if(documentIndex > -1) {
todoArray.remove(documentIndex);
}
} else {
todoArray.add(retrievedDocument);
}
adapter.notifyDataSetChanged();
}
}
});
}
The above snippet will monitor the database for changes. If the change is a delete, then we will remove it from the list view, otherwise we will add it to the list view. Per the Couchbase documentation, it was recommended to use a LiveQuery, but due to a confirmed bug on GitHub, it was not possible at the time of writing this.
This brings us to the final important snippets that keeps this project ticking.
Inside the addTodo
method that gets called when clicking the floating action button you’ll see the following:
Map<String, Object> docContent = new HashMap<String, Object>();
docContent.put("todo", input.getText().toString());
Document document = couchbaseDatabase.createDocument();
try {
document.putProperties(docContent);
} catch (CouchbaseLiteException e) {
Log.e(TAG, "Cannot write document to database", e);
}
The above snippet will let you create a new document with a todo
property that has a value populated from a prompt. It will save it as soon as you call the Document’s putProperties
method.
Finally you’ll find this inside the list view’s long click event:
listItemDocument.delete();
Using that great getIndexOf
function we had created in our TodoAdapter
we were able to come up with the above Document and delete it. Pretty simple!
To build this project, from the command prompt or terminal, run the following:
./gradlew assemble
This will leave you with CouchbaseApp-debug.apk found in your project’s build/output/apk directory. It is now ready to be installed on your device.
We’ve chosen to sway from the defaults of Android and use a Couchbase NoSQL database for managing our data rather than the standard SQLite. Using Couchbase Lite we were able to easily add and remove items from our todo list with minimal effort.