Android(安卓)应用程序中使用 Internet 数据(XML、JSON 和 protocol buffers)
简介:您的很多 Android 应用程序都需要与 Internet 数据交互,这些数据采用各种格式。本文将带您构建一个 Android 应用程序,它可以使用两种流行数据格式 —XML 和 JavaScript Object Notation (JSON)— 以及来自 Google 的比较奇异的 protocol buffers 格式。您将了解到与每个格式相关的性能和编码利弊。
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 用户界面:
图 1. 运行中的 Day Trader 应用程序
文本框及其旁边的Add Stock按钮允许用户输入感兴趣的每支股票的代码。用户按下Download Stock Data按钮后,会从服务器请求所有这些股票的数据,在应用程序中解析并显示在屏幕上。默认情况下,获取的是 XML 数据。通过菜单,您可以在 XML、JSON 或 protocol buffers 数据格式间切换。
清单 1显示用于创建图 1中所示 UI 的布局 XML:
清单 1. Day Trader 布局 XML
<?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 主活动
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
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,只支持 HTTPGET请求。它读入股票的值和格式请求参数。然后调用getStocks()方法。该方法调用 Yahoo! Finance 获取股票数据。Yahoo! 只支持 CSV 格式的数据,因此getStocks()方法将其解析到一个Stock对象列表。清单 4展示了这个简单的数据结构:
清单 4. 股票数据结构
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 消息
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 类。
在清单 3中,一个名为toProtoBuf()的方法将Stock对象列表转换成一个Portfolio消息。清单 6展示了该方法的实现:
清单 6. 创建组合消息
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. 数据解析器基类
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 解析器实现
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— 它也是非常冗长的。
更多相关文章
- Android(安卓)Fragment 真正的完全解析(上)
- android小功能实现之xml文件解析(Pull)
- android 使用Intent传递数据之全局变量传递
- 初学者---Android(安卓)Fragment之间数据传递的三种方式
- Android笔试总结
- Android(安卓)init.rc文件解析过程分析
- android Intents和Intent Filters - 开发文档翻译 - 1
- Android布局文件中命名空间的解析
- Android多媒体数据库之MediaStore研究