5. public class DishService {
public static boolean hasCachedDishList() {
return Storage.getInstance().exists("DishListCache");
}
public static void loadDishesFromCache() {
JSONParser p = new JSONParser();
try {
Map<String, Object> m = p.parseJSON(new InputStreamReader(
Storage.getInstance().createInputStream("DishListCache"), "UTF-8"));
List<Object> l = (List<Object>)m.get("dishes");
Restaurant.getInstance().menu.get().dishes.clear();
for(Object o : l) {
Dish d = new Dish();
d.getPropertyIndex().populateFromMap((Map)o);
Restaurant.getInstance().menu.get().dishes.add(d);
}
List<String> cats = (List<String>)m.get("categories");
Restaurant.getInstance().menu.get().categories.clear();
for(String str : cats) {
Restaurant.getInstance().menu.get().categories.add(str);
}
if(!Restaurant.getInstance().menu.get().dishDownloadFinished.get()) {
Restaurant.getInstance().menu.get().dishDownloadFinished.set(Boolean.TRUE);
}
} catch(IOException err) {
Log.e(err);
ToastBar.showErrorMessage("Error loading list of dishes: " + err);
Log.sendLog();
DishService (Client)
Lets get started with the first item on the agenda the network layer and I’ll do this by creating an abstraction of the server communication. Despite skipping the server
code most of this code should be pretty clear as we go over it.
6. public class DishService {
public static boolean hasCachedDishList() {
return Storage.getInstance().exists("DishListCache");
}
public static void loadDishesFromCache() {
JSONParser p = new JSONParser();
try {
Map<String, Object> m = p.parseJSON(new InputStreamReader(
Storage.getInstance().createInputStream("DishListCache"), "UTF-8"));
List<Object> l = (List<Object>)m.get("dishes");
Restaurant.getInstance().menu.get().dishes.clear();
for(Object o : l) {
Dish d = new Dish();
d.getPropertyIndex().populateFromMap((Map)o);
Restaurant.getInstance().menu.get().dishes.add(d);
}
List<String> cats = (List<String>)m.get("categories");
Restaurant.getInstance().menu.get().categories.clear();
for(String str : cats) {
Restaurant.getInstance().menu.get().categories.add(str);
}
if(!Restaurant.getInstance().menu.get().dishDownloadFinished.get()) {
Restaurant.getInstance().menu.get().dishDownloadFinished.set(Boolean.TRUE);
}
} catch(IOException err) {
Log.e(err);
ToastBar.showErrorMessage("Error loading list of dishes: " + err);
Log.sendLog();
DishService (Client)
The dish service implements caching for the dish list we fetch from the server to make sure the app loads instantly. If we fetch new data we will show it later as it’s more
important that the app loads quickly. Here we check whether cache even exists, this is important for first time activation.
7. public class DishService {
public static boolean hasCachedDishList() {
return Storage.getInstance().exists("DishListCache");
}
public static void loadDishesFromCache() {
JSONParser p = new JSONParser();
try {
Map<String, Object> m = p.parseJSON(new InputStreamReader(
Storage.getInstance().createInputStream("DishListCache"), "UTF-8"));
List<Object> l = (List<Object>)m.get("dishes");
Restaurant.getInstance().menu.get().dishes.clear();
for(Object o : l) {
Dish d = new Dish();
d.getPropertyIndex().populateFromMap((Map)o);
Restaurant.getInstance().menu.get().dishes.add(d);
}
List<String> cats = (List<String>)m.get("categories");
Restaurant.getInstance().menu.get().categories.clear();
for(String str : cats) {
Restaurant.getInstance().menu.get().categories.add(str);
}
if(!Restaurant.getInstance().menu.get().dishDownloadFinished.get()) {
Restaurant.getInstance().menu.get().dishDownloadFinished.set(Boolean.TRUE);
}
} catch(IOException err) {
Log.e(err);
ToastBar.showErrorMessage("Error loading list of dishes: " + err);
Log.sendLog();
DishService (Client)
Assuming cache exists we can just load the data from the cache into the global restaurant object. The cache has the JSON as it was returned from the server so we can
parse it using the standard parsing code.
This is convenient for debugging as we can inspect the json file in the filesystem for the simulator and see what’s saved locally
8. public class DishService {
public static boolean hasCachedDishList() {
return Storage.getInstance().exists("DishListCache");
}
public static void loadDishesFromCache() {
JSONParser p = new JSONParser();
try {
Map<String, Object> m = p.parseJSON(new InputStreamReader(
Storage.getInstance().createInputStream("DishListCache"), "UTF-8"));
List<Object> l = (List<Object>)m.get("dishes");
Restaurant.getInstance().menu.get().dishes.clear();
for(Object o : l) {
Dish d = new Dish();
d.getPropertyIndex().populateFromMap((Map)o);
Restaurant.getInstance().menu.get().dishes.add(d);
}
List<String> cats = (List<String>)m.get("categories");
Restaurant.getInstance().menu.get().categories.clear();
for(String str : cats) {
Restaurant.getInstance().menu.get().categories.add(str);
}
if(!Restaurant.getInstance().menu.get().dishDownloadFinished.get()) {
Restaurant.getInstance().menu.get().dishDownloadFinished.set(Boolean.TRUE);
}
} catch(IOException err) {
Log.e(err);
ToastBar.showErrorMessage("Error loading list of dishes: " + err);
Log.sendLog();
DishService (Client)
The JSON file contains a map object which contains two arrays dishes and categories. The dishes array contains all the information in a dish and every entry within this
map has the same key value pairs as the Dish object properties. One of the features of property objects is the ability to populate a property from a Map object or from
JSON directly and this is exactly the case for which this is useful
9. public class DishService {
public static boolean hasCachedDishList() {
return Storage.getInstance().exists("DishListCache");
}
public static void loadDishesFromCache() {
JSONParser p = new JSONParser();
try {
Map<String, Object> m = p.parseJSON(new InputStreamReader(
Storage.getInstance().createInputStream("DishListCache"), "UTF-8"));
List<Object> l = (List<Object>)m.get("dishes");
Restaurant.getInstance().menu.get().dishes.clear();
for(Object o : l) {
Dish d = new Dish();
d.getPropertyIndex().populateFromMap((Map)o);
Restaurant.getInstance().menu.get().dishes.add(d);
}
List<String> cats = (List<String>)m.get("categories");
Restaurant.getInstance().menu.get().categories.clear();
for(String str : cats) {
Restaurant.getInstance().menu.get().categories.add(str);
}
if(!Restaurant.getInstance().menu.get().dishDownloadFinished.get()) {
Restaurant.getInstance().menu.get().dishDownloadFinished.set(Boolean.TRUE);
}
} catch(IOException err) {
Log.e(err);
ToastBar.showErrorMessage("Error loading list of dishes: " + err);
Log.sendLog();
DishService (Client)
The categories are just string names of all the available categories. We could probably just go over the dishes and extract the categories from there though.
Once we add the categories we set the boolean flag indicating the download was finished, this is important as external code might have a listener bound to the property
so it can refresh the UI once the data finished loading.
10. public static void updateDishesFromServerSync() {
NetworkManager.getInstance().addToQueueAndWait(updateDishesFromServer());
}
public static void updateDishesFromServerAsync() {
NetworkManager.getInstance().addToQueue(updateDishesFromServer());
}
private static ConnectionRequest updateDishesFromServer() {
ConnectionRequest cr = new ConnectionRequest(MyRestaurant.SERVER_URL + "dish", false) {
private boolean autoLoad;
protected void readResponse(InputStream input) throws IOException {
JSONParser p = new JSONParser();
Map<String, Object> m = p.parseJSON(new InputStreamReader(input, "UTF-8"));
String stamp = (String)m.get("stamp");
if(stamp != null) {
Preferences.set("lastTimeStamp", stamp);
OutputStream o = Storage.getInstance().createOutputStream("DishListCache");
o.write(Result.fromContent(m).toString().getBytes());
o.close();
autoLoad = true;
}
}
protected void postResponse() {
if(autoLoad) {
loadDishesFromCache();
}
}
};
cr.addArgument("stamp", Preferences.get("lastTimeStamp", "0"));
cr.addArgument("appId", MyRestaurant.APP_AUTH);
return cr;
DishService (Client)
Moving on we have two separate methods to update the dishes. They are both identical and just use the addToQueueAndWait or addToQueue.
The reason we need both is simple, we use addToQueue when the app launches as we want to launch right away and don’t want to wait for the data. However, pull to
refresh needs to block until the data is here and in that case we need to use addToQueueAndWait.
11. public static void updateDishesFromServerSync() {
NetworkManager.getInstance().addToQueueAndWait(updateDishesFromServer());
}
public static void updateDishesFromServerAsync() {
NetworkManager.getInstance().addToQueue(updateDishesFromServer());
}
private static ConnectionRequest updateDishesFromServer() {
ConnectionRequest cr = new ConnectionRequest(MyRestaurant.SERVER_URL + "dish", false) {
private boolean autoLoad;
protected void readResponse(InputStream input) throws IOException {
JSONParser p = new JSONParser();
Map<String, Object> m = p.parseJSON(new InputStreamReader(input, "UTF-8"));
String stamp = (String)m.get("stamp");
if(stamp != null) {
Preferences.set("lastTimeStamp", stamp);
OutputStream o = Storage.getInstance().createOutputStream("DishListCache");
o.write(Result.fromContent(m).toString().getBytes());
o.close();
autoLoad = true;
}
}
protected void postResponse() {
if(autoLoad) {
loadDishesFromCache();
}
}
};
cr.addArgument("stamp", Preferences.get("lastTimeStamp", "0"));
cr.addArgument("appId", MyRestaurant.APP_AUTH);
return cr;
DishService (Client)
The aptly named update dishes from server is the common code for both update methods, it creates a connection request to the dish URL that performs a GET HTTP
method on the server.
12. public static void updateDishesFromServerSync() {
NetworkManager.getInstance().addToQueueAndWait(updateDishesFromServer());
}
public static void updateDishesFromServerAsync() {
NetworkManager.getInstance().addToQueue(updateDishesFromServer());
}
private static ConnectionRequest updateDishesFromServer() {
ConnectionRequest cr = new ConnectionRequest(MyRestaurant.SERVER_URL + "dish", false) {
private boolean autoLoad;
protected void readResponse(InputStream input) throws IOException {
JSONParser p = new JSONParser();
Map<String, Object> m = p.parseJSON(new InputStreamReader(input, "UTF-8"));
String stamp = (String)m.get("stamp");
if(stamp != null) {
Preferences.set("lastTimeStamp", stamp);
OutputStream o = Storage.getInstance().createOutputStream("DishListCache");
o.write(Result.fromContent(m).toString().getBytes());
o.close();
autoLoad = true;
}
}
protected void postResponse() {
if(autoLoad) {
loadDishesFromCache();
}
}
};
cr.addArgument("stamp", Preferences.get("lastTimeStamp", "0"));
cr.addArgument("appId", MyRestaurant.APP_AUTH);
return cr;
DishService (Client)
The response is parsed mostly so we can check the time stamp and update the local time stamp. The time stamp trick allows us to send a request with the last received
time stamp and initially we just send 0. This means the server is responsible for updating that value and if no change was made since the last time the server changed
that value we don’t need to receive new data.
13. public static void updateDishesFromServerSync() {
NetworkManager.getInstance().addToQueueAndWait(updateDishesFromServer());
}
public static void updateDishesFromServerAsync() {
NetworkManager.getInstance().addToQueue(updateDishesFromServer());
}
private static ConnectionRequest updateDishesFromServer() {
ConnectionRequest cr = new ConnectionRequest(MyRestaurant.SERVER_URL + "dish", false) {
private boolean autoLoad;
protected void readResponse(InputStream input) throws IOException {
JSONParser p = new JSONParser();
Map<String, Object> m = p.parseJSON(new InputStreamReader(input, "UTF-8"));
String stamp = (String)m.get("stamp");
if(stamp != null) {
Preferences.set("lastTimeStamp", stamp);
OutputStream o = Storage.getInstance().createOutputStream("DishListCache");
o.write(Result.fromContent(m).toString().getBytes());
o.close();
autoLoad = true;
}
}
protected void postResponse() {
if(autoLoad) {
loadDishesFromCache();
}
}
};
cr.addArgument("stamp", Preferences.get("lastTimeStamp", "0"));
cr.addArgument("appId", MyRestaurant.APP_AUTH);
return cr;
DishService (Client)
You will notice that if the data is changed we save it to cache then load it from cache later. This is pretty important, we call the loading code from the post response call
which runs on the event dispatch thread. The other callbacks in the connection request occur on a network thread but the post response call is on the event dispatch
thread and thus it can change the UI or UI related elements.
14. public static void updateDishesFromServerSync() {
NetworkManager.getInstance().addToQueueAndWait(updateDishesFromServer());
}
public static void updateDishesFromServerAsync() {
NetworkManager.getInstance().addToQueue(updateDishesFromServer());
}
private static ConnectionRequest updateDishesFromServer() {
ConnectionRequest cr = new ConnectionRequest(MyRestaurant.SERVER_URL + "dish", false) {
private boolean autoLoad;
protected void readResponse(InputStream input) throws IOException {
JSONParser p = new JSONParser();
Map<String, Object> m = p.parseJSON(new InputStreamReader(input, "UTF-8"));
String stamp = (String)m.get("stamp");
if(stamp != null) {
Preferences.set("lastTimeStamp", stamp);
OutputStream o = Storage.getInstance().createOutputStream("DishListCache");
o.write(Result.fromContent(m).toString().getBytes());
o.close();
autoLoad = true;
}
}
protected void postResponse() {
if(autoLoad) {
loadDishesFromCache();
}
}
};
cr.addArgument("stamp", Preferences.get("lastTimeStamp", "0"));
cr.addArgument("appId", MyRestaurant.APP_AUTH);
return cr;
DishService (Client)
As I mentioned before we send the time stamp and also an authorization key to the server. The latter is hardcoded forthe app but the former gets updated by the server
every time the data changes.
15. @Override
protected Container createContent() {
Container c = new Container(BoxLayout.y());
final Menu m = Restaurant.getInstance().menu.get();
if(m.dishes.size() == 0) {
if(DishService.hasCachedDishList()) {
DishService.loadDishesFromCache();
}
}
if(m.dishes.size() > 0) {
for(Dish currentDish : m.dishes) {
c.add(createDishContainer(currentDish));
}
c.addPullToRefresh(() -> {
DishService.updateDishesFromServerSync();
c.removeAll();
for(Dish currentDish : m.dishes) {
c.add(createDishContainer(currentDish));
}
c.revalidate();
});
} else {
MainMenuForm
Let’s move on to the main menu form where we now fetch the dishes from cache if they aren’t loaded yet.
16. @Override
protected Container createContent() {
Container c = new Container(BoxLayout.y());
final Menu m = Restaurant.getInstance().menu.get();
if(m.dishes.size() == 0) {
if(DishService.hasCachedDishList()) {
DishService.loadDishesFromCache();
}
}
if(m.dishes.size() > 0) {
for(Dish currentDish : m.dishes) {
c.add(createDishContainer(currentDish));
}
c.addPullToRefresh(() -> {
DishService.updateDishesFromServerSync();
c.removeAll();
for(Dish currentDish : m.dishes) {
c.add(createDishContainer(currentDish));
}
c.revalidate();
});
} else {
MainMenuForm
You will also notice that the pull to refresh call makes use of the update sync method to fetch the updated dishes synchronously.
17. } else {
final Container infi = FlowLayout.encloseCenter(new InfiniteProgress());
c.add(infi);
m.dishDownloadFinished.addChangeListener(p -> {
c.removeAll();
for(Dish currentDish : m.dishes) {
c.add(createDishContainer(currentDish));
}
categoryModel.removeAll();
for(String s : m.categories) {
categoryModel.addItem(s);
}
c.revalidate();
});
}
c.setScrollableY(true);
c.setScrollVisible(false);
dishesContainer = c;
return c;
}
MainMenuForm
This block represents a very special case of the first invocation when we have no dishes available and are still waiting. In this case we place an infinite progress in place
and bind a listener to the finished download so we can replace the progress. Notice we aren’t concerned about dishes suddenly appearing in a background thread
because everything is modified via the event dispatch thread so we can rely on this being in sync.