Insider's Guide to Udacity Android Developer Nanodegree Part 7 - Full Stack Android
Written by Nikos Vaggalis   
Monday, 23 April 2018
Article Index
Insider's Guide to Udacity Android Developer Nanodegree Part 7 - Full Stack Android
Adapting to Android's Needs
HTTP Requests
Combating Memory Leaks
GSON Problems
Filtering and Comparing Devices
Functional Programming in Java
Conclusions

GSON Problems

So back to the WebSite and in connecting to

http://smadeseek.com/tabletdetails?id=858

to retrieve a device's detailed specifications, the WebSite's TT template would, amongst other UI elements, render a HTML text view with device's id 858, Color values. The data structure that the template works with is an Array of Color values which is attached onto a hashref's 'ColorName' key:

{
   'ColorName' => [
                           'black',
                           'white',
                           'grey',
                           'red',
                           'blue'
                         ],
}

However in case there's just a single value, no Array is used:

{
   'ColorName' =>  'black'
}

Template Toolkit handles both of the cases without issues upon rendering the HTML

 
<tr> <td>Colors</td> <td>[% FOREACH color IN tabletdetailshash.
item('ColorName') %] [% color %] [% UNLESS loop.last() %], [% END %] </td> </tr>

because :

"The FOREACH directive will iterate through the items in a list, 
processing the enclosed block for each one.
The FOREACH directive can also be used to iterate through the
entries in a hash array. Each entry in the hash is returned in
sorted order (based on the key) as a hash array containing
'key' and 'value' items. [% users = { tom => 'Thomas', dick => 'Richard', larry => 'Lawrence', } %] [% FOREACH u IN users %] * [% u.key %] : [% u.value %] [% END %] "

But the equivalent Android call to

http://smadeseek.com/tabletdetails/json?id=858

would sometimes have the GSON parser choke with

01-27 23:24:56.865 24052-24052/nvglabs.android.com.
smartdeviceseeker V/http fail::
java.lang.IllegalStateException:
Expected BEGIN_ARRAY but was
STRING at line 1 column 730
path $.Colorname

This happens when it receives a single value

'ColorName' :  'black'

in place of an array

'ColorName' : [
                           'black',
                           'white',
                           'grey',
                           'red',
                           'blue
 ]

 

because the GSON Deserializer expects to map the JSON to the following Java class structure


@Parcel public class TabletDetail { @SerializedName("ColorName") public List<String> colorName; public List<String> getColorName() { return colorName; } @SerializedName("ModelScreenDensity") public String modelScreenDensity; ........ }

to make a Java object that represents the device together with its specifications.In other words it expects a List of color strings not just a single color string value.

Unlike TT, under Java's strong type system we can have one or the other, a String or a List<String>, not both.So when the Deserializer encounters a single string value in place of the List of strings, it doesn't know what to do with it hence the exception.

To solve this, the first option was to redesign the Model component on the server's side so that it always returns JSON arrays.That would however mean amending the site's design too.The second and easier one was to leave the server side as-is without any modifications and make the necessary changes on the client's side.This in effect meant customizing the way GSON deserializes the JSON feed.


JsonDeserializer<TabletDetail> TabletDetailDeserializer =
new JsonDeserializer<TabletDetail>() { @Override public TabletDetail deserialize(JsonElement json,
Type typeOfT, JsonDeserializationContext context)
throws JsonParseException { JsonObject jsonObject = json.getAsJsonObject(); if (jsonObject.get("ColorName") !=
null && !jsonObject.get("ColorName").toString()
.equals("null") && !jsonObject.get("ColorName")
.isJsonArray()) { JsonArray x = new JsonArray(); try { x.add(jsonObject.get("ColorName")); } catch (Exception j) { //todo } jsonObject.remove("ColorName"); jsonObject.add("ColorName",x); } JsonElement json1 = jsonObject; TabletDetail targetObject = new Gson()
.fromJson(json1, TabletDetail.class); return targetObject; }

For that I had to tap into the JSON tree structure the deserializer sees and make the necessary amendments to the offending nodes.So in this case when the deserializer finds a

'ColorName' node, jsonObject.get("ColorName") != null

which doesn't point to a null value, !jsonObject.get("ColorName").toString().equals("null")

and that value is not an Array, !jsonObject.get("ColorName").isJsonArray()

it means that 'ColorName' points to a non null single string value, as in 'ColorName' : 'black'. If that's the case I immediately create an array with that value ('black') as its only element.Then I attach this array to the JSON tree in place of the bad node, that is, I remove the old one with jsonObject.remove("ColorName") and attach the modified one with jsonObject.add("ColorName",x).

Although I've fixed the troublesome nodes, at the same time I wanted the trouble-free nodes to be parsed as they initially were set to.Therefore at the end of the deserializer's block, I run the default deserialization again but this time on the newly created tree that contains all the nodes, modified or not:


new Gson().fromJson(json1, TabletDetail.class)

 

Working with Fragments and the Backstack

Once in the Listing screen:

 

there's a number of paths you can follow, according to the application's design:

 

 

This diagram clearly demonstrates how the use of Fragments leads to modularized design and thus reusability.However in working with Fragments, you ought to possess a deep understanding of how their lifecycle is utilized, something that is further complicated when the Backstack comes into play.

For example, a common misconception is that popBackStack() pops fragment instances off the stack.In reality it pops transactions which could had involved a number of fragments.Yet another, is not being aware that when such a transaction adds an instance of a fragment to the backstack, the backstack acts as container that preserves the fragment's state by keeping the fragment's member variables in tact.T

The Listing screen (FragmentListDevices) uses this technique when it comes on top of the backstack after returning from the Filter screen (FragmentFilter), that is, after FragmentFilter is popped off the backstack as in FragmentListDevices<--FragmentFilter.

So upon returning to FragmentListDevices its onCreateView is called again, therefore in there I check the mRecyclerView member field, the one that points to the actual RecyclerView instance, for nullity.If it's not, that means that the rest of the Views have been preserved too thus I don't have to go through creating them again!Instead, I just return the stored rootView

   
@Override public View onCreateView(LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) { if (mRecyclerView != null) { return rootView; } rootView = inflater.inflate(
R.layout.fragment_list_devices,
container, false); ... ... ... }

However the same trick won't save the day when the device is rotated.At this point you have to check for savedInstanceState != null, retrieve the saved state from savedInstanceState and with it recreate the RecyclerView from scratch. Another property of Fragments is that Fragments in essence do not die and as such you can reconnect to them and call their methods (setMenuValues() in this case) by retrieving their instance through the Activity's FragmentManager :


FragmentManager fragmentManager =
getSupportFragmentManager(); FragmentListDevices fragment1 =
(FragmentListDevices) fragmentManager.
findFragmentByTag(FRAGMENT_LIST_DEVICES); fragmentManager.beginTransaction() .replace(R.id.fragment_container, fragment1)
.show(fragment1) .commit(); fragment1.setMenuValues(menuValuesIn);

Scrolling, paging and EventBus

Paging was handled manually hence painfully because the website sends over the list of devices in batches of 9.Therefore the app had to be adapted so that when the RecyclerView is scrolled upwards and there's more devices to be fetched, RecyclerView's OnScrollListener() event handler should trigger an event, EventBus.getDefault().post(scrolling).This in turn is intercepted by


@Subscribe(threadMode = ThreadMode.MAIN) public void onMessageEvent(Bundle event) { if (event.containsKey(getResources()
.getString(R.string.scrolling) { getActivity().getSupportLoaderManager()
.restartLoader(22, event,this); }

}

 

which restarts the Loader.The Loader as already gone through, initiates a new network request in order to fetch the next batch of 9.

If it wasn't for Eventbus, the alternative would be just ugly; global variables and tedious control mechanisms in order to make it work.Yet another great use of Eventbus was in facilitating Fragment to Activity communication in place of the classic Interface-Listener pattern.



Last Updated ( Monday, 23 April 2018 )