Android Studio3 OkHttp3クライアントアプリを作ろう

##前書き
[前回](https://qiita.com/noriseto/items/b953408242c41d976239) 、スマホで「『Hello World!』を表示するだけ」アプリの動作確認はできましたので、そのアプリを改良して、適当なWebサイトにGET/POSTするアプリを作成していきます。

<details>
<summary>連載記事リスト：Androidアプリ開発</summary><div>
[第1弾:Android Studio3 セットアップ (Windows10)](https://qiita.com/noriseto/items/b7dff53f8ccd2ec209c4)
[第2弾:Android Studio3 Emulatorでアプリを動かそう](https://qiita.com/noriseto/items/fe6eca453713051be218)
[第3弾:Android Studio3 スマホで自作のアプリを動かそう](https://qiita.com/noriseto/items/b953408242c41d976239)
第4弾:Android Studio3 OkHttp3クライアントアプリを作ろう（このページ）
</div></details>

##アプリ構築ポイント
・AndroidManifest のパーミッションで、「INTERNET」と「STORAGE(READ)」を許可
・http クライアントは『OkHttp3』ライブラリを利用
・非同期処理のThreadは『AsyncTask』で実装
・AsyncTaskへのデータ渡しは、Object配列で
・POST送信で使うファイルは、すでにスマホのストレージにあるファイルを利用
##build.gradleに依存関係追記
OkHttp3 を追加します。

```app/build.gradle 
	dependencies {
	    implementation fileTree(dir: 'libs', include: ['*.jar'])
	    implementation 'com.android.support:appcompat-v7:26.1.0'
	    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
	    implementation 'com.squareup.okhttp3:okhttp:3.9.1'  // ①OkHttp3 を追加
	    testImplementation 'junit:junit:4.12'
	    androidTestImplementation 'com.android.support.test:runner:1.0.1'
	    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
	}
```
①でOkHttp3を追加 [OkHttp公式サイト](http://square.github.io/okhttp/)
執筆時点(2017-12-15)では、最新のOkHttp3 は『ver 3.9.1』ですね。
AndroidStudio2までは、

```app/build.gradle
compile 'com.squareup.okhttp3:okhttp:3.9.1'
```
という書式でしたが、AndroidStudio3 からは、

```app/build.gradle
implementation 'com.squareup.okhttp3:okhttp:3.9.1'
```
と書くようです。エラーにはなりませんが…。
##パーミッション設定追加
今回、パーミッションとして必要な「INTERNET」と「STORAGE(read)」を追加します。

```xml:AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="jp.co.visionarts.noriseto.pocapp">

    <uses-permission android:name="android.permission.INTERNET" />              <!--①-->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>  <!--②-->

    <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>
```
①「INTERNET」パーミッション追加
②「READ_EXTERNAL_STORAGE」パーミッション追加
##レイアウトを修正
「GET」「POST」ボタンを用意し、レスポンスは「TextView」で表示します。

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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="jp.co.visionarts.noriseto.pocapp.MainActivity">

    <LinearLayout
        android:orientation="vertical"
        android:gravity="center_horizontal"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <Button
            android:id="@+id/button_get"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="GET"/>
        <Button
            android:id="@+id/button_post"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="POST"/>
        <!-- ① -->
        <ScrollView
            android:orientation="vertical"
            android:gravity="center_horizontal"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <TextView
                android:id="@+id/textview"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Response..."/>
        </ScrollView>
    </LinearLayout>

</android.support.constraint.ConstraintLayout>
```
①レスポンス表示の「TextView」は「ScrollView」タグで囲って縦スクロールできるようにします。
##ソースコードを修正
####MainActivity.java
```java:MainActivity.java
package jp.co.visionarts.noriseto.pocapp;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;

public class MainActivity extends AppCompatActivity 
                implements View.OnClickListener { //①OnClickListener インタフェイスを継承

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //②ボタンにこのActivityのonClickListenerを割り当てる
        findViewById(R.id.button_get).setOnClickListener(this);
        findViewById(R.id.button_post).setOnClickListener(this);
    }

    //③onClickコールバックをここですべてキャッチして、viewのidで動作を決める
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.button_get:
                //④GET リクエストサンプルとして「Livedoor天気情報」にアクセス
                String weatherURL = "http://weather.livedoor.com/forecast/webservice/json/v1";
                String queryString = "?city=130010"; //東京

                //⑤GetAsyncTaskに渡すパラメータをObject配列に設定
                Object[] getParams = new Object[2];
                getParams[0] = weatherURL;
                getParams[1] = queryString;

                //⑥GetAsyncTaskを実行
                new GetAsyncTask(this).execute(getParams);
                break;

            case R.id.button_post:
                //⑦POSTリクエストサンプルとして、自前のファイル受信サイトへアクセス
                String uploadURL = "http://va-fileupload.azurewebsites.net/uploadserver/api/upload/";
                String title = "Upload image file(JPEG)";
                String uploadFile = "/storage/emulated/0/Pictures/cat.jpg";

                //⑧PostAsyncTaskに渡すパラメータをObject配列に設定
                Object[] postParams = new Object[3];
                postParams[0] = uploadURL;
                postParams[1] = title;
                postParams[2] = uploadFile;

                //⑨PostAsyncTaskを実行
                new PostAsyncTask(this).execute(postParams);
                break;

            default:
                break;
        }
    }

}
```
①OnClickListener インタフェイスを継承
②ボタンにこのActivityのonClickListenerを割り当てる
③onClickコールバックをここですべてキャッチして、viewのidで動作を決める
④GET リクエストサンプルとして「[Livedoor天気情報](http://weather.livedoor.com/weather_hacks/webservice "Weather hacks")」にアクセス
⑤GetAsyncTaskに渡すパラメータをObject配列に設定
⑥GetAsyncTaskをパラメータ付で実行
⑦POSTリクエストサンプルとして、自前のファイル受信サイトへアクセス
　POSTリクエストは、適当なサイトが無かったので、自前サイトにファイルをアップロードしてみます。
⑧PostAsyncTaskに渡すパラメータをObject配列に設定
⑨PostAsyncTaskをパラメータ付で実行
　HTTP通信部分は、GET、POSTメソッドそれぞれ、AsyncTaskを継承した独自クラスに実装していきます。

_自前のアップロードサイトは、Azure WebApp + Springboot で構築してまして、『簡単にWebサーバ作れますよ！』的なところも、いつの日か触れたいと思います。_

####GetAsyncTask.java
```java:GetAsyncTask.java
package jp.co.visionarts.noriseto.pocapp;

import android.app.Activity;
import android.os.AsyncTask;
import android.util.Log;
import android.widget.TextView;

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

import java.io.IOException;
import java.lang.ref.WeakReference;

import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;

/*
 * AsyncTask for GET Method
 * Created by noriseto on 2017/11/28.
 */

public class GetAsyncTask extends AsyncTask<Object, Void, Object> {

    //①USE WEEK REFERENCE
    private final WeakReference<Activity> w_Activity;

    //②コンストラクタで、 呼び出し元Activityを弱参照で変数セット
    GetAsyncTask(Activity activity) {
        this.w_Activity = new WeakReference<>(activity);
    }

    //③バックグラウンド処理
    @Override
    protected Object doInBackground(Object[] data) {

        //Object配列でパラメータを持ってこれたか確認
        String url = (String)data[0];
        String queryString = (String)data[1];

        //④HTTP処理用オプジェクト作成
        OkHttpClient client = new OkHttpClient();

        //⑤送信用リクエストを作成
        Request request = new Request.Builder().url(url + queryString).get().build();

        //⑥受信用オブジェクトを作成
        Call call = client.newCall(request);
        String result = "";

        //⑦送信と受信
        try {
            Response response = call.execute();
            ResponseBody body = response.body();
            if (body != null) {
                result = body.string();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        //⑧結果を返し、onPostExecute で受け取る
        return result;
    }

    //⑨バックグラウンド完了処理
    @Override
    protected void onPostExecute(Object result) {
        super.onPostExecute(result);
        Log.i("onPostExecute", (String)result);
        //⑩簡単にJSONレスポンスをパースしてみる
        String title = "no response";
        String description = "";
        String publicTime = "";
        try {
            JSONObject json = new JSONObject((String)result);
            title = json.getString("title");
            JSONObject descriptionObj = (JSONObject)json.get("description");
            description = descriptionObj.getString("text");
            publicTime = descriptionObj.getString("publicTime");
        } catch (JSONException je) {
            je.getStackTrace();
        }

        //⑪画面表示
        Activity activity = w_Activity.get();
        if (activity == null || activity.isFinishing() || activity.isDestroyed()) {
            // activity is no longer valid, don't do anything!
            return;
        }
        TextView tv = activity.findViewById(R.id.textview);
        String showText = title + "\n" + publicTime + "\n" + description;
        tv.setText(showText);
    }

}
```
①最近は、Activity contextをそのまま変数に入れると「This field leaks context object」というワーニングが出るので、弱参照とする
②コンストラクタで、 呼び出し元Activityを弱参照で変数セット
③バックグラウンド処理でWebサイトにアクセス
④HTTP処理用オプジェクト作成
⑤送信用リクエストを作成。引数のURLとQueryパラメータをセットする
⑥受信用オブジェクトを作成
⑦送信と受信
⑧結果を返し、onPostExecute で受け取る
⑨バックグラウンド処理の最後にレスポンスを画面表示
⑩JSONレスポンスは以下のJSONフォーマット (使うところだけ抜粋)

```
{
  "title": "東京都 東京 の天気",
  "description": {
    "text": " 本州付近は...",
    "publicTime": "2017-11-28T16:39:00+0900"
  }
}
```
⑪画面表示部分で、表示する直前に弱参照Activityから強参照のActivityを取得する

####PostAsyncTask.java
```java:PostAsyncTask.java
package jp.co.visionarts.noriseto.pocapp;

/*
 * AsyncTask for POST Method
 * Created by noriseto on 2017/11/28.
 */

import android.app.Activity;
import android.os.AsyncTask;
import android.util.Log;
import android.widget.TextView;

import java.io.File;
import java.io.IOException;
import java.lang.ref.WeakReference;

import okhttp3.Call;
import okhttp3.Headers;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;

public class PostAsyncTask extends AsyncTask<Object, Void, Object> {

    //①USE WEEK REFERENCE
    private final WeakReference<Activity> w_Activity;

    //②コンストラクタで、 呼び出し元Activityを弱参照で変数セット
    PostAsyncTask(Activity activity) {
        // USE WEEK REFERENCE
        this.w_Activity = new WeakReference<>(activity);
    }

    //③バックグラウンド処理
    @Override
    protected Object doInBackground(Object[] data) {

        //Object配列でパラメータを持ってこれたか確認
        String url = (String)data[0];
        String description = (String)data[1];
        String filePath = (String)data[2];

        //④HTTP処理用オプジェクト作成
        OkHttpClient client = new OkHttpClient();

        //⑤送信用POSTデータを構築（Multipartでお願いします！）
        final String BOUNDARY = String.valueOf(System.currentTimeMillis());
        final MediaType TEXT = MediaType.parse("text/plain; charset=utf-8");
        final MediaType IMAGE = MediaType.parse("image/jpeg");
        RequestBody requestBody = new MultipartBody.Builder(BOUNDARY)
                .setType(MultipartBody.FORM)
                .addPart(
                        Headers.of("Content-Disposition", "form-data; name=\"description\""),
                        RequestBody.create(TEXT, description)
                )
                .addFormDataPart(
                        "upload_file",
                        "cat.jpg",
                        RequestBody.create(IMAGE, new File(filePath))
                )
                .build();

        //⑥送信用リクエストを作成
        Request.Builder requestBuilder = new Request.Builder();
        requestBuilder.url(url);
        requestBuilder.post(requestBody);
        Request request = requestBuilder.build();

        //⑦受信用オブジェクトを作成
        Call call = client.newCall(request);
        String result = "";

        //⑧送信と受信
        try {
            Response response = call.execute();
            ResponseBody body = response.body();
            if (body != null) {
                result = body.string();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        //⑨結果を返し、onPostExecute で受け取る
        return result;
    }

    //⑩バックグラウンド完了処理
    @Override
    protected void onPostExecute(Object result) {
        super.onPostExecute(result);
        Log.i("onPostExecute", (String)result);

        //⑪画面表示
        Activity activity = w_Activity.get();
        if (activity == null || activity.isFinishing() || activity.isDestroyed()) {
            // activity is no longer valid, don't do anything!
            return;
        }
        TextView tv = activity.findViewById(R.id.textview);
        tv.setText((String)result);
    }

}
```
①最近は、Activity contextをそのまま変数に入れると「This field leaks context object」というワーニングが出るので、弱参照とする
②コンストラクタで、 呼び出し元Activityを弱参照で変数セット
③バックグラウンド処理でWebサイトにアクセス
④HTTP通信オプジェクト作成
⑤送信用POSTデータは、どうせならとMultipartでファイルを送ってみる
⑥送信用リクエストを作成
⑦受信用オブジェクトを作成
⑧送信と受信
⑨結果を返し、onPostExecute で受け取る
⑩バックグラウンド処理の最後にレスポンスを画面表示
⑪画面表示部分で、表示する直前に弱参照Activityから強参照のActivityを取得する

##[POC App]アプリを実行
Storage の許可は、今回手抜きしたので、下記手順でPermissionを与えてください。
![android-4-01.png](https://qiita-image-store.s3.amazonaws.com/0/184036/e09c2fff-ada2-ce02-550e-ee9ff90cc24a.png)
[設定] -> [アプリ] で、今回作成しているアプリ [POC App] をタップ
**⇒いまはまだ、何も許可されてないですね。**

[許可] -> [ストレージ] を On に変更
![android-4-02.png](https://qiita-image-store.s3.amazonaws.com/0/184036/dd5bc3be-d4bd-83b0-47d7-50a0ff59fdbb.png)
**⇒ストレージが許可されました。**

[GET]ボタンをタップ
![android-4-03.png](https://qiita-image-store.s3.amazonaws.com/0/184036/23387bb5-94c3-c353-039f-0e564849ef17.png)
**⇒Livedoor天気情報から東京の概況を取ってこれました！**

[POST]ボタンをタップ
![android-4-04.png](https://qiita-image-store.s3.amazonaws.com/0/184036/89003604-4886-984a-ef28-9e0cef3fc6b9.png)
**⇒スマホの画像を自前のサーバにアップロードできました！**
##次回予告
次回は、スマホに搭載されている各種センサーについて、アプリを開発しながら探ってみようと思います。
**Enjoy the holidays!**