＊Android＊【HTTP通信_1】WebAPIを叩いてみる[GET, POST]

# はじめに
こんにちは．今回は，AndroidでWebAPIを叩いてみましょう．初めてWebAPIを叩く方でも読めるように書いていきます．WebAPIを叩く方法はいろいろありますが，Javaの標準ライブラリである`HttpURLConnection`を使用します．面倒な設定などは何もなく，WebAPIを叩くために使用するのであればオーソドックスな方法だと思います．

WebAPIを叩くと戻り値として`JSON`を受け取ることになりますが，AndroidではJSONを整形して画面に出力するのが難しいので，`curl`コマンドを使いつつ，Androidの方を実装していきます．WebAPIを使う場合，curlコマンドに関しては使えた方がかなり作業が楽になると思います．

# 前提
開発環境は以下の通りです．
＊Android Studio 4.0.1
＊targetSdkVersion 28
＊Google Nexus 5x

curlコマンド
＊Ubuntu 20.04 LTS (WSL 2)

# なぜWebAPIを使用するか
音楽の歌詞アプリを作りたい場合や翻訳アプリを作ることを考えます．リアルタイムに増えていく音楽の歌詞のデータを集める，データを集めて翻訳機能を作るというのは時間と手間がかかります．自分で作るのが難しい場合，ライブラリを使用すると思います．標準ライブラリ，外部ライブラリを探しても見つからない場合にどうするか．また歌詞のようにリアルタイムに変わっていくデータを扱いたい場合や，プログラミング言語に依存しないライブラリを使用したい場合にどうするか．WebAPIを探すべきです．

WebAPIは，言語に依存しない，データベースと連携している場合が多いためリアルタイムなデータ・莫大なデータを扱うのが得意といった特徴があります．HTTP通信を使用するためインターネット接続が前提ですが，Androidの場合，インターネットには常時接続できる状況が当たり前なので，WebAPIを使用してアプリを作っても問題ないです．なお，WebAPIがメンテナンスにより一時的に使用できない，仕様が変更される場合には注意が必要です．

# WebAPIの使い方
厳密に説明するとわかりにくくなるので各用語の意味に関しては少し曖昧にしつつざっくり説明すると，WebAPIとは，Webサービスにおいて，開発者がプログラムからアクセスできるように許可された部分，インタフェースを指しています．WebAPIにプログラムからアクセスすることをリクエストと呼び，リクエストの結果が返ってくることをレスポンスと呼びます．リクエストを送るには，URLとHTTPメソッドを指定する必要があります．HTTPメソッドは，最も基本的な`GET`と`POST`を使用します．具体的にどのようにリクエストを送るのか，レスポンスはどのようなものになるか，以下のとおりです．

**リクエスト**

| URL | HTTPメソッド | 
| -------- | -------- |
| `http://example.com/api/get`   | GET     | 
| `http://example.com/api/post`  | POST    | 


**レスポンス**

```json
{
    "name": "foge"
}
```
このように，リクエストすると，レスポンスは`JSON`という形式で返ってきます．GETメソッドはおもにデータの取得に使い，POSTメソッドはおもにリクエストにデータを含めて送信したい場合に使用します．


# どのWebAPIを使用するか
- HTTP通信をテストする手軽な方法として[httpbinのHP](https://httpbin.org/)を使用する方法があります．通常WebAPIを使うには，登録やAPIキーの取得が必要ですが，httpbinのHPではそれらを必要としません．登録なしにHTTP通信を試すことができます．今回はこれを使用します．
- 今回の記事では解説しませんが，登録やAPIキーの取得に関してもやってみたい場合は，APIキーの取得が簡単な[OpenWeatherMap](https://openweathermap.org/)を利用すると良いと思います．東京の現在の天気を取得するといったことは，様々なWebページで解説されているので簡単に試せると思います．

# httpbinについて
`http://httpbin.org`というURLの後ろに決められたパラメータを付与することで，様々なテストが行えます．今回は，以下の機能を使用します．



| 機能 | URL | 
| -------- | -------- | 
| IPアドレスの取得     |  http://httpbin.org/ip        | 
| GETリクエストのテスト     | http://httpbin.org/get     | 
| POSTリクエストのテスト     |  http://httpbin.org/post   | 


# curlコマンド
Windowsでcurlコマンドを使用したい場合は，`WSL2`をインストールするか，Windows用のcurlコマンドをインストールしてください．WSL2は，WindowsでLinuxの環境を使う最も簡単な方法なので，簡単に設定できます．ここではインストールの方法に関しては触れません．
curlコマンドは様々な用途で使えますが，HTTPリクエストをする際によく使われます．では，`httpbin`を使用して実際にWebAPIを叩いてみます．

IPアドレスの取得，GETリクエストのテストは，GETメソッドを用います．curlコマンドでは，オプションを何もつけない場合，GETリクエストを行います．

<br/>

- IPアドレスの取得

**リクエスト**

```bash
curl http://httpbin.org/ip
```
**レスポンス**

```bash
{
  "origin": "111.111.111.111" # ipアドレスは個々で違う
}
```

<br/>

- GETリクエストのテスト

**リクエスト**

```bash
curl http://httpbin.org/get
```
**レスポンス**

```json
{
  "args": {},
  "headers": {
    "Accept": "*/*",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.68.0",
    "X-Amzn-Trace-Id": "Root=1-5f4f2fa5-80f8b602a2eec73861aa12e8"
  },
  "origin": "111.111.111.111",
  "url": "http://httpbin.org/get"
}
```
<br/>

- POSTリクエストのテスト

POSTリクエストを行うには，curlコマンドのオプション`-X`でHTTPメソッドを指定します．`-d` で、POSTリクエストに含めるデータを記述できます．httpbinでは，戻り値の`"form"`にリクエストの際に含めたデータがセットされます．JSONを送信する場合は，`Content-Type`の指定が必要である．リクエストヘッダのオプション`-H`を用いる．


**リクエスト**

```bash
curl -X POST -H 'Content-Type: application/json' -d "name=foge&type=fogefoge" http://httpbin.org/post
```

**レスポンス**

```json
{
  "args": {},
  "data": "",
  "files": {},
  "form": {
    "name": "foge"
    "type": "fogefoge"
  },
  "headers": {
    "Accept": "*/*",
    "Content-Length": "9",
    "Content-Type": "application/x-www-form-urlencoded",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.68.0",
    "X-Amzn-Trace-Id": "Root=1-5f4f32d3-65c03ae0882434bae32b8070"
  },
  "json": null,
  "origin": "111.111.111.111",
  "url": "http://httpbin.org/post"
}
```

# AndroidでWebAPIを叩く
Androidでは，curlコマンドのように簡単にレスポンスの結果を整形して表示することができません．curlコマンドを使うことで適切なURLを指定できているかどうか，正しい出力ができているかどうかを確認しつつ，AndroidでWebAPIを叩く，という風にするのが良いと思います．今回は，上で実行したcurlの結果を使いつつ，Androidで必要な情報を取得したいと思います．

まず，インターネットに接続するため，`AndroidManifest.xml`に以下のように記述します．

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

## HttpURLConnection


通信を開始する前にいくつかの接続設定を行う必要があります．以下の表にまとめています．

| 接続設定 | 説明 |
| -------- | -------- | 
| 接続タイムアウト         | connectメソッドで接続にかかってもよい時間の制限．   |
| 読み取りタイムアウト         | データ取得にかかった時間（リクエストを行いレスポンスが返るまでの時間）         |
| User-Agent         | リクエストを行うソフトウェアやOS等の情報を格納([こちら](https://qiita.com/nightyknite/items/b2590a69f2e0135756dc)がわかりやすい)  |
| Accept-Language         | クライアントがどの言語を理解できるか、どの種類のロケールが推奨されるかを示す         |
| HTTPメソッド     | サーバにどんな操作をしてほしいか伝えるためのもの     |
| リクエストボディの送信許可     | リクエストボディにデータを格納して送信する許可     |
| レスポンスボディの受信許可     | レスポンスボディに格納されたデータを受信する許可     |


では，実際に`HttpURLConnection`を使ってHTTP通信を行うプログラムを示します．`HttpURLConnection`はUIスレッドでは実行できないので，別スレッドを作成して実行する必要があります．
まず，IPアドレスを取得するためにGETリクエストをします．

```java
public String getAPI(){
    HttpURLConnection urlConnection = null;
    InputStream inputStream = null;
    String result = "";
    String str = "";
    try {
        URL url = new URL("http://httpbin.org/ip");
        // 接続先URLへのコネクションを開く．まだ接続されていない
        urlConnection = (HttpURLConnection) url.openConnection();
        // 接続タイムアウトを設定
        urlConnection.setConnectTimeout(10000);
        // レスポンスデータの読み取りタイムアウトを設定
        urlConnection.setReadTimeout(10000);
        // ヘッダーにUser-Agentを設定
        urlConnection.addRequestProperty("User-Agent", "Android");
        // ヘッダーにAccept-Languageを設定
        urlConnection.addRequestProperty("Accept-Language", Locale.getDefault().toString());
        // HTTPメソッドを指定
        urlConnection.setRequestMethod("GET");
        //リクエストボディの送信を許可しない
        urlConnection.setDoOutput(false);
        //レスポンスボディの受信を許可する
        urlConnection.setDoInput(true);
        // 通信開始
        urlConnection.connect();
        // レスポンスコードを取得
        int statusCode = urlConnection.getResponseCode();
        // レスポンスコード200は通信に成功したことを表す
        if (statusCode == 200){
            inputStream = urlConnection.getInputStream();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
            // 1行ずつレスポンス結果を取得しstrに追記
            result = bufferedReader.readLine();
            while (result != null){
                str += result;
                result = bufferedReader.readLine();
            }
            bufferedReader.close();
        }
    } catch (MalformedURLException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
    // レスポンス結果のJSONをString型で返す
    return str;
}

```

次に，POSTリクエストをします．

```java
public String postAPI(){
    HttpURLConnection urlConnection = null;
    InputStream inputStream = null;
    OutputStream outputStream = null;
    String result = "";
    String str = "";
    try {
        URL url = new URL("http://httpbin.org/post");
        // 接続先URLへのコネクションを開く．まだ接続されていない
        urlConnection = (HttpURLConnection) url.openConnection();
        // リクエストボディに格納するデータ
        String postData = "name=foge&type=fogefoge";
        // 接続タイムアウトを設定
        urlConnection.setConnectTimeout(10000);
        // レスポンスデータの読み取りタイムアウトを設定
        urlConnection.setReadTimeout(10000);
        // ヘッダーにUser-Agentを設定
        urlConnection.addRequestProperty("User-Agent", "Android");
        // ヘッダーにAccept-Languageを設定
        urlConnection.addRequestProperty("Accept-Language", Locale.getDefault().toString());
        // HTTPメソッドを指定
        urlConnection.setRequestMethod("POST");
        //レスポンスボディの受信を許可する
        urlConnection.setDoInput(true);
        // リクエストボディの送信を許可する
        urlConnection.setDoOutput(true);
        // 通信開始
        urlConnection.connect();
        // リクエストボディの書き込みを行う
        outputStream = urlConnection.getOutputStream();
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream, "utf-8"));
        bufferedWriter.write(postData);
        bufferedWriter.flush();
        bufferedWriter.close();
        
        // レスポンスコードを取得
        int statusCode = urlConnection.getResponseCode();
        // レスポンスコード200は通信に成功したことを表す
        if (statusCode == 200){
            inputStream = urlConnection.getInputStream();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            // 1行ずつレスポンス結果を取得しstrに追記
            result = bufferedReader.readLine();
            while (result != null){
                str += result;
                result = bufferedReader.readLine();
            }
            bufferedReader.close();
        }
        urlConnection.disconnect();
    } catch (MalformedURLException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
    // レスポンス結果のJSONをString型で返す
    return str;
}

```

## JSONの処理
上で述べたHTTP通信の結果，JSONを文字列として取得できます．文字列からJSONのオブジェクトを生成し，中身を取り出します．

<br/>

**GETリクエストの戻り値からIPアドレスを取り出す**

```java
// GETリクエスト用関数の戻り値を取得．上のGETリクエストの実装を参照．
response = getAPI();
// 文字列からJSONオブジェクトを生成し，これをルートとする．
JSONObject rootJSON = new JSONObject(response); 
// curlでhttp://httpbin.org/ipにリクエストしたときの出力結果を参照
// ルート直下の"origin"というkeyのvalue（文字列）を取得
ip = rootJSON.getString("origin");
```

**POSTリクエストの戻り値からリクエストボディに設定した値を取り出す**
curlコマンドでPOSTリクエストを行った出力結果から，リクエストボディに設定した値は`"form"`に格納されているのを確認できる．`"form"`に格納されているリクエストボディの値を取り出す．

```java
// POSTリクエスト用関数の戻り値を取得．上のPOSTリクエストの実装を参照．
response = getPOST();
// 文字列からJSONオブジェクトを生成し，これをルートとする．
JSONObject rootJSON = new JSONObject(response);
// ルート直下の"form"JSONObjectを取得する
JSONObject formJSON = rootJSON.getJSONObject("form");
// "form"JSONObject直下の"name"keyと"type"keyの値（文字列）を取得する
nameAndType = formJSON.getString("name") + "/" + formJSON.getString("type");
```
以上を踏まえて，サンプルコードを示す．

# サンプルコード

```xml:AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.samplehttpconnection">
    <uses-permission android:name="android.permission.INTERNET"/>

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
```

```java:MainActivity.java
package com.example.samplehttpconnection;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Locale;

public class MainActivity extends AppCompatActivity {
    private Handler handler = new Handler();
    private Button button;
    private Button button2;
    private TextView textView;
    private TextView textView2;
    private String urlIpText = "http://httpbin.org/ip";
    private String urlPostText = "http://httpbin.org/post";
    private String ip = "";
    private String nameAndType = "";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = findViewById(R.id.button);
        button2 = findViewById(R.id.button2);
        textView = findViewById(R.id.textView);
        textView2 = findViewById(R.id.textView2);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Thread thread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        String response = "";
                        try {
                            response = getAPI();
                            JSONObject rootJSON = new JSONObject(response);
                            ip = rootJSON.getString("origin");
                        } catch (JSONException e) {
                            e.printStackTrace();
                        }
                        handler.post(new Runnable() {
                            @Override
                            public void run() {
                                textView.setText(ip);
                            }
                        });
                    }
                });
                thread.start();
            }
        });

        button2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Thread thread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        String response = "";
                        try {
                            response = postAPI();
                            JSONObject rootJSON = new JSONObject(response);
                            JSONObject formJSON = rootJSON.getJSONObject("form");
                            nameAndType = formJSON.getString("name") + "/" + formJSON.getString("type");
                        } catch (JSONException e) {
                            e.printStackTrace();
                        }
                        handler.post(new Runnable() {
                            @Override
                            public void run() {
                                textView2.setText(nameAndType);
                            }
                        });

                    }
                });
                thread.start();
            }
        });
    }

    public String getAPI(){
        HttpURLConnection urlConnection = null;
        InputStream inputStream = null;
        String result = "";
        String str = "";
        try {
            URL url = new URL(urlIpText);
            urlConnection = (HttpURLConnection) url.openConnection();
            urlConnection.setConnectTimeout(10000);
            urlConnection.setReadTimeout(10000);
            urlConnection.addRequestProperty("User-Agent", "Android");
            urlConnection.addRequestProperty("Accept-Language", Locale.getDefault().toString());
            urlConnection.setRequestMethod("GET");
            urlConnection.setDoInput(true);
            urlConnection.setDoOutput(false);
            urlConnection.connect();
            int statusCode = urlConnection.getResponseCode();
            if (statusCode == 200){
                inputStream = urlConnection.getInputStream();
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
                result = bufferedReader.readLine();
                while (result != null){
                    str += result;
                    result = bufferedReader.readLine();
                }
                bufferedReader.close();
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return str;
    }

    public String postAPI(){
        HttpURLConnection urlConnection = null;
        InputStream inputStream = null;
        OutputStream outputStream = null;
        String result = "";
        String str = "";
        try {
            URL url = new URL(urlPostText);
            urlConnection = (HttpURLConnection) url.openConnection();
            String postData = "name=foge&type=fogefoge";
            urlConnection.setConnectTimeout(10000);
            urlConnection.setReadTimeout(10000);
            urlConnection.addRequestProperty("User-Agent", "Android");
            urlConnection.addRequestProperty("Accept-Language", Locale.getDefault().toString());
            urlConnection.setRequestMethod("POST");
            urlConnection.setDoInput(true);
            urlConnection.setDoOutput(true);
            urlConnection.connect();
            outputStream = urlConnection.getOutputStream();
            BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream, "utf-8"));
            bufferedWriter.write(postData);
            bufferedWriter.flush();
            bufferedWriter.close();

            int statusCode = urlConnection.getResponseCode();
            if (statusCode == 200){
                inputStream = urlConnection.getInputStream();
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                result = bufferedReader.readLine();
                while (result != null){
                    str += result;
                    result = bufferedReader.readLine();
                }
                bufferedReader.close();
            }

            urlConnection.disconnect();

        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return str;
    }

}
```

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="100dp"
        android:text="Hello World!"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/button2" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="100dp"
        android:text="GET"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="100dp"
        android:text="Hello World!"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView" />

    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="50dp"
        android:text="POST"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/button" />

</androidx.constraintlayout.widget.ConstraintLayout>
```
# 参照してもらいたいページ

- [次回の記事](https://qiita.com/f-paico/items/ee4c337adc2dd92100ec)