In the previous article we got basic cloud save and restore working. In this concluding article in the series we’ll look at handling conflicts in our data.
Conflicts occur when multiple instances of our app make changes to the cloud data at the same time, or ver close together. Those reading who are familiar with using Source Code Management systems within a team will be more than familiar with conflicts, and how they can be awkward to resolve.
Let’s first look at how we detect conflicts. In the previous article we implemented the OnStateLoadedListener and there was a method named onStateConflict which we left empty. Unsurprisingly this is where we need to handle conflict resolution. This method will be called when we attempt to call updateState() or updateStateImmediate() and the cloud data has changed sine we last called loadState() (or we last updated successfully).
onStateConflict gets called with four arguments: The key which represents which of the four 128KB slots that we attempted to save to, a String representing the version of the cloud data (we’ll need to use this to mark the conflict as resolved), a copy of the local data that we tried to store, and a copy of the current version of the cloud data.
The version string is fairly important here because is protects us against the situation where we resolve the conflict, and then try and update the cloud data accordingly, but it has changed again!
Unfortunately there are no hard and fast rules for conflict resolution, as different applications will have different requirements, and may have to take differing approaches to conflict resolution.
The key thing to remember is that the app instance which attempts to perform updateState will receive the callback if the local version is out of sync with the cloud data. This can often be avoided by performing a loadState before attempting to modify the state. For our app the update is triggered by user interaction, so we can assume that the user wants the cloud data to be what has just been entered, so we just need to mark it as resolved with the current local data:
@Override public void onStateConflict( int i, String s, byte[] bytes, byte[] bytes2) { appStateClient.resolveState(this, i, s, bytes); }
We can prove that this works by entering “Conflict” on one device and saving. Then on a second device we enter “Resolved” (without first doing a load). Then on the first device do a load, and we’ll see that the text changes to “Resolved”. Before we added our resolution code, the cloud data would have remained at “Conflict”.
When you have rather more complex data being persisted, it may be worth considering splitting it up in to logical units and persisting these to different slots. For example if your data consists of two items, one of which will be updated frequently and the other which will be updated much less frequently, it could be beneficial to save these to separate slots. WIth the complexity of the data reduced, it will make the conflicts easier to resolve.
The official documentation gives an example of a game where the level reached and score are persisted together, but explains what can happen if the user improves the score without advancing a level on one device, and then advances the level without improving the score on another. Persisting these together means that some logic is required to merge to two sets of data in order to resolve the conflict. However, if we persisted these separately in different slots, the logic becomes much simpler as we only need to determine the maximum of either the level or the score independently of the other value. Obviously this is a very simple example, but goes to show that understanding your data, and partitioning it intelligently can simplify the logic somewhat when it comes to conflict resolution.
That concludes this series on Cloud Save. The basics are pretty easy to get working, and it works very well. The only area of complexity can be when handling conflict resolution, but with some careful planning and a good understanding of the data being persisted this can be fairly easy to resolve.
The source code for this article can be found here.
© 2013 – 2014, Mark Allison. All rights reserved.
Copyright © 2013 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.