勉強会

XMLとSQLiteを使用する

  • ※現在レビュー中です。内容は間違いを含んでいる可能性があります。

WebAPIなどから取得したXMLを解析(パース)して、内容を取り出すアプローチは、大きく分けてDOMとSAXがあります。 DOMはXMLをツリー上に展開し、自由に読み書きできるものの、比較的多くのメモリを消費します。SAXはXMLを先頭から読み込んで処理していくため、高速でメモリ消費も少ないのですが、複雑な構造のXMLを読み込む場合に制御が複雑になるのと、その性質上汎用的に作るのが難しいため、ケースバイケースで使い分けることになるかと思います。

一方、SQLiteについては強力で使いやすく、Web上にもコードを含む資料がたくさんあるものの、android ver1.0に至るまでの過程でAPIが変化してしまったため、今では動かないコードも散見されます。

ここでは、前のチュートリアルに引き続き、org.w3c.domパッケージ(?)を使ってXMLをパースしているRestfulClientクラスと、アプリケーションの設定値保持などに使用可能な、SQLiteを使った永続化機能付きのHashとしてのSettingsクラスを使って、Twitterクライアントを作ってみます。

  • 確認中:XMLパーサはorg.w3c.domを使っているという表現で良いでしょうか? これってインターフェイスしか定義してませんよね?

手順1:公開されているandroid用のユーティリティのソースファイルをダウンロードし、Eclipseに登録する

  • Androidでorg.apache.http.clientを使用するの手順1に従い、GitHubから最新版ユーティリティのソースファイルをダウンロードし、Eclipseにセットアップします。
  • RestfulClientクラスにBASIC認証機能を付けたので、先のチュートリアルを試した方も、再度ダウンロードし直してください。

手順2:プロジェクトを作成しましょう

Twitterクライアント用のプロジェクトを作成します。

  • "File -> New -> Project"を選択して、開いたダイアログで"Android Poroject"を選択して、"Next"ボタンを押します。
  • 開いたダイアログで、以下のように入力してFinishボタンを押します。
    • Project Name: HelloXML
    • Package name: net.it4myself.helloxml
    • Activity Name: .HelloXMLActivity
    • Application Name: ハローXML

手順3:マニフェストファイルを設定する

  • このプロジェクトではインターネットにアクセスするため、パーミッションの使用を許可する必要があります。
  • もうソース無くても大丈夫でしょう?

手順4:レイアウトを決定する

  • このプロジェクトは2つのレイアウトファイルを使います。
  • 画面のレイアウトを決定するために、res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical"
    xmlns:android="http://schemas.android.com/apk/res/android" 
>
<LinearLayout
    android:layout_width="fill_parent"
    android:layout_height="wrap_content" 
>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="User:"
    >
    </TextView>
    <EditText
        android:id="@+id/username_edit_id"
        android:layout_width="200px"
        android:layout_height="wrap_content"
        android:textSize="18sp"
    >
    </EditText>
</LinearLayout>
<LinearLayout
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Password:"
    >
    </TextView>
    <EditText
        android:id="@+id/password_edit_id"
        android:layout_width="200px"
        android:layout_height="wrap_content"
        android:textSize="18sp"
    >
    </EditText>
</LinearLayout> 
<LinearLayout
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
>
    <Button
        android:id="@+id/publicTimeline_button_id"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="PublicTimeline"
    >
    </Button>
    <Button
        android:id="@+id/myTimeline_button_id"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="My Timeline"
    >
    </Button>
</LinearLayout>
<ListView
    android:id="@+id/android:list"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
>
</ListView>
</LinearLayout>
  • リストの各行のレイアウトのために、res/layout/list_row.xml
<?xml version="1.0" encoding="utf-8"?> 
<TextView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
/>
  • main.xmlのListViewのidを"@+id/android:list" とするのは、黒魔術のおまじないかもしれません。(後に説明しますが、データを用意するだけで良きに計らってくれるので)

手順5:手順1で作ったプロジェクトをビルドパスに加える

手順6:パブリックタイムラインとフレンドタイムラインを取得するため、Twitter APIを理解する

  • ※筆者はTwitterにあまり詳しくないので、用語など間違えていたらごめんなさい。
  • Twitter APIは英文らしいので、Twitter API 仕様書 日本語訳版を使って仕様を確認しましょう。
  • Twitterユーザ全体のタイムラインである、パブリックタイムラインをXMLで取得するにはhttp://twitter.com/statuses/public_timeline.xmlからXMLを読む必要があります。パブリックタイムラインは、BASIC認証を使う必要はありません。
  • 自分がフォローしている友達だけのタイムラインである、フレンドタイムラインをXMLで取得するにはhttp://twitter.com/statuses/friends_timeline.xmlからXMLを読む必要があります。この際、BASIC認証を使って自分のTwitter用IDとパスワードを一緒に渡す必要があります。BASIC認証でID/パスワードを送信するため、httpよりは暗号化されたhttpsの方が好ましいかも知れません。RestfulClientクラス、TwitterAPIサーバ共にSSLに対応しているので、接続先スキーマをhttpsにするだけで使えます。
  • 先にブラウザを使ってどんなXMLが返ってくるのか確認しておくのが吉です。
  • BASIC認証用にRestfulClientクラスを改良しておいたので、確認してみてください。RestfulClient.basicAuthUsername = "YOUR ID";とRestfulClient.basicAuthPassword = "YOUR PASSWORD";でTwitterのID/パスワードを設定できます。RestfulClient.basicAuthUsername = "";とするとBASIC認証は使わなくなります。

手順7:パブリックタイムラインとフレンドタイムラインを取得する

Settingsクラスについては

  • mSettings = new Settings(this, null);で生成
  • String user = mSettings.get("user");でuserに関連づけてある値の取得
  • mSettings.set("user", "hoge");でuserにhogeを設定
  • 設定したタイミングでSQLiteに格納するので、途中でアプリケーションが終了しても、値は保持されます。
  • これらを利用して、入力されたID/パスワードを保存しておき、次回起動時に復元させる。

また、XMLのパースについては

  • RestfulClient.Get(uri, null, mFactory.newDocumentBuilder());として、Documentとして結果を取得する
  • RestfulClient.RemoveEmptyNodes(result.getDocumentElement());を使って、改行だけのテキストノードを削除する。(削除しなくても構いませんが、削除した方が理解しやすいと思います)
  • Node.getChildNodes();で、現在のノードの子ノードリストを取得し、ループにかける
  • Node.getNodeName();で欲しいタグ名を探す
  • 欲しいタグを見つけたらNode.getFirstChild().getNodeValue();でテキストを取得する。getFirstChild()を入れるのは、テキストは現在のノードの子ノード(テキストノード)に格納されているため。
  • これらを利用し、Twitterでの発言内容とユーザ名を取得し、ListViewを使って表示する。
  • ※android ver1.0時点ではorg.w3c.domパッケージ(?)はバグが多いらしく、一部オプションが動いてくれなかったため、空テキストノード削除用にRemoveEmptyNodes()も用意しました。DOMオブジェクトを扱いたい場合はお使いください。

実際にコーディングしてみます。

package net.it4myself.helloxml;

import java.util.ArrayList;
import java.util.List;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import net.it4myself.helloxml.R;
import net.it4myself.util.RestfulClient;
import net.it4myself.util.Settings;
import android.app.ListActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

public class helloXMLActivity extends ListActivity { 
    private EditText mUsernameEdit;
    private EditText mPasswordEdit;
    private DocumentBuilderFactory mFactory;
    private Settings mSettings;
	
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        mUsernameEdit = (EditText)findViewById(R.id.username_edit_id);
        mPasswordEdit = (EditText)findViewById(R.id.password_edit_id);
        mFactory = DocumentBuilderFactory.newInstance();
        mSettings = new Settings(this, null);
        String user = mSettings.get("user");
        if(null != user){
            mUsernameEdit.setText(user);
        }
        String pass = mSettings.get("pass");
        if(null != pass){
            mPasswordEdit.setText(pass);
        }
        
        Button b1 = (Button)findViewById(R.id.publicTimeline_button_id);
        b1.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                RestfulClient.basicAuthUsername = "";
                RestfulClient.basicAuthPassword = "";
                showTimeline("https://twitter.com/statuses/public_timeline.xml");
            }
        });
        Button b2 = (Button)findViewById(R.id.myTimeline_button_id);
        b2.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                String user = mUsernameEdit.getText().toString();
                String pass = mPasswordEdit.getText().toString();
                mSettings.set("user", user);
                mSettings.set("pass", pass);
                RestfulClient.basicAuthUsername = user;
                RestfulClient.basicAuthPassword = pass;
                showTimeline("https://twitter.com/statuses/friends_timeline.xml");
            }
        });
    }
    
    private void showTimeline(String uri){
        try { 
	     Document result = RestfulClient.Get(uri, null, mFactory.newDocumentBuilder());
	     Node removedNode = RestfulClient.RemoveEmptyNodes(result.getDocumentElement());
	     NodeList statusNodes, l1Nodes, l2Nodes;
	     Node statusNode, l1Node, l2Node;
	     List<String> items = new ArrayList<String>();
	     StringBuilder sb;
			
	     statusNodes = removedNode.getChildNodes();
	     int statusCounter = statusNodes.getLength();
	     for(int i=0; i < statusCounter; i++){
                statusNode = statusNodes.item(i);
                sb = new StringBuilder();
		 l1Nodes = statusNode.getChildNodes();
		 int l1Counter = l1Nodes.getLength();
		 for(int ii=0; ii < l1Counter; ii++){
		     l1Node = l1Nodes.item(ii);
		     String nodeName = l1Node.getNodeName();
		     if(nodeName.equals("text")){
		         Log.v("helloxml", "l1 text node value: " + l1Node.getFirstChild().getNodeValue());
		         sb.append(l1Node.getFirstChild().getNodeValue()).append(" by ");
		     } else if(nodeName.equals("user")){
		         l2Nodes = l1Node.getChildNodes();
			 int l2Counter = l2Nodes.getLength();
			 for(int iii=0; iii < l2Counter; iii++){
			     l2Node = l2Nodes.item(iii);
			     String userNodeName = l2Node.getNodeName();
			     if(userNodeName.equals("name")){
			         Log.v("helloxml", "l2 name node value: " + l2Node.getFirstChild().getNodeValue());
			         sb.append(l2Node.getFirstChild().getNodeValue());
				 break;
			     }
			 }
		     }
		 }
	         items.add(sb.toString());
		 // Log.v("helloxml", "item base: " + sb.toString());
	     }
	     ArrayAdapter<String> itemAdapter = new ArrayAdapter<String>(this, R.layout.list_row, items);
	     setListAdapter(itemAdapter); // ここで渡したアダプタがListViewに表示されるまでが黒魔術っぽい
    	 } catch (Exception e) {
	     e.printStackTrace();
	     Toast.makeText(this, "パスワード合ってる?", Toast.LENGTH_SHORT).show();
	 }
    }
}

手順7:SQLiteの使い方を追ってみる

  • android_utilsプロジェクトのsrcの下のnet.it4myself.util.Settings.javaを開いて、流れを追ってみましょう。
  • DatabaseHelperクラスを追って、ヘルパークラスを理解する
    • SQLiteOpenHelperクラスを継承したDatabaseHelperクラスを定義しています。SQLiteOpenHelperクラスは、データベースの作成・テーブルの作成・初期データ等のインサート・バージョン管理・読み書き用インスタンスの管理など、SQLiteを扱う上で便利な機能をまとめたヘルパークラスとなっています。このクラスを継承して個々のアプリケーションの設定を施します。
    • 初期データとしてseedという名前で、タイムスタンプを元にしたユニークな値を入れています。これはユーザ名などと掛け合わせて、同一クライアントからのアクセスであることを保証するためのシードとして使うために用意したものです。用途に合わなければ削除しても構いません。
  • setメソッドとgetメソッドは、一般的なセッター/ゲッターとして動作しますが、set時には値をSQLiteにも書き込んでいます。よって、どのタイミングでも設定値を保持できています。
  • hasKeyInDB()を追って、SELECT時の動作を確認する
    • ユーザの入力値に由来する値をSQLに含めるときは、SQLインジェクション対策などのため、SQLiteQueryBuilderクラスを使ってSQLを構築する方法が推奨されています。
    • SQLに使いたいパラメータをselectionArgsにまとめます。
    • データベースファイルを壊さないように、SELECT時にはmOpenHelper.getReadableDatabase()を使って、読み取り用のデータベースインスタンスを取得します。
    • qb.query()を実行すると、結果がCursor型で返ってくるので、cursor.moveToNext()などを使って各行を取得します。restore()メソッドあたりを参考にしてください。
  • insertToDB()を追って、INSERT時の動作を確認する
    • INSERT文に含める名前と値は、あらかじめContentValuesのインスタンスに登録しておきます。SQLインジェクション対策などのためです。
    • INSERTなど、データベースに書き込みを行うときは、mOpenHelper.getWritableDatabase()を使って書き込み用のデータベースインスタンスを取得しておきます。裏でロック処理などを行っているようです。
    • db.insert()を行うとINSERT文が実行され、追加された行数が返ります。
  • updateDB()とdelete()を追って、それぞれの動作を確認する
    • 基本的な考えはINSERT時と同じです。
    • db.update(TABLE_NAME, values, "key = ?", new String[] {key})のように、WHERE句のためのパラメーターは"?"を使ってバインドさせます。この例では?はkeyの内容に置き換わります。

宿題

  • このチュートリアルではタイムラインの取得と表示だけを扱っていますが、RestfulClientを使えば投稿も簡単にできます。投稿できるように改造してみましょう!
  • 既にお気づきかも知れませんが、パブリックタイムラインでは日本語を表示できるものの、フレンドタイムラインでは日本語が消えてしまいます。おそらくキャラクタセット(UTF8)の取り扱いがどこかでおかしくなっているのですが、Twitter側の問題なのかコード側の問題なのかわかりませんでした。(パブリックとフレンドのWebAPIのレスポンスを比べてみると、HTTPレスポンスヘッダーもちょっと違うので、このへんなのかなぁと) がんばって直せる人居ますか?

勉強会