Yavor Georgiev

Yavor is a PM at Snowflake working on developer experience. Previously at Docker, Auth0, Hulu, and Microsoft Azure.

Episode I: Working with typed and untyped data in the Mobile Services Android client

04 March 2013

To celebrate the Mobile Services Android launch, I’ve decided to put together a blog series over the next week to highlight some of the cool new features in our Android SDK. Here is the list of subjects I plan to cover:

Today, let’s focus on the data access aspect of the client, which lets us work with server entities in a typed or untyped way. Let’s start with a simple class that we want to store in a server-side table. We’re naming our class and properties to resemble the serialized JSON, we will cover how to customize the mapping between the properties and the serialized representation in a later post.

public class droid {
    public int id;
    public String name;
    public Float weight;
    public Integer numberOfLegs;
    public Integer numberOfWheels;
}

Note that a phantom menace lurks here: the class needs to contain a property called Id, ID, or id which will be populated by the server when it stores the entity.

To store instances of the class inside Mobile Services, we first need to create our Mobile Service in the Azure portal and then create a table called droid. Capitalization is important here. We then use the URL and application key provided in the portal to instantiate the MobileServiceClient.

MobileServiceClient client = new MobileServiceClient(
    "https://droid.azure-mobile.net/",
    "RrBQdeuaScZCIUsDSNnHPUyKowDDdW89",
    this);

The third parameter we pass here is the Activity or Service where the client is instantiated. We use that reference to grab the application context, which we use to store the installation ID for this application and also to display our authentication UI dialog.

Now we need to get a reference to the table object so we can insert data into it. Let’s look at the different overloads of the getTable method. 

public class MobileServiceClient {
    public MobileServiceJsonTable getTable(String name);
    public <E> MobileServiceTable<E> getTable(String name, Class<E> clazz);
    public <E> MobileServiceTable<E> getTable(Class<E> clazz);
}

The first overload is for working with our untyped JSON-based programming model, we will discuss that in a second. The second overload lets you work with a table if its name is different from the type name. The last overload assumes the class name and the table name are the same; this is the simplest one and that’s what we’ll use here.

MobileServiceTable<droid> table = client.getTable(droid.class);

We can then instantiate a new instance of the droid class and insert it into our server-side table.

droid r2d2 = new droid();
r2d2.name = "R2D2";
r2d2.numberOfLegs = 0;
r2d2.numberOfWheels = 2;
r2d2.weight = 432.21f;

table.insert(r2d2, new TableOperationCallback<droid>() {
    public void onCompleted(droid entity, Exception exception,
            ServiceFilterResponse response){
        if(exception == null){
            Log.i(TAG, "Object inserted with ID " + entity.id);
        }
    }
});

A few things about the above snippet:

  • You will notice the use of the onCompleted method of the TableOperationCallback<E> class. This is how we implement the asynchronous pattern that lets us make network requests without blocking the UI thread. If you want code to run “after” the results are inserted on the server, place the code inside that method.
  • Always check the exception property to make sure the operation was successful before you attempt to use the entity property.
  • The entity property will contain the newly inserted object. Note that the id property has been populated on the server.
  • The insert and update methods mutate the original object instance. In other words entity == r2d2 in the above example.
  • The response property gives you direct access to the HTTP response that came back on the server. More on why that’s useful in a subsequent post.

Note that by default mobile services have dynamic schematization turned on. That means that the objects inserted determine the columns that end up being created in the database. This is a handy feature to speed up development but you probably want to turn it off in production.

Updating an object is straightforward. 

r2d2.weight = 500f;
table.update(r2d2, new TableOperationCallback<droid>() {
    public void onCompleted(droid entity, Exception exception,
			ServiceFilterResponse response) {
		if(exception == null){
			Log.i(TAG, "Updated object with ID " + entity.id);
		}
	}
});

So is deleting. Notice the shape of the onCompleted method on the TableDeleteCallback class is slightly different because there is no object to return. 

table.delete(r2d2, new TableDeleteCallback() {
    public void onCompleted(Exception exception, 
            ServiceFilterResponse response) {
        if(exception == null){
            Log.i(TAG, "Object deleted");
        }
    }
});

// You can also use the ID directly
// table.delete(r2d2.id, ...);

We will look at reading in a subsequent post, but for now let’s stick to a very simple example. This first snippet reads an object by ID.

table.lookUp(1, new TableOperationCallback<droid>() {
    public void onCompleted(droid entity, Exception exception,
            ServiceFilterResponse response) {
        if(exception == null){
            Log.i(TAG, "Read object with ID " + entity.id);    
        }
    }
});

This second example will load up all server objects (a “select *” query). We will take a look at the details of the query model in a subsequent post.

table.execute(new TableQueryCallback<droid>(){
    public void onCompleted(List<droid> result, int count, Exception exception,
            ServiceFilterResponse response) {
        if(exception == null){
            for (droid d : result) {
                Log.i(TAG, "Read object with ID " + d.id);	
            }
        }	
    }
});

Now let’s take a look at the untyped model. Frequently having a client-side type to represent server-side data is very handy for debugging and data-binding purposes. But sometimes creating types is just cumbersome and you want to simply treat your entities as dictionaries on the client. 

The key here is to get an instance of the untyped MobileServiceJsonTable by calling the right getTable overload on MobileServiceClient.

MobileServiceJsonTable untypedTable = client.getTable("droid");

Note that you need to specify the table name here since we are not supplying a type to deserialize into, so the client has no way of knowing what table name to expect.

Our untyped model relies on the gson serialization library, so we will use its JsonObject type to build up our object.The following snippet covers the same scenarios as shown above in the typed case. One difference in behavior here is that the original objects are not mutated in the insert/update case.

JsonObject c3po = new JsonObject();
c3po.addProperty("name", "C3PO");
c3po.addProperty("numberOfLegs", 2);
c3po.addProperty("numberOfWheels", 0);
c3po.addProperty("weight", 300f);

untypedTable.insert(c3po, new TableJsonOperationCallback() {
    public void onCompleted(JsonObject jsonObject, Exception exception,
            ServiceFilterResponse response) {
        if(exception == null){
            Log.i(TAG, "Object inserted with ID " + 
        jsonObject.getAsJsonPrimitive("id").getAsInt());
        }
    }
});

// In the JSON case, we don't touch the original object, so the
// id does not get added and we need to add it ourselves
c3po.addProperty("id", 8);
c3po.addProperty("weight", 200f);
untypedTable.update(c3po, new TableJsonOperationCallback() {
    public void onCompleted(JsonObject jsonObject, Exception exception,
            ServiceFilterResponse response) {
        if(exception == null){
            Log.i(TAG, "Updated object with ID " + 
        jsonObject.getAsJsonPrimitive("id").getAsInt());
        }
    }
});

untypedTable.delete(c3po, new TableDeleteCallback() {
    public void onCompleted(Exception exception, ServiceFilterResponse response) {
        if(exception == null){
            Log.i(TAG, "Object deleted");
        }
    }
});

// You can also use the ID directly
// untypedTable.delete(c3po.getAsJsonPrimitive("id").getAsInt(), ...)

untypedTable.lookUp(1, new TableJsonOperationCallback() {
    public void onCompleted(JsonObject jsonObject, Exception exception,
            ServiceFilterResponse response) {	
        if(exception == null){
            Log.i(TAG, "Read object with ID " + 
        jsonObject.getAsJsonPrimitive("id").getAsInt());	
        }
    }
});

untypedTable.execute(new TableJsonQueryCallback() {
    public void onCompleted(JsonElement result, int count, Exception exception,
            ServiceFilterResponse response) {
        if(exception == null){
            JsonArray results = result.getAsJsonArray();
            for(JsonElement item : results){
                Log.i(TAG, "Read object with ID " + 
            item.getAsJsonObject().getAsJsonPrimitive("id").getAsInt());
            }
        }
    }
});

That is all for today. In the next episode, we look at how to customize serialization with the power of the gson library.