9.3. Data storage and exchange formats: JSON and XML
We have already seen that our app operates with objects. However, we may need these items to be stored in permanent storage. Moreover, we may also need them to be sent using a process-to-process intercom mechanism. Thus, we may need to convert these objects into text strings that can be saved or sent.
To do this, we can use two alternative textual formats: JSON or XML.
9.3.1. JSON (JavaScript Object Notation)
JSON is highly tied to JavaScript. JavaScript provides methods that allow conversion of JavaScript objects to JSON strings (serialization) and conversion of JSON strings to JavaScript objects (deserialization). Its simplicity allowed it to spread quickly.
In Kotlin, the process is just as simple if we add the following to our Gradle file:
plugins {
id 'org.jetbrains.kotlin.plugin.serialization' version "1.6.10" apply false
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2")
}
Let us look at an example of how to do this. To start, we create the Kotlin class we want to serialize and deserialize in JSON. We import serialization and above the class we add the annotation @Serializable
.
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString
@Serializable
data class Item(val id: Long,
val name: String,
val description: String,
val phones:List<String>)
Now we can convert our instances of the Item
class to JSON. To do it, we import the following packages:
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString
import kotlinx.serialization.decodeFromString
We can now create the instance and convert it into a string:
var it:Item = Item(1234,"Adam","Scottish economist and philosopher who was a pioneer of political economy",listOf("999999999","55555545","4343242342"))
val json_str:String = Json.encodeToString(it)
The result is this:
{"id":1234,"name":"Adam","description":"Scottish economist and philosopher who was a pioneer of political economy","phones":["999999999","55555545","4343242342"]}
To convert the string back to a Kotlin object, simply type the following:
var it2:Item = Json.decodeFromString<Item>(json_str)
9.3.2. XML (Extensible Markup Language)
XML is a mark-up language that is very similar to HTML but where the user decides the set of tags are and their attributes. The user may also decide the hierarchical structure of the tags: for a particular node, which is the set of children that is supported. All these decisions are described in a Document Type Definition (DTD).
The Android development process is full of XML files used by the environment itself: the manifest.xml
file, the layout files, the files found in the res/values
folder, … In addition to these files, we may create or use XML files specific to our application. These files can be read from a web server or as a file within the res/raw
folder.
<?xml version="1.0" encoding="utf-8"?>
<doc>
<item id='1' type='draft'>
<link>https://www.uoc.edu/portal/en/index1.html</link>
<description><![CDATA[description1]]></description>
</item>
<item id='2' type='draft'>
<link>https://www.uoc.edu/portal/en/index2.html</link>
<description><![CDATA[description2]]></description>
</item>
<item id='3' type='publish'>
<link>https://www.uoc.edu/portal/en/index3.html</link>
<description><![CDATA[description3]]></description>
</item>
<item id='4' type='draft'>
<link>https://www.uoc.edu/portal/en/index4.html</link>
<description><![CDATA[description4]]></description>
</item>
</doc>
In Android there are three ways to analyze XML files:
- Using
XMLPullParser
or SAX analyzers. Both are event-driven analyzers. They are fast and use a low amount of memory, so it is the right way when we want to analyze very large XML files. - The
XMLPullParser
is more modern and user-friendly than the SAX analyzer. - The third method involves using an analyzer that loads the entire DOM (Document Object Model) into memory and allows us to traverse XML file in any way we want. This DOM is a tree that represents the entire XML file in memory.
First, we will see how the XMLPullParser
parser would work with the previous XML. To this end, we load the file found in res/raw
using:
istream value: InputStream = this.getResources().openRawResource(R.raw.document)
We create the parser and assign the istream
to the parser:
val parserFactory = XmlPullParserFactory.newInstance()
val parser = parserFactory.newPullParser()
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
parser.setInput(istream, null)
We declare some variables that we will use during the analysis:
var tag: String? = ""
var text: String? = ""
var link_str : String? = ""
var id_str : String? = ""
var description_str : String? = ""
var event = parser.eventType
We will go through the entire document until the XmlPullParser.END_DOCUMENT
event is received:
while (event != XmlPullParser.END_DOCUMENT) {
tag = parser.name
when (event) {
XmlPullParser.START_TAG -> if (tag == "item") {
id_str = parser.getAttributeValue(null, "id")
}
XmlPullParser.TEXT -> text = parser.text
XmlPullParser.END_TAG -> when (tag) {
"item" -> Log.d("msg",link_str + " " + description_str + " "+ id_str)
"link" -> link_str = text
"description" -> description_str = text
}
}
event = parser.next()
}
The core idea behind the loop can be summarized as follows:
-
When we receive an
XmlPullParser.TEXT
event we store the text in an auxiliary variable (text
). -
With the
XmlPullParser.START_TAG
event, we request the name of the tag,tag = parser.name
. If the tag is relevant to our analysis, we save the value of its attributes temporary variables. -
When the
XmlPullParser.END_TAG
event is received, we can now save our object to a data structure in our application because we have all the necessary data in the temporary variables.
If we are now performing the same analysis using the DOM analyzer, we will need to perform the following steps:
Again, we load the file found in res/raw
using:
istream value: InputStream =this.getResources().openRawResource(R.raw.document)
We create the parser and assigned the istream
to the parser:
val builderFactory: DocumentBuilderFactory = DocumentBuilderFactory.newInstance()
wow docBuilder: DocumentBuilder = builderFactory.newDocumentBuilder()
wow doc: Document = docBuilder.parse(istream)
DOM-type parsers load the entire document into memory and we can query them to access the nodes we want. For example, to list the nodes of type item
we would use the following call:
val nList: NodeList = doc.getElementsByTagName("item")
The power of this type of analyzer is that it is able to perform complex queries using a query language called XPath. We can define an XPath query that returns only those nodes of type item
where the type
attribute has the value draft
:
val xpFactory = XPathFactory.newInstance()
var xPath = xpFactory.newXPath()
var xpath = "//doc/item[@type='draft']"
val nList2: NodeList = xPath.evaluate(xpath, doc, XPathConstants.NODESET) as NodeList
In nList2
we will have the list of nodes that meet the condition. We can now navigate through these nodes:
for (i in 0..nList2.length - 1) {
val node = nList2.item(i)
val attributeValue = node.attributes.getNamedItem("id")
val value = attributeValue.nodeValue
xPath = xpFactory.newXPath()
xpath = "./link"
val child1 = xPath.evaluate(xpath, node, XPathConstants.NODE) as Node
val child1_value = child1.firstChild.nodeValue
xPath = xpFactory.newXPath()
xpath = "./description"
val child2 = xPath.evaluate(xpath, node, XPathConstants.NODE) as Node
val child2_value = child2.firstChild.nodeValue
}
To access the attributes of a node we use:
val attributeValue = node.attributes.getNamedItem("id")
val value = attributeValue.nodeValue
For each node we declare two more XPaths to access the internal link
and description
nodes. The text is in the firstChild.nodeValue
of these nodes.
Learn more: It is not correct to access nodes using the node position because this would cause the analysis to fail if new nodes are added in between. Another reason not to use the node position is because the spaces and line breaks between XML nodes on Android count as text nodes and this could make us use a wrong index for the node we want to access.