Zum Hauptinhalt wechseln

 Subscribe

In my last post, I talked about the new (alpha) version of the Android SDK for Azure Mobile Services, which introduced support for futures. In that post I briefly mentioned that we also added offline support so that now the Android SDK is on feature parity with the managed and iOS ones. In this post we’ll go deeper into the offline support, by walking through the steps required to make our Todo app offline-enabled, talking about each feature as its needed in the app.

TL;DR: the post shows how to update the quickstart application downloaded from the Azure portal to make it offline-enabled, using the features released in the new version of the Azure Mobile Services Android SDK. You can also see the full code for the example in this post in our samples repository and go through the SDK sources in the Android branch of our main repository.

Initial setup

To start off the walkthough, let’s use the quickstart application that you can download in the portal. Create a new mobile service. For this example, let’s use the node.js backend which we can setup without needing to use Visual Studio.

001-CreateMobileService

Once the service is created, choose the Android platform, then select “Create TodoItem Table” and then download the starter project to your computer.

002-DownloadTodoApp

Open the project in Eclipse and we’re ready to start.

Updating the quickstart app

The offline support we’re previewing here has some nice features. One of them is the ability to resolve conflicts which can happen when pushing the local changes to the table in Azure. For example, if you’re running the same app in two phones, and changed the same item in both phones locally, when you’re ready to push the changes back to the server one of them will fail with a conflict. The SDK allows you to deal with those conflicts via code, and decide what to do with the item which has a conflict.

The current quickstart, however, doesn’t really have many occasions where conflicts can arise, since the only action we can take for an item is to mark it as complete. True, we can mark the item as complete in one client and then do the same in another client, but although technically this is a conflict (and the framework will flag it as such) it’s not too interesting. Let’s then change the quickstart to make it more interesting by allowing full editing of the todo items.

In the code you can download you’ll see that I’ve updated the app to allow for editing the items. If you want to follow the same steps I did for that, you can go to the appendix at the end of this post.

Updating the SDK

The first thing we’ll need to do to add offline support in our application is to get a version of the Mobile Services Android SDK which supports it. Since we’re launching it as a preview feature, it won’t be in the official download location. For now you can go to https://aka.ms/Iajk6q and download it locally. Once it’s downloaded, extract the files mobileservices-2.0.0-alpha.jar and guava-17.0.jar and copy those to the libs folder of your project; remove the old version of the SDK (as of the writing of this blog, mobileservices-1.1.5.jar) and refresh the contents of the libs folder in Eclipse.

Your project now will have lots of errors – caused by the breaking changes listed in my previous post. They’re mostly on the ToDoActivity.java, but most of them are related to classes which moved to different packages. After selecting the file in Eclipse, select “Organize Imports” on the “Source” menu (Ctrl+Shift+O) and most of them will be fixed. To fix the remaining ones: add the following import before the class declaration:

import static com.microsoft.windowsazure.mobileservices.table.query.QueryOperations.*;

And update the service filter used to indicate progress to comply with the new contract for the interface, using futures:

private class ProgressFilter implements ServiceFilter {

    @Override
    public ListenableFuture handleRequest(
            ServiceFilterRequest request, NextServiceFilterCallback next) {

        runOnUiThread(new Runnable() {

            @Override
            public void run() {
                if (mProgressBar != null) mProgressBar.setVisibility(ProgressBar.VISIBLE);
            }
        });

        ListenableFuture result = next.onNext(request);

        Futures.addCallback(result, new FutureCallback() {
            @Override
            public void onFailure(Throwable exc) {
                dismissProgressBar();
            }

            @Override
            public void onSuccess(ServiceFilterResponse resp) {
                dismissProgressBar();
            }

            private void dismissProgressBar() {
                runOnUiThread(new Runnable() {

                    @Override
                    public void run() {
                        if (mProgressBar != null) mProgressBar.setVisibility(ProgressBar.GONE);
                    }
                });
            }
        });

        return result;
    }
}

The app now uses the latest SDK, and should now continue working just as it did before.

Update mobile service calls to use the new (futures-based) API

One more step which we should do before enabling offline per se is to change the table operations to use the new, futures-based APIs introduced in this release – the classes used for offline don’t have the callback-based methods, so it will be easier to transition from a fully-connected to an occasionally connected application. What we need to do is to start up any actions which may involve the operation to a background thread, and once we get the operation result, if we need to change any UI component we need to post the call back to the main (UI) thread. For example, the addItem method would be rewritten as:

public void addItem(View view) {
    if (mClient == null) {
        return;
    }

    // Create a new item
    final ToDoItem item = new ToDoItem();

    item.setText(mTextNewToDo.getText().toString());
    item.setComplete(false);

    // Insert the new item
    new AsyncTask() {

        @Override
        protected Void doInBackground(Void... params) {
            try {
                mToDoTable.insert(item).get();
                if (!item.isComplete()) {
                    runOnUiThread(new Runnable() {
                        public void run() {
                            mAdapter.add(item);
                        }
                    });
                }
            } catch (Exception exception) {
                createAndShowDialog(exception, "Error");
            }
            return null;
        }
    }.execute();

    mTextNewToDo.setText("");
}

The update method is similar:

private void updateItem(final ToDoItem item) {
    if (mClient == null) {
        return;
    }

    new AsyncTask() {

        @Override
        protected Void doInBackground(Void... params) {
            try {
                mToDoTable.update(item).get();
                runOnUiThread(new Runnable() {
                    public void run() {
                        if (item.isComplete()) {
                            mAdapter.remove(item);
                        }
                        refreshItemsFromTable();
                    }
                });
            } catch (Exception exception) {
                createAndShowDialog(exception, "Error");
            }
            return null;
        }
    }.execute();
}

Finally when we want to retrieve items from the table we need to do the same.

private void refreshItemsFromTable() {

    // Get the items that weren't marked as completed and add them in the
    // adapter
    new AsyncTask() {

        @Override
        protected Void doInBackground(Void... params) {
            try {
                final MobileServiceList result = mToDoTable.where().field("complete").eq(false).execute().get();
                runOnUiThread(new Runnable() {

                    @Override
                    public void run() {
                        mAdapter.clear();

                        for (ToDoItem item : result) {
                            mAdapter.add(item);
                        }
                    }
                });
            } catch (Exception exception) {
                createAndShowDialog(exception, "Error");
            }
            return null;
        }
    }.execute();
}

At this point you should be able to run the application again, and it should continue to work just as it did before.

From tables to sync tables

We’re now ready to make the app offline-capable. The first thing we need to do is to change the class we use to access our tables: instead of using the MobileServiceTable or MobileServiceJsonTable, we’ll use the new MobileServiceSyncTable or MobileServiceJsonSyncTable classes. As in the other platforms, a sync table is basically a local table that “knows” how to push changes made locally to its corresponding “remote” table, as well as “pull” items from the remote table locally. The tracking of the changes is made via a synchronization context, which needs to be initialized with a local store that is used to store the items locally. The Mobile Services SDK provides an implementation of the store based on a SQLite database, and we’ll use it for this post.

Let’s first handle the initialization of the local store. In the onCreate method, add the following lines after creating the MobileServiceClient instance:

SQLiteLocalStore localStore = new SQLiteLocalStore(mClient.getContext(), "ToDoItem", null, 1);
SimpleSyncHandler handler = new SimpleSyncHandler();
MobileServiceSyncContext syncContext = mClient.getSyncContext();

Map tableDefinition = new HashMap();
tableDefinition.put("id", ColumnDataType.String);
tableDefinition.put("text", ColumnDataType.String);
tableDefinition.put("complete", ColumnDataType.Boolean);

localStore.defineTable("ToDoItem", tableDefinition);
syncContext.initialize(localStore, handler).get();

The last couple of lines may throw exceptions when accessing the local table (defining it in the store) or initializing the context, so we should update the exception handler to deal with those as well.

} catch (MalformedURLException e) {
    createAndShowDialog(new Exception("There was an error creating the Mobile Service. Verify the URL"), "Error");
} catch (Exception e) {
    Throwable t = e;
    while (t.getCause() != null) {
        t = t.getCause();
    }
    createAndShowDialog(new Exception("Unknown error: " + t.getMessage()), "Error");
}

Now that the context is initialized, change the type of the mToDoTable field in the main activity from MobileServiceTable or MobileServiceSyncTable. In the onCreate method, change the initialization of that field to use the getSyncTable method. There is another thing which we need to change. When reading from a local table, we need to pass a query to it, and the we can get the query object from a “regular” table. So define a new private field:

/**
 * The query used to pull data from the remote server
 */
private Query mPullQuery;

Initialize it in the onCreate method:

// Saves the query which will be used for reading data
mPullQuery = mClient.getTable(ToDoItem.class).where().field("complete").eq(false);

And update the code on the refreshItemsFromTable method to use that query:

final MobileServiceList result = mToDoTable.read(mPullQuery).get();

You can now run the app and it will be running completely offline.

Pulling and pushing

If you did run the app before, you may have noticed something strange: any items which were showing before in the app before will not appear in the list anymore. The issue is that we’re now talking to the local (sync) table, but we’re not doing any synchronization with the server side, so if this is the first time you run this code, it will be running against an empty (local) table. What we need to do is to somehow tell the sync table to pull existing data from the server, and also push any changes made locally to the server side.

The quickstart app has a good place where we can implement that logic – the refresh menu item (which is located on the top-right corner of the app). Other apps may have different requirements regarding when data needs to be synchronized, but for demo purposes I’ll have the user explicitly request it. On the onOptionsItemSelected method, start a new background task and at that location first push all the changes made to the local store (by using the synchronization context) and later pull all data that you want to come to the local table (via the sync table).

public boolean onOptionsItemSelected(MenuItem item) {
    if (item.getItemId() == R.id.menu_refresh) {
        new AsyncTask() {

            @Override
            protected Void doInBackground(Void... params) {
                try {
                    mClient.getSyncContext().push().get();
                    mToDoTable.pull(mPullQuery).get();
                    refreshItemsFromTable();
                } catch (Exception exception) {
                    createAndShowDialog(exception, "Error");
                }
                return null;
            }

        }.execute();
    }

    return true;
}

Now if you run the app and tap the refresh button, you should see all the items from the server. At that point you should be able to turn off the networking from the device, continue making changes – the app will work just fine. When it’s time to sync the changes to the server, turn the network back on, and tap the ‘refresh’ button again.

One thing which is important to point out: if there are pending changes in the local store, a pull operation will first push those changes to the server (so that if there are changes in the same row, the push operation will fail and the application has an opportunity to handle the conflicts appropriately). That means that the push call in the code above isn’t necessarily required, but I think it’s always a good practice to be explicit about what the code is doing.

Handling conflicts

Well, offline handling of data is great, but what happens if there is a conflict when a push operation is being executed? As the app is written until now, since the ToDoItem class doesn’t store a version column, it will ignore any prior changes in the server data and overwrite the server data (basically, a “client-wins” conflict resolution policy). But if we can be smarter than that. Let’s first add the version so that optimistic concurrency is enforced and updates become conditional to the item version. In the ToDoItem class add the following members:

/**
 * The version of the item in the database
 */
@com.google.gson.annotations.SerializedName("__version")
private String mVersion;

/**
 * Gets the version of the item in the database
 *
 * @return the version of the item in the database
 */
public String getVersion() {
    return mVersion;
}

/**
 * Sets the version of the item in the database
 *
 * @param mVersion the version of the item in the database
 */
public void setVersion(String mVersion) {
    this.mVersion = mVersion;
}

And add a new column when defining the local table the onCreate method

tableDefinition.put("__version", ColumnDataType.String);

Now, if you use a tool such as Fiddler or Postmon to edit an item and edit the same item on the application (or edit the same item in two different devices / emulators), when you try to push you’ll get an error. But we can deal with that error, and implement our own conflict resolution policy, by implementing a custom sync handler, as shown below.

private class ConflictResolvingSyncHandler implements MobileServiceSyncHandler {

    @Override
    public JsonObject executeTableOperation(
            RemoteTableOperationProcessor processor, TableOperation operation)
            throws MobileServiceSyncHandlerException {

        MobileServicePreconditionFailedExceptionBase ex = null;
        JsonObject result = null;
        try {
            result = operation.accept(processor);
        } catch (MobileServicePreconditionFailedExceptionBase e) {
            ex = e;
        } catch (Throwable e) {
            ex = (MobileServicePreconditionFailedExceptionBase) e.getCause();
        }

        if (ex != null) {
            // A conflict was detected; let's force the server to "win"
            // by discarding the client version of the item
            // Other policies could be used, such as prompt the user for
            // which version to maintain.
            JsonObject serverItem = ex.getValue();

            if (serverItem == null) {
                // Item not returned in the exception, retrieving it from the server
                try {
                    serverItem = mClient.getTable(operation.getTableName()).lookUp(operation.getItemId()).get();
                } catch (Exception e) {
                    throw new MobileServiceSyncHandlerException(e);
                }
            }

            result = serverItem;
        }

        return result;
    }

    @Override
    public void onPushComplete(MobileServicePushCompletionResult result)
            throws MobileServiceSyncHandlerException {
    }
}

Now if you run the app and there are conflicts, they will be automatically handled.

Wrapping up

We’re now adding offline support for native iOS applications, and like in the managed SDK, we’re releasing it in a preview format. We really appreciate your feedback so we can continue improving in the SDKs for Azure Mobile Services. As usual, please leave comments / suggestions / questions in this post, in our MSDN Forum or via twitter @AzureMobile.

If you want to download the code used in this post, you can get it in the mobile services samples repository in GitHub, under our samples repository. And you can get the Azure Mobile Service SDK for Android version 2.0 alpha at https://aka.ms/Iajk6q. And if you are one of those brave souls, you can go over the full client SDK in the Android branch of the azure-mobile-services repository.

Appendix: updates to the ToDoItem project to make it ready for the post

Here are the steps needed to change the existing ToDo list app downloaded from the portal to make the items editable.

Adding a new activity

We’ll handle the editing of the items in a new screen, so let’s add a new activity to the project. Right-click the project icon in Eclipse, select “New” –> “Other”, then expend the Android node and select “Android Activity” –> “Empty Activity”.

A001-CreateNewActivity

Name the new activity EditToDoActivity then click finish.

A002-NameNewActivity

The activity is now ready to be configured. Let’s open the strings file (res/values/strings.xml) and make those changes to prevent localization warnings:

  • Remove the “hello_world” string value
  • Change the value of “title_activity_edit_to_do” from “EditToDoActivity” to “Edit ToDo item”
  • Add the following new strings:
    • Item
    • Complete
    • Done

Now open the new activity layout, remove the text view which comes by default, then add the following controls: an edit text (@+id/textBoxEditItem, hint:@string/title_activity_edit_to_do) that contains the text of the item to be edited; a checkbox (@+id/checkBoxItemComplete; text:@string/label_complete) that contains the complete status of the item; and a button (@+id/buttonDoneEditing; text:@string/button_item_edit_done) to signal the end of the editing. The image below shows how we implemented that layout. We also added an optional text view with the label for the item text (text:@string/label_item_text) to make it a little more user-friendly.

A003-NewActivityLayout

Now change the row template for the table (res/layout/row_list_to_do.xml) by removing the existing checkbox, and replacing it with a text view (@+id/todoItemText) instead. In the main activity we’ll only display the items, and as they’re clicked we’ll transition to the new activity to allow it to be edited.

On to the code now. Open the file EditToDoActivity.java (under src/com.example.your-project-name) and replace its contents with the code below.

public class EditToDoActivity extends Activity {

    protected static final String ITEM_TEXT_KEY = "com.example.blog20140807.item_text";
    protected static final String ITEM_COMPLETE_KEY = "com.example.blog20140807.item_complete";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_edit_to_do);

        Bundle extras = getIntent().getExtras();
        String itemText = extras.getString(ITEM_TEXT_KEY);
        boolean itemComplete = extras.getBoolean(ITEM_COMPLETE_KEY);

        final EditText itemTextBox = (EditText)findViewById(R.id.textBoxEditItem);
        itemTextBox.setText(itemText);
        final CheckBox completeCheckbox = (CheckBox)findViewById(R.id.checkBoxItemComplete);
        completeCheckbox.setChecked(itemComplete);

        Button btnDone = (Button)findViewById(R.id.buttonDoneEditing);
        btnDone.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                Intent i = new Intent();
                i.putExtra(ITEM_TEXT_KEY, itemTextBox.getText().toString());
                i.putExtra(ITEM_COMPLETE_KEY, completeCheckbox.isChecked());
                setResult(RESULT_OK, i);
                finish();
            }
        });
    }
}

Updating the code to use the new activity

Now open the adapter code (src/com.example.your-project-name/ToDoItemAdapter.java) and replace the getView implementation with the one below:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    View row = convertView;

    final ToDoItem currentItem = getItem(position);

    if (row == null) {
        LayoutInflater inflater = ((Activity) mContext).getLayoutInflater();
        row = inflater.inflate(mLayoutResourceId, parent, false);
    }

    row.setTag(currentItem);
    final TextView textView = (TextView) row.findViewById(R.id.todoItemText);
    textView.setText(currentItem.getText());

    return row;
}

Let’s move on to the main activity (src/com.example.your-project-name/ToDoActivity.java). First, remove the public checkItem method and replace it with the new updateItem method shown below (since we can now make changes other than just marking items as complete).

private void updateItem(ToDoItem item) {
    if (mClient == null) {
        return;
    }

    mToDoTable.update(item, new TableOperationCallback() {

        public void onCompleted(ToDoItem entity, Exception exception, ServiceFilterResponse response) {
            if (exception == null) {
                if (entity.isComplete()) {
                    mAdapter.remove(entity);
                }

                refreshItemsFromTable();
            } else {
                createAndShowDialog(exception, "Error");
            }
        }

    });
}

Now, let’s add the following fields to the class – one to track the item which is being edited, and one to tag the edit activity.

/**
 * The position of the item which is being edited
 */
private int mEditedItemPosition = -1;

private static final int EDIT_ACTIVITY_REQUEST_CODE = 1234;

And we that we can start with the activity jumping code. To move to the edit activity, change the code in the onCreate method to add an OnItemClickListener to the list which displays the items. On the handler, create a new Intent, add the information about the item being edited (which will be retrieved in the other activity), and start that activity.

// Create an adapter to bind the items with the view
mAdapter = new ToDoItemAdapter(this, R.layout.row_list_to_do);
ListView listViewToDo = (ListView) findViewById(R.id.listViewToDo);
final ListView listViewToDo = (ListView) findViewById(R.id.listViewToDo);
listViewToDo.setAdapter(mAdapter);

listViewToDo.setOnItemClickListener(new OnItemClickListener() {

    @Override
    public void onItemClick(AdapterView parent, View view,
            int position, long id) {
        Intent i = new Intent(getApplicationContext(), EditToDoActivity.class);
        mEditedItemPosition = position;
        ToDoItem item = mAdapter.getItem(position);
        i.putExtra(EditToDoActivity.ITEM_COMPLETE_KEY, item.isComplete());
        i.putExtra(EditToDoActivity.ITEM_TEXT_KEY, item.getText());
        startActivityForResult(i, EDIT_ACTIVITY_REQUEST_CODE);
    }
});

// Load the items from the Mobile Service
refreshItemsFromTable();

Finally, override the onActivityResult method, and on the implementation, if the result came from the new activity, check whether there was any changes in the item, and if so, call the updateItem method to persist the changes.

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
    if (requestCode == EDIT_ACTIVITY_REQUEST_CODE && resultCode == RESULT_OK && mEditedItemPosition >= 0) {
        ToDoItem item = mAdapter.getItem(mEditedItemPosition);
        String text = intent.getExtras().getString(EditToDoActivity.ITEM_TEXT_KEY);
        boolean complete = intent.getExtras().getBoolean(EditToDoActivity.ITEM_COMPLETE_KEY);

        if (!item.getText().equals(text) || item.isComplete() != complete) {
            item.setText(text);
            item.setComplete(complete);
            updateItem(item);
        }
    }
}

And we’re done with the preparation. The quickstart has been updated to allow edits, and it’s ready to have the offline support added to it as described in this post.

  • Explore

     

    Let us know what you think of Azure and what you would like to see in the future.

     

    Provide feedback

  • Build your cloud computing and Azure skills with free courses by Microsoft Learn.

     

    Explore Azure learning


Join the conversation