この記事では、厄介な状況に対処するために Android SDK に用意されているいくつかの手段について説明します。Android アプリケーションを開発するためには最新の Android SDK が必要であり、この SDK には JDK (Java Development Kit) が必要です。私は Android 2.2 と JDK 1.6.0_17 を使用しました (これらのツールについては「参考文献」のリンクを参照) が、実際の Android 搭載機器を用意する必要はありません。この記事で説明するコードはすべて、SDK に用意されている Android エミュレーターで問題なく動作します。この記事では Android 開発に関する基本的な事項は説明しないため、読者は Android プログラミングについて十分理解している必要があります。ただし Java プログラミング言語の知識があれば、この記事を理解することはできるはずです。
Android アプリケーションで最も一般的なタスクの 1 つが、リモート・サーバーとの間でのネットワークを介したデータの送受信です。多くの場合、この操作の結果として何らかの新しいデータが得られ、このデータをユーザーに表示する必要があります。これはつまり、ユーザー・インターフェースを変更する必要があるということです。ほとんどの開発者は、長期間実行される可能性のあるタスク、例えば (ネットワーク接続が非常に低速な場合がある携帯電話では特に) ネットワークを介したデータ・アクセスなどをメインの UI スレッドで実行してはならないことを理解しています。そうしたタスクをメインの UI スレッドで実行すると、そのタスクが完了するまでアプリケーションがフリーズしてしまいます。実際、そのタスクの処理が 5 秒を越えると、Android オペレーティング・システムはそのアプリケーションに対し、「Application Not Responding (アプリケーションが応答していません)」という悪名高いダイアログを表示します (図 1)。
図 1. Android の悪名高いダイアログ「Application Not Responding (アプリケーションが応答していません)」
ユーザーのネットワーク接続がどの程度低速なのか、知ることはできません。こうしたダイアログが表示されることになる運命を避けるには、これらのタスクを別のスレッドで、少なくともメインの UI スレッド以外で実行する必要があります。Android アプリケーションは、大部分とは言わないまでも多くの場合、複数のスレッドを処理する必要があり、従って並行処理が必要です。またアプリケーションは多くの場合データをローカルに永続化する必要があり、そのため Android のローカル・データベースは魅力的な選択肢です。こうした 3 つのシナリオすべて (異なる複数のスレッド、並行処理、ローカルでのデータ永続化) に関して、Java 環境にはそうしたシナリオを処理するための標準的な方法がいくつか用意されています。しかしこれから説明するように、Android には Java 環境における標準的な方法とは異なる、いくつかの方法が用意されています。それでは、上述の 3 つのシナリオに関して Android に用意されているそれぞれの方法について、利点や欠点も併せて見ていきましょう。
Java プログラミングでは、ネットワークを介した呼び出しをするのは簡単です。おなじみの java.net パッケージには、そのためのクラスがいくつか含まれています。それらのクラスの大部分は Android にも含まれており、また実際、java.net.URL や java.net.URLConnection といったクラスは、他の任意の Java アプリケーションの場合と同じように使用することができます。しかし、Android には Apache HttpClient ライブラリーが含まれています。Android でネットワークの処理を行うためには、このライブラリーを使った方が適切です。たとえ通常の Java クラスを使う場合であっても、Android の実装ではやはり HttpClient を使用します。リスト 1 は、この Android でネットワークの処理を行う上で欠かせないライブラリーの使い方を示しています。(ソース・コードについてはすべて「ダウンロード」を参照。)
リスト 1. Android で Http Client ライブラリーを使う
private ArrayList<Stock> fetchStockData(Stock[] oldStocks)
throws ClientProtocolException, IOException{
StringBuilder sb = new StringBuilder();
for (Stock stock : oldStocks){
sb.append(stock.getSymbol());
sb.append('+');
}
sb.deleteCharAt(sb.length() - 1);
String urlStr =
"http://finance.yahoo.com/d/quotes.csv?f=sb2n&s=" +
sb.toString();
HttpClient client = new DefaultHttpClient();
HttpGet request = new HttpGet(urlStr.toString());
HttpResponse response = client.execute(request);
BufferedReader reader = new BufferedReader(
new InputStreamReader(response.getEntity().getContent()));
String line = reader.readLine();
int i = 0;
ArrayList<Stock> newStocks = new ArrayList<Stock>(oldStocks.length);
while (line != null){
String[] values = line.split(",");
Stock stock = new Stock(oldStocks[i], oldStocks[i].getId());
stock.setCurrentPrice(Double.parseDouble(values[1]));
stock.setName(values[2]);
newStocks.add(stock);
line = reader.readLine();
i++;
}
return newStocks;
}
|
このコードでは、Stock オブジェクトの配列を取得しています。これらの Stock オブジェクトは基本的なデータ構造オブジェクトであり、ユーザーが所有する株式に関する情報 (銘柄、株価など) と、もっと個人的な情報 (その株式に対してユーザーがいくら支払ったか、など) を保持しています。ユーザーは HttpClient クラスを使って Yahoo Finance から動的データ (例えば、その株式の最新株価など) を取得します。HttpClient は HttpUriRequest を引数に取り、この場合には HttpUriRequest のサブクラスである HttpGet を使っています。同様に、リモート・サーバーにデータを POST しなければならないときのために、HttpPost クラスがあります。クライアントから HttpResponse を取得すると、そのレスポンスに含まれている InputStream にアクセスし、そのストリームをバッファーするとともに解析して株価データを取得します。
ここまでで、ネットワーク経由でデータを取得する方法を理解できたので、このデータとマルチスレッドを利用して賢明な方法で Android の UI を更新する方法を調べてみましょう。
リスト 1 のコードをアプリケーションのメイン UI スレッドで実行すると、ユーザーのネットワークの速度によっては「Application Not Responding (アプリケーションが応答していません)」というダイアログが表示される可能性があります。そのため、このデータを取得するためのスレッドを必ず生成する必要があります。リスト 2 は、その 1 つの方法を示しています。
リスト 2. 不完全なマルチスレッド (こんなことをしてはいけません。これでは動作しません!)
private void refreshStockData(){
Runnable task = new Runnable(){
public void run() {
try {
ArrayList<Stock> newStocks =
fetchStockData(stocks.toArray(
new Stock[stocks.size()]));
for (int i=0;i<stocks.size();i++){
Stock s = stocks.get(i);
s.setCurrentPrice(
newStocks.get(i).getCurrentPrice());
s.setName(newStocks.get(i).getName());
refresh();
}
} catch (Exception e) {
Log.e("StockPortfolioViewStocks",
"Exception getting stock data", e);
}
}
};
Thread t = new Thread(task);
t.start();
}
|
リスト 2 のキャプションには、不完全なコードであることを示す見出しを付けましたが、実際にリスト 2 のコードは不完全です。この簡単な例では、リスト 1 の fetchStockData メソッドを呼び出すために、fetchStockData メソッドを Runnable オブジェクトの中にラップして新しいスレッドで実行しています。この新しいスレッドの中で、外側にある Activity (UI を作成するクラス) のメンバー変数である stocks にアクセスしています。名前からもわかるように、stocks は Stock オブジェクトのデータ構造 (この場合は java.util.ArrayList) です。別の言い方をするなら、この方法では、メインの UI スレッドと、(リスト 2 の中で呼び出される) 生成されたスレッド、という 2 つのスレッドの間でデータを共有しています。生成されたスレッドの中で共有データを変更した場合には、Activity オブジェクトの refresh メソッドを呼び出すことで UI を変更します。
Java Swing アプリケーションのプログラムを作成したことがある人ならば、このようなパターンを使ったことがあるかもしれません。しかしこの方法は Android では使えません。生成されたスレッドは、まったく UI を変更できないからです。では、UI をフリーズさせることなくデータを取得し、データを取得したら UI を変更できるようにするにはどのようにすればよいのでしょう。android.os.Handler クラスを使用すると、スレッド間を調整したりスレッド間で通信したりすることができます。リスト 3 は、Handler を使用するように変更された refreshStockData メソッドを示しています。
リスト 3. (
Handlerを使用することで) 実際に動作するマルチスレッド
private void refreshStockData(){
final ArrayList<Stock> localStocks =
new ArrayList<Stock>(stocks.size());
for (Stock stock : stocks){
localStocks.add(new Stock(stock, stock.getId()));
}
final Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
for (int i=0;i<stocks.size();i++){
stocks.set(i, localStocks.get(i));
}
refresh();
}
};
Runnable task = new Runnable(){
public void run() {
try {
ArrayList<Stock> newStocks =
fetchStockData(localStocks.toArray(
new Stock[localStocks.size()]));
for (int i=0;i<localStocks.size();i++){
Stock ns = newStocks.get(i);
Stock ls = localStocks.get(i);
ls.setName(ns.getName());
ls.setCurrentPrice(ns.getCurrentPrice());
}
handler.sendEmptyMessage(RESULT_OK);
} catch (Exception e) {
Log.e("StockPortfolioViewStocks",
"Exception getting stock data", e);
}
}
};
Thread dataThread = new Thread(task);
dataThread.start();
}
|
リスト 2 とリスト 3 の間には 2 つの大きな違いがあります。明らかな違いは Handler があることです。2 番目の違いは、生成されたスレッドの中では UI を変更していないことです。UI を変更する代わりに Handler にメッセージを送信し、Handler が UI を変更しています。またスレッドの中で、先ほどとは異なり、stocks メンバー変数を変更していないことに注意してください。stocks メンバー変数を変更する代わりに、データのローカル・コピーを変更しています。こうすることが必ず必要なわけではありませんが、こうした方が安全です。
リスト 3 は、実は並行プログラミングで非常に一般的なパターンを示しています。つまりデータをコピーして新しいスレッドに渡し、そのスレッドが何らかの長期実行タスクを実行して結果のデータをメインの UI スレッドに渡し、そのデータを使ってメインの UI スレッドを更新する、というパターンです。このパターンを Android で行う場合、メインの通信メカニズムが Handlers であり、また Handlers によってそのパターンの実装が容易になります。ただしリスト 3 にはまだ多くのボイラープレート・コードが含まれています。幸い Android には、そうしたボイラープレート・コードの大部分をカプセル化して省略するための手段が用意されています。リスト 4 はそれを示しています。
リスト 4.
AsyncTask を使ってマルチスレッド動作を容易にする
private void refreshStockData() {
new AsyncTask<Stock, Void, ArrayList<Stock>>(){
@Override
protected void onPostExecute(ArrayList<Stock> result) {
ViewStocks.this.stocks = result;
refresh();
}
@Override
protected ArrayList<Stock> doInBackground(Stock... stocks){
try {
return fetchStockData(stocks);
} catch (Exception e) {
Log.e("StockPortfolioViewStocks", "Exception getting stock data", e);
}
return null;
}
}.execute(stocks.toArray(new Stock[stocks.size()]));
}
|
これを見るとわかるように、リスト 4 に含まれるボイラープレート・コードはリスト 3 よりも大幅に少なくなっています。スレッドや Handlers をまったく作成していません。スレッドや Handlers の作成はすべて、AsyncTask を使ってカプセル化されています。AsyncTask を作成するためには、doInBackground メソッドを実装する必要があります。この doInBackground メソッドは必ず別のスレッドで実行されるため、長期間実行されるタスクを自由に呼び出すことができます。doInBackground メソッドの入力型は、ユーザーが作成する AsyncTask の型パラメーターによって指定されます。この場合、1 番目の型パラメーターは Stock だったので、doInBackground は渡された Stock オブジェクトの配列を取得します。同様に、doInBackground は ArrayList<Stock> を返します。これは ArrayList<Stock> が AsyncTask の 3 番目の型パラメーターであるからです。またこの例では、onPostExecute メソッドをオーバーライドすることにしました。onPostExecute はオプションのメソッドであり、doInBackground から返されるデータを使った処理をする必要がある場合には onPostExecute メソッドを実装する必要があります。このメソッドは必ずメインの UI スレッドで実行されるため、UI を変更するためのメソッドとして理想的です。
AsyncTask を利用することで、マルチスレッドのコードを大幅に単純化することができると同時に、開発過程に数多く潜在する並行処理の落とし穴がなくなります。ただし AsyncTask を使ったとしても、やはりいくつか問題があることがわかります。例えば、AsyncTask オブジェクトの doInBackground メソッドを実行中に機器の縦横方向が変更されたらどうなるのでしょう。こうしたケースに対応するための手法については「参考文献」のリンクを参照してください。
では、Android と通常の Java の流儀とが大幅に異なる一般的なタスクの別の例として、データベースの処理に移りましょう。
Android の非常に便利な機能の 1 つとして、Android にはローカルのリレーショナル・データベースがあります。確かにローカル・ファイルにデータを保管することはできますが、多くの場合は RDBMS (Relational Database Management System) を使ってデータを保管した方が便利です。Android では、よく使われている SQLite を使ってデータベース処理を行うことができます。というのも、この SQLite データベースは Android のような組み込みシステム用に高度に最適化されているからです。SQLite データベースは Android のコア・アプリケーションの多くに使用されています。例えば、ユーザーのアドレス帳は SQLite データベースに保管されます。ここで、Android が Java をベースに実装されていることを考えると、これらのデータベースに JDBC を使ってアクセスするのだと思う人がいるかもしれません。しかし驚いたことに、Android には JDBC API の大部分を構成する java.sql パッケージと javax.sql パッケージまで含まれています。ただし、ローカルの Android データベースを扱う上では、それらのパッケージは何も役に立たないことがわかります。それらのパッケージの代わりに、android.database パッケージと android.database.sqlite パッケージを使う必要があります。リスト 5 は、これらのパッケージのクラスを使ってデータの保管と取得を行う例を示しています。
リスト 5. Android でのデータベース・アクセス
public class StocksDb {
private static final String DB_NAME = "stocks.db";
private static final int DB_VERSION = 1;
private static final String TABLE_NAME = "stock";
private static final String CREATE_TABLE = "CREATE TABLE " +
TABLE_NAME + " (id INTEGER PRIMARY KEY, symbol TEXT, max_price DECIMAL(8,2), " +
"min_price DECIMAL(8,2), price_paid DECIMAL(8,2), " +
"quantity INTEGER)";
private static final String INSERT_SQL = "INSERT INTO " + TABLE_NAME +
" (symbol, max_price, min_price, price_paid, quantity) " +
"VALUES (?,?,?,?,?)";
private static final String READ_SQL = "SELECT id, symbol, max_price, " +
"min_price, price_paid, quantity FROM " + TABLE_NAME;
private final Context context;
private final SQLiteOpenHelper helper;
private final SQLiteStatement stmt;
private final SQLiteDatabase db;
public StocksDb(Context context){
this.context = context;
helper = new SQLiteOpenHelper(context, DB_NAME, null,
DB_VERSION){
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion,
int newVersion) {
throw new UnsupportedOperationException();
}
};
db = helper.getWritableDatabase();
stmt = db.compileStatement(INSERT_SQL);
}
public Stock addStock(Stock stock){
stmt.bindString(1, stock.getSymbol());
stmt.bindDouble(2, stock.getMaxPrice());
stmt.bindDouble(3, stock.getMinPrice());
stmt.bindDouble(4, stock.getPricePaid());
stmt.bindLong(5, stock.getQuantity());
int id = (int) stmt.executeInsert();
return new Stock (stock, id);
}
public ArrayList<Stock> getStocks() {
Cursor results = db.rawQuery(READ_SQL, null);
ArrayList<Stock> stocks =
new ArrayList<Stock>(results.getCount());
if (results.moveToFirst()){
int idCol = results.getColumnIndex("id");
int symbolCol = results.getColumnIndex("symbol");
int maxCol = results.getColumnIndex("max_price");
int minCol = results.getColumnIndex("min_price");
int priceCol = results.getColumnIndex("price_paid");
int quanitytCol = results.getColumnIndex("quantity");
do {
Stock stock = new Stock(results.getString(symbolCol),
results.getDouble(priceCol),
results.getInt(quanitytCol),
results.getInt(idCol));
stock.setMaxPrice(results.getDouble(maxCol));
stock.setMinPrice(results.getDouble(minCol));
stocks.add(stock);
} while (results.moveToNext());
}
if (!results.isClosed()){
results.close();
}
return stocks;
}
public void close(){
helper.close();
}
}
|
リスト 5 のクラスは株式の情報の保管に使われる SQLite データベースを完全にカプセル化しています。作業の対象となる組み込みのデータベースはアプリケーションに使用されるだけではなく、作成もアプリケーションによって行われるため、データベースを作成するためのコードを提供する必要があります。Android にはそのために、SQLiteOpenHelper という便利な抽象ヘルパー・クラスが用意されています。SQLiteOpenHelper を実装するためには、この抽象クラスを継承し、そのクラスの onCreate メソッドの中で、データベースを作成するためのコードを提供します。このヘルパーのインスタンスを作成すると、SQLiteDatabase のインスタンスが得られ、このインスタンスを使って任意の SQL 文を実行することができます。
データベース・クラスにはコンビニエンス・メソッドが 2 つあります。一方のメソッドは、新しい株式をデータベースに保管するための addStock メソッドです。このメソッドでは、SQLiteStatement インスタンスを使用していることに注意してください。SQLiteStatement は java.sql.PreparedStatement と似ています。SQLiteStatement インスタンスがクラスのコンストラクターの中でコンパイルされることに注意してください。そのため、addStock が呼び出されるたびに SQLiteStatement インスタンスを再利用することができます。addStock が呼び出されるたびに、SQLiteStatement の変数 (INSERT_SQL ストリングの中の疑問符) は、addStock に渡されるデータにバインドされます。これも、おそらく読者が JDBC でおなじみの PreparedStatement ととてもよく似ています。
もう一方のコンビニエンス・メソッドは getStocks です。名前からわかるように、getStocks はデータベースからすべての株式を取得します。この場合も、JDBC の場合とまったく同じように SQL ストリングを使うことに注意してください。これを SQLiteDatabase クラスの rawQuery メソッドを使って行います。このクラスにもいくつかのクエリー・メソッドがあり、これらのメソッドを使用すると SQL を使用せずに直接データベースにクエリーを実行することができます。こうしたさまざまなメソッドはすべて、java.sql.ResultSet にとてもよく似た Cursor オブジェクトを返します。クエリーによって返された何行ものデータの上で、Cursor を移動することができます。それぞれの行の上で、getInt メソッドや getString メソッド、その他のメソッドを使用することで、クエリーの実行対象のデータベース・テーブルのさまざまな列に関連付けられた値を取得します。これは、今度は ResultSet ととてもよく似ています。また ResultSet と同じように、Cursor を使い終わったら Cursor を閉じることが重要です。Cursor を閉じないと、すぐにメモリーが足りなくなり、アプリケーションが異常終了する可能性があります。
ローカルのデータベースに対してクエリーを実行するプロセスは時間がかかる可能性があります。データの行数が多い場合や、複数のテーブルを結合する複雑なクエリーを実行する必要がある場合はなおさらのことです。データベースに対するクエリーの実行や、データの挿入に 5 秒を超える時間がかかり、「Application Not Responding (アプリケーションが応答していません)」というダイアログが表示されるようなことはありえませんが、それでもコードがデータの読み書きを行っている間に UI がフリーズする可能性があるのは望ましいことではありません。当然ですが、そうした状況を避ける最も容易な方法は、AsyncTask を使用することです。その例を示しているのがリスト 6 です。
リスト 6. 別のスレッドでデータベースに挿入する
Button button = (Button) findViewById(R.id.btn);
button.setOnClickListener(new OnClickListener(){
public void onClick(View v) {
String symbol = symbolIn.getText().toString();
symbolIn.setText("");
double max = Double.parseDouble(maxIn.getText().toString());
maxIn.setText("");
double min = Double.parseDouble(minIn.getText().toString());
minIn.setText("");
double pricePaid =
Double.parseDouble(priceIn.getText().toString());
priceIn.setText("");
int quantity = Integer.parseInt(quantIn.getText().toString());
quantIn.setText("");
Stock stock = new Stock(symbol, pricePaid, quantity);
stock.setMaxPrice(max);
stock.setMinPrice(min);
new AsyncTask<Stock,Void,Stock>(){
@Override
protected Stock doInBackground(Stock... newStocks) {
// There can be only one!
return db.addStock(newStocks[0]);
}
@Override
protected void onPostExecute(Stock s){
addStockAndRefresh(s);
}
}.execute(stock);
}
});
|
まず、あるボタンに対するイベント・リスナーを作成することから始めます。このボタンをユーザーがクリックした時に、さまざまなウィジェット (具体的には EditText ウィジェット) から株価データを読み取り、新しい Stock オブジェクトを追加します。doInBackground メソッドを使って AsyncTask を作成し、リスト 5 の addStock メソッドを呼び出します。こうすることで、addStock はバックグラウンド・スレッドで実行され、メインの UI スレッドでは実行されません。addStock が処理を完了したら、データベースの新しい Stock オブジェクトを addStockAndRefresh メソッドに渡し、この addStockAndRefresh メソッドがメインの UI スレッドで実行されるようにします。
Android では Java 環境にある数多くの API のサブセットしかサポートしていないものの、機能に関しては Android に決して不足がないということを、この記事では説明しました。例えば Android では、ネットワーク機能に関しておなじみの API を完全に実装しているだけではなく、さらに便利な方法もいくつか提供しています。あるいは並行処理に関して、Android には API や規則が追加されており、これらに従わなければなりません。そしてデータベース・アクセスに関しては、Android はまったく異なる方法を提供していますが、そこではお馴染みの概念が数多く使われています。このような方法の違いは、標準的な Java 技術と Android の Java 技術との間の偶然の違いではありません。こうした Android の方法は、Android 開発での基本的なビルディング・ブロックの一部を形成しています。
| 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|---|---|---|
| Article source code | StockPortfolio.zip | 18KB | HTTP |
学ぶために
- 「Android で XML を扱う」(Michael Galpin 著、developerWorks、2009年6月) を読み、Android SDK を使って XML を構文解析するためのさまざまな方法について学んでください。
- 「Develop Android applications with Eclipse」(Frank Ableson 著、developerWorks、2008年2月) を読んでください。このチュートリアルでは、アプリケーション開発を加速する優れた Eclipse プラグインを Android が提供していることを学びます。
- アプリケーションがネットワークを介してデータを取得する場合には、この記事、「Android によるネットワーキング」(Frank Ableson 著、developerWorks、2009年6月) を読む必要があります。
- Android を初めて扱うのであれば、developerWorks の記事、「Android 開発入門」(Frank Ableson 著、developerWorks、2009年5月) が出発点として最適です。
- 「Android と iPhone のブラウザー戦争: 第 1 回 WebKit による救いの手」(Frank Ableson 著、developerWorks、2009年12月) では、モバイル・ブラウザーの機能を活用する方法を解説しています。
- このブログ・エントリー、Introducing Droid-Fu では、AsyncTask の落とし穴のいくつかについて、またそれらの落とし穴を回避する方法を解説しています。
- この記事の著者による他の記事を読んでください (Michael Galpin 著、developerWorks、2006年4月から現在まで)。Android、XML、HTML 5、Eclipse、Apache Geronimo、Ajax、他の Google API、その他の技術が解説されています。
- developerWorks の XML ゾーンには、XML の領域でのスキルを磨くためのリソースが豊富に用意されています。
- My developerWorks で developerWorks のエクスペリエンスをパーソナライズしてください。
- XML および関連技術において IBM 認定技術者になる方法については、IBM XML certification を参照してください。
- developerWorks の XML ゾーンを XML の技術ライブラリーとして利用してください。広範な話題を網羅した技術記事やヒント、チュートリアル、技術標準、IBM Redbooks などが用意されています。また XML に関する他のヒント記事も読んでください。
- developerWorks の Technical events and webcasts で最新情報を入手してください。
- 今すぐ developerWorks に参加し、developerWorks を Twitter でフォローしてください。
- developerWorks podcasts ではソフトウェア開発者のための興味深いインタビューや議論を聞くことができます。
製品や技術を入手するために
- Android SDK をダウンロードしてください。この記事の演習にはバージョン 1.5 またはそれ以降が必要です。
- 完全なプラットフォームであり、ランタイム環境でもある Java Development Kit をダウンロードしてください。この記事ではJDK 1.6.0_17 を使用しました。
- Android オープンソース・プロジェクトのサイトを訪れ、Android のオープンソース・コードを入手してください。
- IBM 製品の評価版をダウンロードするか、あるいは IBM SOA Sandbox のオンライン試用版で、DB2®、Lotus®、Rational®、Tivoli®、WebSphere® などが提供するアプリケーション開発ツールやミドルウェア製品を試してみてください。
議論するために
- XML zone discussion forums に参加してください。これらのフォーラムでは XML を中心に議論が行われています。
- developerWorks blogs に参加してください。

Michael Galpin は eBay のアーキテクトであり、developerWorks に頻繁に寄稿しています。彼は JavaOne、EclipseCon、AjaxWorld など、さまざまな技術カンファレンスで講演を行っています。彼が次に取り組もうとしていることを知るには、Twitter で @michaelg をフォローしてください。