入门
从互联网上学习构建可与XML一起使用的Android应用程序。 Android应用程序是用Java™编程语言编写的,因此必须具备Java技术的经验。 要为Android开发,您将需要Android SDK。 本文中显示的所有代码都可以与任何版本的Android SDK一起使用,但是使用SDK 1.5_pre来开发代码。 您可以仅使用SDK和文本编辑器来开发Android应用程序,但是使用Eclipse插件Android Developer Tools(ADT)则要容易得多。 对于本文,ADT 0.9版本与Eclipse 3.4.2 Java版本一起使用。
Android上的XML
Android平台是一个开源的移动开发平台。 它使您可以访问其运行的移动设备的各个方面,从低级图形到硬件,例如手机上的摄像头。 使用Android可以实现如此多的功能,您可能想知道为什么需要使用XML。 使用XML并不是那么有趣。 它正在与它所支持的事物一起工作。 XML通常在Internet上用作数据格式。 如果要从Internet访问数据,则数据很可能采用XML格式。 如果要将数据发送到Web服务,则可能还需要发送XML。 简而言之,如果您的Android应用程序将利用Internet,那么您可能需要使用XML。 幸运的是,您可以使用很多选项在Android上使用XML。
XML解析器
Android平台的最大优势之一就是它利用Java编程语言。 Android SDK并未完全提供标准Java运行时环境(JRE)可用的所有功能,但支持其中的大部分。 Java平台在相当长的一段时间内一直支持使用XML的许多不同方式,并且大多数Java与XML相关的API在Android上都完全受支持。 例如,Java的XML简单API(SAX)和文档对象模型(DOM)均可在Android上使用。 这两个API多年来都是Java技术的一部分。 较新的XML流API(StAX)在Android中不可用。 但是,Android提供了功能上等效的库。 最后,Java XML Binding API在Android中也不可用。 该API肯定可以在Android中实现。 但是,它往往是重量级的API,通常需要许多不同类的许多实例来表示XML文档。 因此,对于受约束的环境(例如设计用于运行Android的手持设备)而言,它并不理想。 在以下各节中,您将获取Internet上可用的XML的简单来源,并了解如何使用上述各种API在Android应用程序中解析XML。 首先,看一下将使用Internet中XML的简单应用程序的基本部分。
Android新闻阅读器
该应用程序将从流行的Android开发人员网站Androidster中获取RSS提要,并将其解析为可用于支持Android ListView的简单Java对象列表(请参阅下载以获取源代码)。 这是经典的多态行为-提供相同行为的不同实现(不同的XML解析算法)。 清单1显示了使用接口在Java代码中建模的难易程度。
清单1. XML feed解析器接口
package org.developerworks.android;
import java.util.List;
public interface FeedParser {
List<Message> parse();
}
在清单2中 , Message
类是一个经典的Plain Old Java Object(POJO),它代表一个数据结构。
清单2. Message
POJO
public class Message implements Comparable<Message>{
static SimpleDateFormat FORMATTER =
new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z");
private String title;
private URL link;
private String description;
private Date date;
// getters and setters omitted for brevity
public void setLink(String link) {
try {
this.link = new URL(link);
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
public String getDate() {
return FORMATTER.format(this.date);
}
public void setDate(String date) {
// pad the date if necessary
while (!date.endsWith("00")){
date += "0";
}
try {
this.date = FORMATTER.parse(date.trim());
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
@Override
public String toString() {
// omitted for brevity
}
@Override
public int hashCode() {
// omitted for brevity
}
@Override
public boolean equals(Object obj) {
// omitted for brevity
}
// sort by date
public int compareTo(Message another) {
if (another == null) return 1;
// sort descending, most recent first
return another.date.compareTo(date);
}
}
清单2中的 Message通常很简单。 它确实通过允许将日期和链接作为简单的字符串来访问而将它们表示为更强类型的对象( java.util.Date
和java.net.URL
)来隐藏其内部状态。 这是一个经典的Value Object,因此它根据其内部状态实现equals()
和hashCode()
。 它还实现了Comparable
接口,因此您可以使用它进行排序(按日期)。 实际上,数据总是从提要中排序,因此这不是必需的。
每个解析器实现都需要使用一个指向Androidster feed的URL,并使用该URL打开与Androidster站点的HTTP连接。 这种常见的行为自然会在Java代码中使用清单3所示的抽象基类进行建模。
清单3.基本提要解析器类
public abstract class BaseFeedParser implements FeedParser {
// names of the XML tags
static final String PUB_DATE = "pubDate";
static final String DESCRIPTION = "description";
static final String LINK = "link";
static final String TITLE = "title";
static final String ITEM = "item";
final URL feedUrl;
protected BaseFeedParser(String feedUrl){
try {
this.feedUrl = new URL(feedUrl);
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
protected InputStream getInputStream() {
try {
return feedUrl.openConnection().getInputStream();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
基类存储feedUrl
并使用它打开java.io.InputStream
。 如果出了什么问题,它只会抛出RuntimeException
,因此应用程序很快就会失败。 基类还为标记的名称定义了一些简单的常量。 清单4显示了提要中的一些示例内容,因此您可以看到这些标记的重要性。
清单4.示例XML提要
<?xml version="1.0" encoding="UTF-8"?>
<!-- generator="FeedCreator 1.7.2" -->
<rss version="2.0">
<channel>
<title>android_news</title>
<description>android_news</description>
<link>http://www.androidster.com/android_news.php</link>
<lastBuildDate>Sun, 19 Apr 2009 19:43:45 +0100</lastBuildDate>
<generator>FeedCreator 1.7.2</generator>
<item>
<title>Samsung S8000 to Run Android, Play DivX, Take Over the
World</title>
<link>http://www.androidster.com/android_news/samsung-s8000-to-run-android-
play-divx-take-over-the-world</link>
<description>More details have emerged on the first Samsung handset
to run Android. A yet-to-be announced phone called the S8000 is being
reported ...</description>
<pubDate>Thu, 16 Apr 2009 07:18:51 +0100</pubDate>
</item>
<item>
<title>Android Cupcake Update on the Horizon</title>
<link>http://www.androidster.com/android_news/android-cupcake-update-
on-the-horizon</link>
<description>After months of discovery and hearsay, the Android
build that we have all been waiting for is about to finally make it
out ...</description>
<pubDate>Tue, 14 Apr 2009 04:13:21 +0100</pubDate>
</item>
</channel>
</rss>
从清单4的示例中可以看到, ITEM
对应于Message
实例。 项目的子节点( TITLE
, LINK
等)对应于Message
实例的属性。 既然您已经了解了Feed的外观,并且已经准备好了所有常见的部分,那么就来看看如何使用Android上可用的各种技术来解析此Feed。 您将从SAX开始。
使用SAX
在Java环境中,当您需要快速解析器并希望最大程度地减少应用程序的内存占用时,通常可以使用SAX API。 这使其非常适合运行Android的移动设备。 您可以直接从Java环境使用SAX API,而无需进行特殊修改即可在Android上运行。 清单5显示了FeedParser
接口的SAX实现。
清单5. SAX实现
public class SaxFeedParser extends BaseFeedParser {
protected SaxFeedParser(String feedUrl){
super(feedUrl);
}
public List<Message> parse() {
SAXParserFactory factory = SAXParserFactory.newInstance();
try {
SAXParser parser = factory.newSAXParser();
RssHandler handler = new RssHandler();
parser.parse(this.getInputStream(), handler);
return handler.getMessages();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
如果您以前使用过SAX,则看起来非常熟悉。 与任何SAX实现一样,大多数细节都在SAX处理程序中。 处理程序在翻阅XML文档时会从SAX解析器接收事件。 在这种情况下,您创建了一个名为RssHandler
的新类,并将其注册为解析器的处理程序,如清单6所示 。
清单6. SAX处理程序
import static org.developerworks.android.BaseFeedParser.*;
public class RssHandler extends DefaultHandler{
private List<Message> messages;
private Message currentMessage;
private StringBuilder builder;
public List<Message> getMessages(){
return this.messages;
}
@Override
public void characters(char[] ch, int start, int length)
throws SAXException {
super.characters(ch, start, length);
builder.append(ch, start, length);
}
@Override
public void endElement(String uri, String localName, String name)
throws SAXException {
super.endElement(uri, localName, name);
if (this.currentMessage != null){
if (localName.equalsIgnoreCase(TITLE)){
currentMessage.setTitle(builder.toString());
} else if (localName.equalsIgnoreCase(LINK)){
currentMessage.setLink(builder.toString());
} else if (localName.equalsIgnoreCase(DESCRIPTION)){
currentMessage.setDescription(builder.toString());
} else if (localName.equalsIgnoreCase(PUB_DATE)){
currentMessage.setDate(builder.toString());
} else if (localName.equalsIgnoreCase(ITEM)){
messages.add(currentMessage);
}
builder.setLength(0);
}
}
@Override
public void startDocument() throws SAXException {
super.startDocument();
messages = new ArrayList<Message>();
builder = new StringBuilder();
}
@Override
public void startElement(String uri, String localName, String name,
Attributes attributes) throws SAXException {
super.startElement(uri, localName, name, attributes);
if (localName.equalsIgnoreCase(ITEM)){
this.currentMessage = new Message();
}
}
}
RssHandler
类扩展了org.xml.sax.helpers.DefaultHandler
类。 此类为与SAX解析器引发的事件相对应的所有方法提供默认的,无操作的实现。 这允许子类仅根据需要覆盖方法。 RssHandler
还有一个附加的API,即getMessages
。 这将返回处理程序在从SAX解析器接收事件时收集的Message
对象的列表。 它具有其他两个内部变量,一个用于解析的Message
实例的currentMessage
,以及一个称为builder
的StringBuilder
变量,用于存储来自文本节点的字符数据。 当解析器将相应的事件发送到处理程序时,调用startDocument
方法时,它们都将被初始化。
看一下清单6中的startElement
方法。 每当在XML文档中遇到开始标签时,就会调用此方法。 您只需关心该标签是ITEM
标签。 在这种情况下,您将创建一个新的Message
。 现在看一下characters
方法。 当遇到来自文本节点的字符数据时,将调用此方法。 数据仅添加到builder
变量中。 最后看一下endElement
方法。 遇到结束标签时将调用此方法。 对于与Message
属性(例如TITLE
和LINK
相对应的标记,使用来自builder
变量的数据在currentMessage
上设置适当的属性。 如果结束标签是ITEM
,则将currentMessage
添加到Messages列表中。 这都是非常典型的SAX解析。 这里没有什么是Android独有的。 因此,如果您知道如何编写Java SAX解析器,那么您就会知道如何编写Android SAX解析器。 但是,Android SDK确实在SAX之上添加了一些便利功能。
简化SAX解析
Android SDK包含一个名为android.util.Xml
的实用程序类。 清单7显示了如何使用相同的实用程序类设置SAX解析器。
清单7. Android SAX解析器
public class AndroidSaxFeedParser extends BaseFeedParser {
public AndroidSaxFeedParser(String feedUrl) {
super(feedUrl);
}
public List<Message> parse() {
RssHandler handler = new RssHandler();
try {
Xml.parse(this.getInputStream(), Xml.Encoding.UTF_8, handler);
} catch (Exception e) {
throw new RuntimeException(e);
}
return handler.getMessages();
}
}
注意,该类仍然使用标准的SAX处理程序,因此您只需重用上面清单7 RssHandler
所示的RssHandler
即可。 能够重用SAX处理程序非常棒,但这是一段复杂的代码。 您可以想象,如果必须解析一个更为复杂的XML文档,则该处理程序可能会成为错误的温床。 例如,回顾清单6中的endElement
方法。 注意,在尝试设置属性之前,如何检查currentMessage
是否为null? 现在回顾一下清单4中的示例XML。 请注意, ITEM
标签之外还有TITLE
和LINK
标签。 这就是为什么要进行空检查的原因。否则,第一个TITLE
标记可能会导致NullPointerException
。 Android包括其自己的SAX API变体(请参见清单8 ),从而无需您编写自己的SAX处理程序。
清单8.简化的Android SAX解析器
public class AndroidSaxFeedParser extends BaseFeedParser {
public AndroidSaxFeedParser(String feedUrl) {
super(feedUrl);
}
public List<Message> parse() {
final Message currentMessage = new Message();
RootElement root = new RootElement("rss");
final List<Message> messages = new ArrayList<Message>();
Element channel = root.getChild("channel");
Element item = channel.getChild(ITEM);
item.setEndElementListener(new EndElementListener(){
public void end() {
messages.add(currentMessage.copy());
}
});
item.getChild(TITLE).setEndTextElementListener(new EndTextElementListener(){
public void end(String body) {
currentMessage.setTitle(body);
}
});
item.getChild(LINK).setEndTextElementListener(new EndTextElementListener(){
public void end(String body) {
currentMessage.setLink(body);
}
});
item.getChild(DESCRIPTION).setEndTextElementListener(new
EndTextElementListener(){
public void end(String body) {
currentMessage.setDescription(body);
}
});
item.getChild(PUB_DATE).setEndTextElementListener(new EndTextElementListener(){
public void end(String body) {
currentMessage.setDate(body);
}
});
try {
Xml.parse(this.getInputStream(), Xml.Encoding.UTF_8,
root.getContentHandler());
} catch (Exception e) {
throw new RuntimeException(e);
}
return messages;
}
}
如所承诺的,新的SAX解析代码不使用SAX处理程序。 相反,它使用SDK中android.sax包中的类。 这些使您可以对XML文档的结构进行建模,并根据需要添加事件侦听器。 在上面的代码中,您声明文档将具有一个名为rss
的根元素,并且该元素将具有一个名为channel
的子元素。 然后,您说该channel
将有一个称为ITEM
的子元素,然后开始附加侦听器。 对于每个侦听器,您使用了一个匿名内部类,该内部类实现了您感兴趣的接口( EndElementListner
或EndTextElementListener
)。 请注意,无需跟踪字符数据。 这不仅更简单,而且实际上更有效。 最后,当您调用Xml.parse实用程序方法时,现在将传入从根元素生成的处理程序。
清单8中的所有上述代码都是可选的。 如果您对Java环境中的标准SAX解析代码感到满意,则可以坚持这样做。 如果您想试用Android SDK提供的便捷包装,也可以使用它。 如果您根本不想使用SAX,该怎么办? 有一些替代方法。 您将要看到的第一个是DOM。
使用DOM
完全支持Android上的DOM解析。 它的工作方式与在台式机或服务器上运行的Java代码完全相同。 清单9显示了解析器接口的基于DOM的实现。
清单9. feed解析器的基于DOM的实现
public class DomFeedParser extends BaseFeedParser {
protected DomFeedParser(String feedUrl) {
super(feedUrl);
}
public List<Message> parse() {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
List<Message> messages = new ArrayList<Message>();
try {
DocumentBuilder builder = factory.newDocumentBuilder();
Document dom = builder.parse(this.getInputStream());
Element root = dom.getDocumentElement();
NodeList items = root.getElementsByTagName(ITEM);
for (int i=0;i<items.getLength();i++){
Message message = new Message();
Node item = items.item(i);
NodeList properties = item.getChildNodes();
for (int j=0;j<properties.getLength();j++){
Node property = properties.item(j);
String name = property.getNodeName();
if (name.equalsIgnoreCase(TITLE)){
message.setTitle(property.getFirstChild().getNodeValue());
} else if (name.equalsIgnoreCase(LINK)){
message.setLink(property.getFirstChild().getNodeValue());
} else if (name.equalsIgnoreCase(DESCRIPTION)){
StringBuilder text = new StringBuilder();
NodeList chars = property.getChildNodes();
for (int k=0;k<chars.getLength();k++){
text.append(chars.item(k).getNodeValue());
}
message.setDescription(text.toString());
} else if (name.equalsIgnoreCase(PUB_DATE)){
message.setDate(property.getFirstChild().getNodeValue());
}
}
messages.add(message);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return messages;
}
}
像第一个SAX示例一样,此代码也不是Android特有的。 DOM解析器将所有XML文档读入内存,然后允许您使用DOM API来遍历XML树,从而检索所需的数据。 这是非常简单的代码,并且在某些方面比基于SAX的实现更简单。 但是,由于首先将所有内容读取到内存中,所以DOM通常会消耗更多的内存。 在运行Android的移动设备上,这可能是个问题,但是在某些用例中,XML文档的大小永远不会很大,这可以令人满意。 可能暗示Android开发人员猜测SAX解析在Android应用程序上会更加普遍,因此为此提供了额外的实用程序。 您可以在Android上使用另一种XML解析器,那就是拉式解析器。
XML拉式解析器
如前所述,Android不提供对Java的StAX API的支持。 但是,Android确实提供了与StAX相似的提取解析器。 它允许您的应用程序代码从解析器中提取或查找事件,而不是SAX解析器将事件自动推送到处理程序。 清单10显示了提要解析器接口的拉式解析器实现。
清单10.基于拉式解析器的实现
public class XmlPullFeedParser extends BaseFeedParser {
public XmlPullFeedParser(String feedUrl) {
super(feedUrl);
}
public List<Message> parse() {
List<Message> messages = null;
XmlPullParser parser = Xml.newPullParser();
try {
// auto-detect the encoding from the stream
parser.setInput(this.getInputStream(), null);
int eventType = parser.getEventType();
Message currentMessage = null;
boolean done = false;
while (eventType != XmlPullParser.END_DOCUMENT && !done){
String name = null;
switch (eventType){
case XmlPullParser.START_DOCUMENT:
messages = new ArrayList<Message>();
break;
case XmlPullParser.START_TAG:
name = parser.getName();
if (name.equalsIgnoreCase(ITEM)){
currentMessage = new Message();
} else if (currentMessage != null){
if (name.equalsIgnoreCase(LINK)){
currentMessage.setLink(parser.nextText());
} else if (name.equalsIgnoreCase(DESCRIPTION)){
currentMessage.setDescription(parser.nextText());
} else if (name.equalsIgnoreCase(PUB_DATE)){
currentMessage.setDate(parser.nextText());
} else if (name.equalsIgnoreCase(TITLE)){
currentMessage.setTitle(parser.nextText());
}
}
break;
case XmlPullParser.END_TAG:
name = parser.getName();
if (name.equalsIgnoreCase(ITEM) &&
currentMessage != null){
messages.add(currentMessage);
} else if (name.equalsIgnoreCase(CHANNEL)){
done = true;
}
break;
}
eventType = parser.next();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return messages;
}
}
拉式解析器的工作方式与SAX解析器类似。 它具有类似的事件(开始元素,结束元素),但是您必须从它们中拉出(parser.next()
)。 事件以数字代码发送,因此您可以使用简单的大小写切换。 请注意,通过拉式解析,而不是像SAX解析中那样侦听元素的结尾,而是在开始时进行大多数处理很简单。 在清单10的代码中,当元素启动时,您可以调用parser.nextText()
从XML文档中提取所有字符数据。 这为SAX解析提供了很好的简化。 还要注意,您设置了一个标志(布尔变量done
)来标识何时到达您感兴趣的内容的末尾。这使您可以及早停止读取XML文档,因为您知道代码将不在乎关于文档的其余部分。 这可能非常有用,尤其是在您只需要访问XML文档的一小部分的情况下。 您可以通过尽快停止解析来大大减少解析时间。 同样,这种优化在连接速度可能很慢的移动设备上尤其重要。 拉式解析器可以具有一些不错的性能优势,并且易于使用。 它也可以用来编写XML。
创建XML
到目前为止,我一直专注于从Internet解析XML。 但是,有时您的应用程序可能需要将XML发送到远程服务器。 您显然可以只使用StringBuilder
或类似的东西来创建XML字符串。 清单11中的pull解析器是另一个选择。
清单11.使用拉式解析器编写XML
private String writeXml(List<Message> messages){
XmlSerializer serializer = Xml.newSerializer();
StringWriter writer = new StringWriter();
try {
serializer.setOutput(writer);
serializer.startDocument("UTF-8", true);
serializer.startTag("", "messages");
serializer.attribute("", "number", String.valueOf(messages.size()));
for (Message msg: messages){
serializer.startTag("", "message");
serializer.attribute("", "date", msg.getDate());
serializer.startTag("", "title");
serializer.text(msg.getTitle());
serializer.endTag("", "title");
serializer.startTag("", "url");
serializer.text(msg.getLink().toExternalForm());
serializer.endTag("", "url");
serializer.startTag("", "body");
serializer.text(msg.getDescription());
serializer.endTag("", "body");
serializer.endTag("", "message");
}
serializer.endTag("", "messages");
serializer.endDocument();
return writer.toString();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
XmlSerializer
类与上一节中使用的XmlPullParser
是同一软件包的一部分 。 它没有拉入事件,而是将它们推到流或编写器中。 在这种情况下,它只是将它们推送到java.io.StringWriter
实例。 它提供了一个简单的API,其中包含用于开始和结束文档,处理元素以及添加文本或属性的方法。 这可以是使用StringBuilder
的不错选择,因为更容易确保XML格式正确。
摘要
您要为Android设备构建哪种应用程序? 不管是什么,如果它需要使用Internet上的数据,那么它可能需要使用XML。 在本文中,您看到Android装有许多用于处理XML的工具。 您可以选择其中一种作为您的选择工具,也可以根据用例进行选择。 大多数情况下,安全的选择是使用SAX,而Android为您提供了一种传统的SAX制作方法以及一个在SAX之上的精美便捷包装。 如果您的文档很小,那么也许使用DOM是更简单的方法。 如果您的文档很大,但是只需要文档的一部分,那么XML提取解析器可能是更有效的方法。 最后,对于编写XML,拉解析器包也提供了一种方便的方法。 因此,无论您的XML需求是什么,Android SDK都能为您提供所需的东西。
翻译自: https://www.ibm.com/developerworks/xml/library/x-android/index.html