Android アプリケーションのためのクールな Java 技術
並行処理、ネットワーキング、データベース・アクセスを Android に実装する
はじめに
この記事では、厄介な状況に対処するために 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 に用意されているそれぞれの方法について、利点や欠点も併せて見ていきましょう。
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 を更新する方法を調べてみましょう。
Android での並行処理の実際
リスト 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 でのデータベース接続
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 開発での基本的なビルディング・ブロックの一部を形成しています。
ダウンロード可能なリソース
- このコンテンツのPDF
- Article source code (StockPortfolio.zip | 18KB)
関連トピック
- 「Android で XML を扱う」(Michael Galpin 著、developerWorks、2009年6月) を読み、Android SDK を使って XML を構文解析するためのさまざまな方法について学んでください。
- アプリケーションがネットワークを介してデータを取得する場合には、この記事、「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 の落とし穴のいくつかについて、またそれらの落とし穴を回避する方法を解説しています。
- developerWorks の XML ゾーンには、XML の領域でのスキルを磨くためのリソースが豊富に用意されています。
- XML および関連技術において IBM 認定技術者になる方法については、IBM XML certification を参照してください。
- developerWorks の XML ゾーンを XML の技術ライブラリーとして利用してください。広範な話題を網羅した技術記事やヒント、チュートリアル、技術標準、IBM Redbooks などが用意されています。また XML に関する他のヒント記事も読んでください。
- 今すぐ 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® などが提供するアプリケーション開発ツールやミドルウェア製品を試してみてください。