• 追加された行はこの色です。
  • 削除された行はこの色です。
[[勉強会]]

* XMLとSQLiteを使用する [#mdc78c0d]

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

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

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

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


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

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

** 手順2:プロジェクトを作成しましょう [#h9cf8a87]
Twitterクライアント用のプロジェクトを作成します。
- "File -> New -> Project"を選択して、開いたダイアログで"Android Poroject"を選択して、"Next"ボタンを押します。
- 開いたダイアログで、以下のように入力してFinishボタンを押します。
-- Project Name: HelloXML
-- Package name: net.it4myself.helloxml
-- Activity Name: .HelloXMLActivity
-- Application Name: ハローXML


** 手順3:マニフェストファイルを設定する [#j007d933]

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

** 手順4:レイアウトを決定する [#x055f760]

- このプロジェクトは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で作ったプロジェクトをビルドパスに加える [#m02d1021]

- [[Androidでorg.apache.http.clientを使用する>勉強会/HttpClient]]の手順5に従い、net.it4myself.utilパッケージをビルドパスに加えます。

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

- ※筆者はTwitterにあまり詳しくないので、用語など間違えていたらごめんなさい。
- Twitter APIは英文らしいので、[[Twitter API 仕様書 日本語訳:http://watcher.moe-nifty.com/memo/docs/twitterAPI.txt]]を使って仕様を確認しましょう。
- Twitterユーザ全体のタイムラインである、パブリックタイムラインをXMLで取得するにはhttp://twitter.com/statuses/public_timeline.xmlからXMLを読む必要があります。パブリックタイムラインは、BASIC認証を使う必要はありません。
- 自分がフォローしている友達だけのタイムラインである、フレンドタイムラインをXMLで取得するにはhttp://twitter.com/statuses/friends_timeline.xmlからXMLを読む必要があります。この際、BASIC認証を使って自分のTwitter用IDとパスワードを一緒に渡す必要があります。
- 先にブラウザを使ってどんなXMLが返ってくるのか確認しておくのが吉です。
- BASIC認証用にRestfulClientクラスを改良しておいたので、確認してみてください。RestfulClient.basicauth_username = "YOUR ID";とRestfulClient.basicauth_password = "YOUR PASSWORD";でTwitterのID/パスワードを設定できます。RestfulClient.basicauth_username = "";とするとBASIC認証は使わなくなります。

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

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を使って表示する。


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

 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.basicauth_username = "";
             	RestfulClient.basicauth_password = "";
             	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.basicauth_username = user;
             	RestfulClient.basicauth_password = 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の使い方を追ってみる [#td67fd6b]

- 力尽きたのでまた今度
 
-----
[[勉強会]]