Androidアプリのサーバとの通信

現状のAndroidアプリケーションは,ダミーの書籍データを表示しているだけで,キーワードによるデータベースの検索やデータベースに格納されている書籍情報の表示ができているわけではない。ここでは,アプリ上で入力されたキーワードをサーバに渡し,サーバから返される検索結果をスマホの画面に表示するように拡張していく。

サーバからのJSON形式による応答

先に構築したWebアプリケーションでは,サーバサイドからHTML形式のデータが応答として返され,それをWebブラウザで表示するという方式であった。このHTML文書には,キーワードに基づくデータベースの検索結果が含まれているので,同じサーバサイドプログラムをAndroidアプリから利用することも考えられるが,HTML形式は情報の表示方法を記述しているもので,本の情報自体を記述するにはうまく構造化がされていない。したがって,スマートフォンアプリケーションでは,クライアントとサーバがデータをやり取りするための方式として,JSON形式と呼ばれるものが多くのシステムで使われている。

例えば,JSON形式で今回の書籍情報を表現すると,以下のようになる。

[
    {'ID': 1, 'TITLE': 'Androidの基本', 'AUTHOR': '立命太郎', 'PUBLISHER': '立命出版', 'PRICE': 1000, 'ISBN': '1234567890'},
    {'ID': 2, 'TITLE': 'Androidの応用', 'AUTHOR': '立命次郎', 'PUBLISHER': '立命書籍', 'PRICE': 1200, 'ISBN': '2345678901'},
    {'ID': 3, 'TITLE': 'Androidのススメ', 'AUTHOR': '立命三郎', 'PUBLISHER': '立命プレス', 'PRICE': 1500, 'ISBN': '3456789012'}
]
これを見れば分かるように,3つの書籍の情報が配列として表現され,それぞれの書籍の情報は「キー:バリュー」の形式で表現されている。JSON (JavaScript Object Notation)は,軽量なデータ記述言語の一つであり,人間にとっても読み書きが容易で,簡単なプログラムで読み書きが行えるデータ形式である。

Pythonからは,以下のプログラムに示すように,表現すべきデータを辞書のリストで表現し,print文で出力すると,簡単にJSON形式のデータが得られる。

books = []

book1 = {"ID": 1,\
          "TITLE": "Androidの基本",\
          "AUTHOR": "立命太郎",\
          "PUBLISHER": "立命出版",\
          "PRICE": 1000,\
          "ISBN": "1234567890"}
books.append(book1)

book2 = {"ID": 2,\
          "TITLE": "Androidの応用",\
          "AUTHOR": "立命次郎",\
          "PUBLISHER": "立命書籍",\
          "PRICE": 1200,\
          "ISBN": "2345678901"}
books.append(book2)

book3 = {"ID": 3,\
          "TITLE": "Androidのススメ",\
          "AUTHOR": "立命三郎",\
          "PUBLISHER": "立命プレス",\
          "PRICE": 1500,\
          "ISBN": "3456789012"}
books.append(book3)

print(books)

演習課題9(→提出先)

以下のURLにアクセスすることにより,キーワード(この場合は"python")で指定された書籍の情報がJSON形式で返ってくることをWebブラウザで確認せよ。この時,例えばキーワードを"Java"にしてみたりするなど,キーワードによって返されるデータが変わることも確認すること。

http://www.cm.is.ritsumei.ac.jp/class/saproglab/server/cgi-bin/booksearch_json.py?query=python

Activity間(画面間)のデータ渡し

実際のAndroidアプリケーションでは複数の画面間(Activity間)で遷移が行われる。この場合,現在表示されている画面から次の画面にデータを渡すことが必要な場合がある。例えば,サーバから検索結果を取得して表示できるようにするには,入力されたキーワードや書籍のタイトル情報などを画面間で渡し,前の画面で使われていた情報を次の画面でも使えるようにする必要がある。下図は,今回構築する書籍管理アプリ全体のデータフローを表したものである。

画面間の遷移には,Intentによって「キー:バリュー」の形式でデータ渡しを行うことができる。キーワード入力画面から検索結果画面へ入力されたキーワードを渡した上でResultActivityを開くように,MainActivityのonSearchButtonClicked()メソッドを以下のように書き換える。

Intent intent = new Intent(this, ResultActivity.class);
EditText keywordText = findViewById(R.id.keywordText);
intent.putExtra("QUERY", keywordText.getText().toString());
startActivity(intent);
このプログラムの2行目では,キーワード入力のためのユーザインタフェースコンポーネント(EditText)への参照をIDによって取得し,入力されている文字列を"QUERY"というキーのバリューとしてIntentに登録している。

遷移先のActivityでは,以下のようなプログラムで渡されたデータを受け取ることができる。今回は,これをResultActivityのonCreate()メソッド内に記述する。

Intent intent = getIntent();
String query = intent.getStringExtra("QUERY");

サーバとの通信

まず,Androidアプリからインターネット接続を許可するために,AndroidManifest.xmlというファイル(下図の左上付近に見える)に以下の行を追加する(<manifest>タグの要素として)。

<uses-permission android:name="android.permission.INTERNET" />

また,HTTPSではなく,HTTPでの通信を許可するために,以下の属性を<application>タグに追加する(今回は簡単化のために暗号化無しでの通信を許可するが,実際のアプリケーションでは推奨されない)。

android:usesCleartextTraffic="true"

上のデータフロー図に示しているように,サーバとの通信はResultActivityから行う。本来であれば,ResultActivityクラスにサーバとの通信処理を書けばよさそうであるが,サーバとの通信処理は,非同期処理(アプリの画面処理とは別のスレッド)で行う必要があるため,新しいクラスが必要である。新しいクラスを作成するには,下図のように,プロジェクトツリー上の"java"フォルダ内の一番上のパッケージ(この場合は,com.example.htakada.bookdatabase)を右クリックし,"New"→"Java Class"を選択する。新しいクラスの名前は"SearchTask"とする。

これによってSearchTask.javaというファイルが新しくできるので,その中身を以下のように書き換える(冒頭のpackage文はそのまま残す)。

import android.os.AsyncTask;

public class SearchTask extends AsyncTask<String, Void, String> {
    private Listener listener;

    protected String doInBackground(String... params) {
    	// サーバとの通信処理を記述する
    }

    protected void onPostExecute(String result) {
    	super.onPostExecute(result);

		// サーバとの通信が終了したら,画面を更新する
        if (listener != null) {
            listener.onSuccess(result);
        }
    }

	// 画面更新処理を登録するためのメソッド
    void setListener(Listener listener) {
        this.listener = listener;
    }

	// 画面更新処理を呼び出すためのインタフェース
    interface Listener {
        void onSuccess(String result);
    }
}
サーバとの通信処理(入力されたキーワードをサーバに渡し,検索結果をJSON形式で受け取る処理)は,doInBackground()メソッド内に記述する。このメソッドの返り値は,受け取ったJSON形式の検索結果の文字列(String)である。

doInBackground()メソッド内に記述するサーバとの通信処理を行うプログラムを,以下に順に示す。必要に応じてimport文を追加しないと,コンパイルできないので注意すること。

  1. 返り値として返す文字列変数resultを定義し,デフォルトの返り値として空のリストで初期化する。
    String result = "[]";
    
  2. 接続先のURLを指定してサーバに接続する(例外が発生する可能性があるので,try節で囲む)。
    try {
    	URL url = new URL("http://www.cm.is.ritsumei.ac.jp/class/saproglab/server/cgi-bin/booksearch_json.py");
    	HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
    
  3. サーバに入力されたキーワードを送る。
    	try {
    		urlConnection.setDoOutput(true);
    		OutputStream out = urlConnection.getOutputStream();
    		String query = "query=" + params[0];	// query=Androidなど
    		try {
    			out.write(query.getBytes("UTF-8"));
    			out.flush();
    		} catch(Exception e) {
    			e.printStackTrace();
    		} finally {
    			out.close();
    		}
    
  4. サーバからJSON形式の検索結果を受け取る。結果は変数resultに格納される。
    		InputStream in = urlConnection.getInputStream();
    		try {
    			StringBuffer buffer = new StringBuffer();
    			BufferedReader reader = new BufferedReader(new InputStreamReader(in,"UTF-8"));
    			String str;
    			while((str = reader.readLine()) != null) {
    				buffer.append(str);
    			}
    			result = buffer.toString();
    		} catch(Exception e) {
    			e.printStackTrace();
    		} finally {
    			in.close();
    		}
    
  5. 例外処理を行う。
    	} catch(Exception e) {
    		e.printStackTrace();
    	} finally {
    		urlConnection.disconnect();
    	}
    } catch(Exception e) {
    	e.printStackTrace();
    }
    
  6. 結果(変数resultの値)を返す。
    return result;
    

この時点ではまだ動作確認は行えないが,コンパイルエラーが起こっていないかを確認しておくこと。

検索結果の表示

サーバとの通信処理と,サーバからの応答に基づく検索結果の表示処理は,ResultActivityクラスのonCreate()メソッドの中で行う。onCreate()メソッドは,対応するActivityが作成されたときに起動されるメソッドである。以下,これに関するプログラムを順に説明する。

  1. 書籍情報を管理するためのクラス(構造体)BookInfoをResultActivity内に定義する。
    private class BookInfo {
    	int ID;
    	String title;
    	String author;
    	String publisher;
    	int price;
    	String isbn;
    }
  2. すでにダミーの書籍情報を表示するために記述していたonCreate()メソッドを以下のように書き換える。
    protected void onCreate(Bundle savedInstanceState) {
    	super.onCreate(savedInstanceState);
    	setContentView(R.layout.activity_result);
    
    	// キーワード入力画面から入力されたキーワードを受け取る
    	Intent intent = getIntent();
    	String query = intent.getStringExtra("QUERY");
    
    	// ListViewに表示するデータを格納するためのArrayList
    	final ArrayList<HashMap<String, String>> listData = new ArrayList<>();
    
    	// 非同期で実行するサーバとの通信処理を行うクラスのインスタンスを作成
    	SearchTask task = new SearchTask();
    	// サーバから応答が返ってきたときの処理を記述
    	task.setListener(new SearchTask.Listener() {
    		@Override
    		public void onSuccess(String result) {
    			// 
    			// サーバから受け取ったJSON形式の検索結果を処理をここに記載
    			//
    			
    			// ListViewに表示するためのAdapterを生成
    			SimpleAdapter adapter = new SimpleAdapter(ResultActivity.this,
    				listData,   // ListViewに表示するデータ
    				android.R.layout.simple_list_item_2, // ListViewで使用するレイアウト(2つのテキスト)
    				new String[]{"title","author"},     // 表示するHashMapのキー
    				new int[]{android.R.id.text1, android.R.id.text2} // データを表示するid
    			);
    
    			// ListViewの初期化処理
    			ListView listView = (ListView) findViewById(R.id.listView);
    			listView.setAdapter(adapter);
    
    			// ListView中の要素がタップされたときの処理を記述
    			listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    				@Override
    				public void onItemClick(AdapterView parent, View view, int position, long id) {
    					Intent intent = new Intent(ResultActivity.this, BookInfoActivity.class);
    					startActivity(intent);
    				}
    			});
    		}
    	});
    
    	// サーバとの通信を非同期で起動
    	task.execute(query);
    }

もっとも重要な処理は,上記のonSuccess()メソッド内にある「サーバから受け取ったJSON形式の検索結果を処理」の部分である。この部分のプログラムを以下に示す。

  1. 検索結果の書籍情報をBookInfoの配列として保持するためのArrayListを作成する。
    final ArrayList<BookInfo> bookList = new ArrayList<BookInfo>();
  2. JSON形式の検索結果(文字列)を,JSONArrayというクラスに展開する(例外が発生する可能性があるので,tryで囲む)。
    try {
    	JSONArray jsonArray = new JSONArray(result);
    
  3. JSONArrayに含まれているリスト要素の文だけ繰り返し処理を行うためのforループを構成する。
    	for(int i=0;i<jsonArray.length();i++) {
    		JSONObject jsonObject = jsonArray.getJSONObject(i);
    		// ここにJSON形式からデータを取り出す処理を記述
    	}
  4. 上記のforループの中で,JSON形式からデータを取り出し,BookInfoクラスのインスタンスに値を設定して,ArrayListに追加する。
    		final BookInfo bookInfo = new BookInfo();
    		bookInfo.ID        = jsonObject.getInt("ID");
    		bookInfo.title     = jsonObject.getString("TITLE");
    		bookInfo.author    = jsonObject.getString("AUTHOR");
    		bookInfo.publisher = jsonObject.getString("PUBLISHER");
    		bookInfo.price     = jsonObject.getInt("PRICE");
    		bookInfo.isbn      = jsonObject.getString("ISBN");
    
    		bookList.add(bookInfo);
    
  5. ListViewに表示するタイトルと著者名を,JSON形式のデータから取り出したデータをもとに設定する。
    		listData.add(new HashMap() {
    			{put("title", bookInfo.title);}
    			{put("author", bookInfo.author);}
    		});
    
  6. 例外が発生したときの処理を行う。
    } catch(JSONException e) {
    	e.printStackTrace();
    }

以上により,キーワード入力画面で入力した文字列に基づいてサーバ上で検索を行い,その結果がアプリの画面上に表示されるはずである。

書籍情報画面での書籍情報の表示

最後に,検索結果画面上でタップされた書籍の情報を書籍情報画面へ渡し,表示するプログラムを作成する。

  1. ResultActivityにおいて,ListViewの要素がタップされたときに呼び出されるonItemClick()メソッド内で,書籍の情報(ID,タイトル,著者,出版社,価格,ISBN)をIntentに設定する。
                       BookInfo bookInfo = bookList.get(position);
                       intent.putExtra("id", bookInfo.ID);
                       intent.putExtra("title", bookInfo.title);
                       intent.putExtra("author", bookInfo.author);
                       intent.putExtra("publisher", bookInfo.publisher);
                       intent.putExtra("price", bookInfo.price);
                       intent.putExtra("isbn", bookInfo.isbn);
  2. BookInfoActivityのonCreate()メソッド内で,Intentに渡された書籍情報を取り出し,画面に表示する。
        // Intentからの情報の取り出し
        Intent intent = getIntent();
        int id = intent.getIntExtra("id", -1);
        String title = intent.getStringExtra("title");
        String author = intent.getStringExtra("author");
        String publisher = intent.getStringExtra("publisher");
        int price = intent.getIntExtra("price", 0);
        String isbn = intent.getStringExtra("isbn");
    
    	// ユーザインタフェースコンポーネントへの参照の取得
        TextView titleView = (TextView) findViewById(R.id.titleView);
        TextView authorView = (TextView) findViewById(R.id.authorView);
        TextView publisherView = (TextView) findViewById(R.id.publisherView);
        TextView priceView = (TextView) findViewById(R.id.priceView);
        TextView isbnView = (TextView) findViewById(R.id.isbnView);
    
    	// 書籍情報の表示
        titleView.setText(title);
        authorView.setText(author);
        publisherView.setText(publisher);
        priceView.setText(new Integer(price).toString());
        isbnView.setText(isbn);
    

演習課題10(→提出先)

以上のプログラムにより,検索結果画面で要素をタップすると書籍情報画面へ遷移し,書籍の詳細情報(タイトル,著者,出版社,価格,ISBN)が表示されることを確認せよ。

Copyright © 2018-2020 Hideyuki Takada