原文:http://www.ibm.com/developerworks/cn/xml/x-dataAndroid/index.html
Android 应用程序必须访问位于 Internet 上的数据,而 Internet 数据可以有几种不同的格式。本文将介绍在 Android 应用程序中如何使用三种数据格式:
- XML
- JSON
- Google 的 protocol buffers
首先开发一个 Web 服务,将 CSV 数据转换成 XML、JSON 和 protocol-buffers 格式。然后构建一个样例 Android 应用程序,可以从 Web 服务中以任何一种格式提取数据并将其解析并显示给用户。
要进行本文中的练习,您需要最新的 Android SDK(参见 参考资料)和 Android 2.2 平台。SDK 还要求您安装一个 Java™ 开发包(JDK);本文中使用了 JDK 1.6.0_17。您不需要有 Android 物理设备;所有代码都将在 SDK 的 Android 仿真器中运行。本文并没有教您如何进行 Android 开发,因此建议您熟悉 Android 编程。当然,只凭借 Java 编程语言的知识也可以完成本文的学习。
您还需要一个 Java web 应用程序服务器来运行 Android 应用程序使用的 Web 服务。此外,也可以将服务器端代码部署到 Google App Engine。参见 下载 部分获得完整的源代码。
Day Trader 应用程序
您将开发一个简单的 Android 应用程序,叫做 Day Trader。Day Trader 允许用户输入一个或更多的股票代码并获取其所代表股票的最新价格信息。用户可以指定数据使用的格式:XML、JSON 或 protocol buffers。实际的 Android 应用程序通常不会提供此选择,但是通过实现此功能,您可以了解如何让您的应用程序处理每一种格式。图 1 展示了 Day Trader 用户界面:
文本框及其旁边的 Add Stock 按钮允许用户输入感兴趣的每支股票的代码。用户按下Download Stock Data 按钮后,会从服务器请求所有这些股票的数据,在应用程序中解析并显示在屏幕上。默认情况下,获取的是 XML 数据。通过菜单,您可以在 XML、JSON 或 protocol buffers 数据格式间切换。
清单 1 显示用于创建 图 1 中所示 UI 的布局 XML:
清单 1. Day Trader 布局 XML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
<?
xml
version
=
"1.0"
encoding
=
"utf-8"
?>
<
LinearLayout
xmlns:android
=
"http://schemas.android.com/apk/res/android"
android:orientation
=
"vertical"
android:layout_width
=
"fill_parent"
android:layout_height
=
"fill_parent"
>
<
LinearLayout
android:orientation
=
"horizontal"
android:layout_width
=
"fill_parent"
android:layout_height
=
"wrap_content"
>
<
EditText
android:id
=
"@+id/symbol"
android:layout_width
=
"wrap_content"
android:layout_height
=
"wrap_content"
android:width
=
"120dip"
/>
<
Button
android:id
=
"@+id/addBtn"
android:layout_width
=
"wrap_content"
android:layout_height
=
"wrap_content"
android:text
=
"@string/addBtnLbl"
/>
</
LinearLayout
>
<
LinearLayout
android:orientation
=
"horizontal"
android:layout_width
=
"fill_parent"
android:layout_height
=
"wrap_content"
>
<
TextView
android:layout_width
=
"wrap_content"
android:layout_height
=
"wrap_content"
android:id
=
"@+id/symList"
/>
<
Button
android:id
=
"@+id/dlBtn"
android:layout_width
=
"wrap_content"
android:layout_height
=
"wrap_content"
android:text
=
"@string/dlBtnLbl"
/>
</
LinearLayout
>
<
ListView
android:id
=
"@android:id/list"
android:layout_height
=
"fill_parent"
android:layout_width
=
"fill_parent"
android:layout_weight
=
"1"
/>
</
LinearLayout
>
|
清单 1 中的大部分代码都简单明了。可以看到几个小部件创建了 图 1 所示的输入和按钮。还会看到一个 ListView
,Android 小部件中真正的瑞士军刀。此 ListView
将用从服务器下载的股票数据填充。清单 2 显示了控制该视图的 Activity
:
清单 2. Day Trader 主活动
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
public
class
Main extends ListActivity {
private
int
mode = XML;
// default
@Override
public
void
onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
final EditText input = (EditText) findViewById(R.id.symbol);
final TextView symbolsList = (TextView) findViewById(R.id.symList);
final Button addButton = (Button) findViewById(R.id.addBtn);
final Button dlButton = (Button) findViewById(R.id.dlBtn);
addButton.setOnClickListener(
new
OnClickListener(){
public
void
onClick(View v) {
String newSymbol = input.getText().toString();
if
(symbolsList.getText() ==
null
||
symbolsList.getText().length() == 0){
symbolsList.setText(newSymbol);
}
else
{
StringBuilder sb =
new
StringBuilder(symbolsList.getText());
sb.append(
","
);
sb.append(newSymbol);
symbolsList.setText(sb.toString());
}
input.setText(
""
);
}
});
dlButton.setOnClickListener(
new
OnClickListener(){
public
void
onClick(View v) {
String symList = symbolsList.getText().toString();
String[] symbols = symList.split(
","
);
symbolsList.setText(
""
);
switch
(mode){
case
JSON :
new
StockJsonParser().execute(symbols);
break
;
case
PROTOBUF :
new
StockProtoBufParser().execute(symbols);
break
;
default
:
new
StockXmlParser().execute(symbols);
break
;
}
}
});
}
}
|
此 Activity
设置了 清单 1 中 XML 文件的布局,它将几个事件处理程序连接起来。首先,对于Add Stock 按钮而言,代码读取文本框中的代码并将其添加到 symList TextView
中,用逗号分隔每个代码。接下来,对于 Download 按钮而言,处理程序从 symList TextView
中读取数据,然后 —基于 mode
变量— 使用三个不同的类之一从服务器下载数据。菜单设置 mode
变量的值;这个代码不是很重要,因此我在 清单 2 中省略了它。在了解各种数据下载/解析类之前,我先为您展示一下服务器如何提供此数据。
提供股票数据
应用程序服务器需要能够做两件事。第一,它必须获取股票代码列表并检索它们的数据。然后,它需要接受一个格式参数并基于该格式编码数据。对于 XML 和 JSON 格式而言,该服务器将返回作为文本的串行化的股票数据。对于 protocol buffers 而言,它必须发送二进制数据。 清单 3显示了处理这些步骤的 servlet:
清单 3. Stock Broker servlet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
public
class
StockBrokerServlet extends HttpServlet {
public
void
doGet(HttpServletRequest request,
HttpServletResponse response) throws IOException {
String[] symbols = request.getParameterValues(
"stock"
);
List<Stock> stocks = getStocks(symbols);
String format = request.getParameter(
"format"
);
String data =
""
;
if
(format ==
null
|| format.equalsIgnoreCase(
"xml"
)){
data = Stock.toXml(stocks);
response.setContentType(
"text/xml"
);
}
else
if
(format.equalsIgnoreCase(
"json"
)){
data = Stock.toJson(stocks);
response.setContentType(
"application/json"
);
}
else
if
(format.equalsIgnoreCase(
"protobuf"
)){
Portfolio p = Stock.toProtoBuf(stocks);
response.setContentType(
"application/octet-stream"
);
response.setContentLength(p.getSerializedSize());
p.writeTo(response.getOutputStream());
response.flushBuffer();
return
;
}
response.setContentLength(data.length());
response.getWriter().print(data);
response.flushBuffer();
response.getWriter().close();
}
public
List<Stock> getStocks(String... symbols) throws IOException{
StringBuilder sb =
new
StringBuilder();
for
(String symbol : symbols){
sb.append(symbol);
sb.append(
'+'
);
}
sb.deleteCharAt(sb.length() - 1);
String urlStr =
"http://finance.yahoo.com/d/quotes.csv?f=sb2n&s="
+
sb.toString();
URL url =
new
URL(urlStr);
HttpURLConnection conn =
(HttpURLConnection) url.openConnection();
BufferedReader reader =
new
BufferedReader(
new
InputStreamReader(conn.getInputStream()));
String quote = reader.readLine();
List<Stock> stocks =
new
ArrayList<Stock>(symbols.length);
while
(quote !=
null
){
String[] values = quote.split(
","
);
Stock s =
new
Stock(values[0], values[2],
Double.parseDouble(values[1]));
stocks.add(s);
quote = reader.readLine();
}
return
stocks;
}
}
|
这是一个简单的 Java servlet,只支持 HTTP GET
请求。它读入股票的值和格式请求参数。然后调用 getStocks()
方法。该方法调用 Yahoo! Finance 获取股票数据。Yahoo! 只支持 CSV 格式的数据,因此 getStocks()
方法将其解析到一个 Stock
对象列表。清单 4 展示了这个简单的数据结构:
清单 4. 股票数据结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
public
class
Stock {
private
final String symbol;
private
final String name;
private
final
double
price;
//getters and setters omitted
public
String toXml(){
return
"<stock><symbol>"
+ symbol +
"</symbol><name><![CDATA["
+
name +
"]]></name><price>"
+ price +
"</price></stock>"
;
}
public
String toJson(){
return
"{ 'stock' : { 'symbol' : "
+symbol +
", 'name':"
+ name +
", 'price': '"
+ price +
"'}}"
;
}
public
static
String toXml(List<Stock> stocks){
StringBuilder xml =
new
StringBuilder(
"<stocks>"
);
for
(Stock s : stocks){
xml.append(s.toXml());
}
xml.append(
"</stocks>"
);
return
xml.toString();
}
public
static
String toJson(List<Stock> stocks){
StringBuilder json =
new
StringBuilder(
"{'stocks' : ["
);
for
(Stock s : stocks){
json.append(s.toJson());
json.append(
','
);
}
json.deleteCharAt(json.length() - 1);
json.append(
"]}"
);
return
json.toString();
}
}
|
每个 Stock
都有三个属性— symbol
、name
和 price
— 和几个便捷的方法,以便将其自己转换成 XML 字符串或 JSON 字符串。它提供了一个工具方法,用于将 Stock
对象列表转换成 XML 或 JSON。回到 清单 3,根据格式请求参数,Stock
对象列表被转换成 XML 或 JSON 字符串并被发送回客户端。
XML 和 JSON 用例非常类似和直接。对于 protocol buffers,您必须生成 protocol buffers 格式的代码读写对象。为此,您需要使用 protocol buffers 规范格式定义数据结构。清单 5 展示了一个示例:
清单 5. 股票的 Protocol buffers 消息
1
2
3
4
5
6
7
8
9
10
11
12
13
|
package stocks;
option java_package =
"org.developerworks.stocks"
;
message Quote{
required
string
symbol = 1;
required
string
name = 2;
required
double
price = 3;
}
message Portfolio{
repeated Quote quote = 1;
}
|
protocol buffers 消息格式类似于接口描述语言 (IDL),它与语言无关,因此可以将其与各种语言一起使用。在本例中,运行 protocol buffers 编译器(protoc
)将 清单 5 中的代码编译成要用于客户端和服务器的 Java 类。有关将 protocol buffers 消息编译成 Java 类的详细信息,请参阅 Protocol Buffers Developer Guide(参见 参考资料)。
在 清单 3 中,一个名为 toProtoBuf()
的方法将 Stock
对象列表转换成一个 Portfolio
消息。清单 6 展示了该方法的实现:
清单 6. 创建组合消息
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public
static
Stocks.Portfolio toProtoBuf(List<Stock> stocks){
List<Stocks.Quote> quotes =
new
ArrayList<Stocks.Quote>(stocks.size());
for
(Stock s : stocks){
Quote q =
Quote.newBuilder()
.setName(s.name)
.setSymbol(s.symbol)
.setPrice(s.price)
.build();
quotes.add(q);
}
return
Portfolio.newBuilder().addAllQuote(quotes).build();
}
|
清单 6 中的代码使用了从 清单 5 中的消息生成的代码 — Quote
和 Portfolio
类。只需构建来自每个 Stock
对象的 Quote
,然后将其添加到 清单 3 中返回到 servlet 的 Portfolio
对象即可。在 清单 3 中,servlet 直接打开到客户端的流并使用生成的代码编写到流的二进制协议 buffers 数据。
现在,您了解了服务器如何创建要发送到 Android 应用程序的数据。接下来将学习应用程序如何解析此数据。
使用数据格式
清单 2 中的主 Activity
需要使用服务器可以发送的各种格式的数据。它还需要请求适当格式的数据并且数据一旦解析,就用它来填充其 ListView
。因此,无论数据格式是什么,大部分功能都是通用的。
首先,创建一个抽象的基类,封装此通用功能,如 清单 7 所示:
清单 7. 数据解析器基类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
abstract
class
BaseStockParser extends AsyncTask<String, Integer, Stock[]>{
String urlStr =
"http://protostocks.appspot.com/stockbroker?format="
;
protected
BaseStockParser(String format){
urlStr += format;
}
private
String makeUrlString(String... symbols) {
StringBuilder sb =
new
StringBuilder(urlStr);
for
(
int
i=0;i<symbols.length;i++){
sb.append(
"&stock="
);
sb.append(symbols[i]);
}
return
sb.toString();
}
protected
InputStream getData(String[] symbols) throws Exception{
HttpClient client =
new
DefaultHttpClient();
HttpGet request =
new
HttpGet(
new
URI(makeUrlString(symbols)));
HttpResponse response = client.execute(request);
return
response.getEntity().getContent();
}
@Override
protected
void
onPostExecute(Stock[] stocks){
ArrayAdapter<Stock> adapter =
new
ArrayAdapter<Stock>(Main.
this
, R.layout.stock,
stocks );
setListAdapter(adapter);
}
}
|
清单 7 中的基类扩展了 android.os.AsyncTask
。这是一个常用的用于异步操作的类。它抽象出线程和处理程序的创建,用于请求主 UI 线程。它是基于其输入和输出数据类型参数化的。对于所有解析器而言,输入总是一样的:股票代码字符串。 输出也是一样的:Stock
对象数组。基类获取 format
,这是一个指定了要使用的数据格式的字符串。然后提供一个方法,发出适当的 HTTP 请求并返回一个流响应。最后,它覆盖 AsyncTask
的 onPostExecute()
方法并使用从解析器返回的数据为 Activity
的 ListView
创建一个 Adapter
。
现在看到三个解析器的功能是通用的。我将为您展示更具体的解析代码,从 XML 解析器开始。
用 SAX 解析 XML
Android SDK 提供了几种使用 XML 的方式,包括标准 DOM 和 SAX。 对于一些对内存密集型情况,可以使用 SDK 的 pull-parser。大部分时候,SAX 是最快的方式。Android 包括一些便捷的 API 使得使用 SAX 更轻松。清单 8 显示了 Day Trader 应用程序的 XML 解析器:
清单 8. XML 解析器实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
private
class
StockXmlParser extends BaseStockParser{
public
StockXmlParser(){
super(
"xml"
);
}
@Override
protected
Stock[] doInBackground(String... symbols) {
ArrayList<Stock> stocks =
new
ArrayList<Stock>(symbols.length);
try
{
ContentHandler handler = newHandler(stocks);
Xml.parse(getData(symbols), Xml.Encoding.UTF_8, handler);
}
catch
(Exception e){
Log.e(
"DayTrader"
,
"Exception getting XML data"
, e);
}
Stock[] array =
new
Stock[symbols.length];
return
stocks.toArray(array);
}
private
ContentHandler newHandler(final ArrayList<Stock> stocks){
RootElement root =
new
RootElement(
"stocks"
);
Element stock = root.getChild(
"stock"
);
final Stock currentStock =
new
Stock();
stock.setEndElementListener(
new
EndElementListener(){
public
void
end() {
stocks.add((Stock) currentStock.clone());
}
}
);
stock.getChild(
"name"
).setEndTextElementListener(
new
EndTextElementListener(){
public
void
end(String body) {
currentStock.setName(body);
}
}
);
stock.getChild(
"symbol"
).setEndTextElementListener(
new
EndTextElementListener(){
public
void
end(String body) {
currentStock.setSymbol(body);
}
}
);
stock.getChild(
"price"
).setEndTextElementListener(
new
EndTextElementListener(){
public
void
end(String body) {
currentStock.setPrice(Double.parseDouble(body));
}
}
);
return
root.getContentHandler();
}
}
|
清单 8 中的大部分代码都在 newHandler()
方法中,该方法创建一个 ContentHandler
。如果熟悉 SAX 解析, 会知道 ContentHandler
通过响应 SAX 解析器触发的各种事件创建解析数据。newHandler()
方法使用 Android 便捷 API 指定使用事件处理程序的 ContentHandler
。代码只是侦听在解析器遇到各种标记时触发的事件,然后选取数据,放到 Stock
对象列表中。 创建ContentHandler
后,调用 Xml.parse()
方法来解析基类提供的 InputStream
并返回 Stock
对象数组。这是快速解析 XML 的方法,但是 —即使使用 Android 提供的便捷 API— 它也是非常冗长的。
使用 JSON
XML 是 Android 上的一等公民,鉴于依赖于 XML 的 Web 服务的数量,这是个好事。很多服务还支持另一个流行格式 JSON。它通常比 XML 简洁一些,但也是人们可读的,使得它更易于使用,并且可以更轻松地将其用于调试使用它的应用程序。Android 包括一个 JSON 解析器。(您可以从 JSON.org 网站获得该解析器,只是要去除几个手机不需要的类)。 清单 9 显示了使用中的解析器:
清单 9. JSON 解析器实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
private
class
StockJsonParser extends BaseStockParser{
public
StockJsonParser(){
super(
"json"
);
}
@Override
protected
Stock[] doInBackground(String... symbols) {
Stock[] stocks =
new
Stock[symbols.length];
try
{
StringBuilder json =
new
StringBuilder();
BufferedReader reader =
new
BufferedReader(
new
InputStreamReader(getData(symbols)));
String line = reader.readLine();
while
(line !=
null
){
json.append(line);
line = reader.readLine();
}
JSONObject jsonObj =
new
JSONObject(json.toString());
JSONArray stockArray = jsonObj.getJSONArray(
"stocks"
);
for
(
int
i=0;i<stocks.length;i++){
JSONObject
object
=
stockArray.getJSONObject(i).getJSONObject(
"stock"
);
stocks[i] =
new
Stock(
object
.getString(
"symbol"
),
object
.getString(
"name"
),
object
.getDouble(
"price"
));
}
}
catch
(Exception e){
Log.e(
"DayTrader"
,
"Exception getting JSON data"
, e);
}
return
stocks;
}
}
|
可以看到在 Android 中使用 JSON 解析器是多么简单。您将来自服务器的流转换成传递给 JSON 解析器的字符串。您遍历对象图并创建 Stock
对象数组。如果使用过 XML DOM 解析,这看起来很类似,因为编程模型几乎一样。
像 DOM 一样,JSON 解析器可以用于内存密集型应用。在 清单 9 中,所有来自服务器的数据都表示为字符串,然后作为 JSONObject
,最后作为 Stock
对象数组。换句话说,同一数据通过三种不同的方式表示。可以看到,对于大量数据而言,这可能是个问题。当然,一旦到达方法末尾,这三种数据表示方式中的两种都会落在范围之外,被垃圾回收器回收。但是,只是触发更频繁的垃圾回收可能会对用户体验带来负面影响,造成处理速度下降。如果内存效率和性能很重要,使用 protocol buffers 的解析器可能是个较好的选择。
使用 protocol buffers 处理二进制
Protocol buffers 是一个由 Google 开发的与语言无关的数据串行化格式,旨在比 XML 更快地通过网络传送数据。它是 Google 用于服务器对服务器调用的事实 标准。Google 将该格式及其用于 C++、Java 和 Python 编程语言的绑定工具以开源方式提供。
在 清单 3 和 清单 6 中看到 protocol buffers 是二进制格式。如您所料,这使得数据很简洁。如果在客户端和服务器端启用 gzip 压缩,在使用 XML 和 JSON 时通常也可以得到类似的消息大小,但是 protocol buffers 仍然有一些大小上的优势。它还是一种可以迅速解析的格式。最后,它提供了一个相当简单的 API。 清单 10 显示了一个示例解析器实现:
清单 10. Protocol buffers 解析器实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
private
class
StockProtoBufParser extends BaseStockParser{
public
StockProtoBufParser(){
super(
"protobuf"
);
}
@Override
protected
Stock[] doInBackground(String... symbols) {
Stock[] stocks =
new
Stock[symbols.length];
try
{
Stocks.Portfolio portfolio =
Stocks.Portfolio.parseFrom(getData(symbols));
for
|