Help us understand the problem. What is going on with this article?

Android入門!

はじめに

こんにちは、今週いっぱいで学校が終わり、来週から中身のない夏休みを過ごす僕です。
今回は前回の記事で作ったDjangoのAPIに実際にAndroidアプリを使用してHTTP通信をしてみたいと思います。

ちなみにこれあたふたしながらも1日で実装したものなのであまりクオリティは高くないです。(とは言っても16時間くらいかかってますが)

基本のき!

僕が一番苦労したのはここです。実行構成が全くわからなかったこと、全体像が全く見えなかったこと、フロントエンドはどうやって実装するの?などなど。
まずは全体像が把握できないと何から始めていいのかわかりません。そのため、まずはどのような構成で作られているのか、何ができるのかを理解する必要があります。

また、Androidアプリを開発するにあたって最近はKotlinやReactNativeなどありますが、今回はオワコンレガシー言語であるJavaを使用します。

大事なもの

ここではAndroidアプリを作成する上で大切なことをいくつか列挙します。
最低限この構成くらいはわかってて欲しいってレベルなのでガバガバですがご容赦ください。

  • アクティビティ
    • 実行全体のフローを表します
    • メインの実行するクラスにonCreate()をオーバーライドして実行します
    • onCreateのあとはonStart、onResumeが実行され、アプリは実行状態になります
    • このサイトにわかりやすい説明がありました
  • マニフェスト
    • マニフェストファイルではAndroidの設定を示します。
    • 後で書きますが、HTTP通信を行う際にはインターネットを使用するという宣言をする必要があります
    • 実行するメインのクラス名もここで明示的に示してあげます(今回はExSample.java)
    • そのほかにも外部のAPIを使用したりする時はここに書きます。
  • XML
    • 見た目を作る部分を定義します。
    • パーツにIDをつけてクラスでの処理を対応させ、そこに値を入れたりイベントを起こしたりすることができます。
  • エミュレータ
    • Androidの端末がなくても擬似的なエミュレータを使用して実行することができます。
    • あるなら実機を使った方がいいです。
    • 本記事ではエミュレータの設定方法は割愛します。

ウェッブ大好きな人がMVCに例えると、モデルは今回はDjangoのサーバにあり、Androidを構成するjavaのコードがコントローラ、XMLで記述する部分がビューって感じです。

実行の構成

今回、HTTP通信をするということで、僕自信一番わからなかった部分がここです。
そもそもAndroidってどこのクラスが実行されるの?という感じだったのでとても困りました。
大前提として、
Androidを実行するとマニフェストに指定したクラスが実行されます。
つまり、ほかにクラスを定義した場合でも最終的にはそこに集約する必要があります。(これが理解できてなかった)

以下の図のようなイメージになります。
image.png

汚い図ですが、最終的に実行されるクラスはメインとなるクラスだけなので、このほかにたくさんクラスがあってもやりたい処理はメインのクラスでしか実行されません。回り道してもいいから、なんとかメインにたどり着かせましょう!!!

では実際にコードを書いていきます。

今回の使用するクラスは以下の通りです。

クラス 用途
ExSample.java メインの実行クラス
AsyncHttpRequestGet.java 全てのデータを取得
AsyncHttpRequestPost.java 配達完了通知を送信
ListAndView.java リニアレイアウトのなかにリストビューを設置するためのクラス

今回はこの4つを使用してアンドロイドアプリを作っていきます。
※配達完了通知に関してですが、これは前回の記事から少しAPIをいじりました。

作る!

では書いていきます。

ExSampleでのポイント

  • AppCompatActivityクラスを継承してプログラムを組む。
  • onCreateメソッドで実行する。
  • あとで出てきますが、ListViewという表を作るみたいなレイアウトを使うときにメインのクラスのContextを使用することになるのでどこでも使えるようにgetInstanceメソッドを作成する。
  • AsyncHttpRequestGetを使用して最初にget requestを送信している。
ExSample.java
package es.exsample;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;

public class ExSample extends AppCompatActivity {

    // クラスを示す
    private static final String TAG = "ExSample";
    // 違うクラスでContextを参照するための変数
    private static ExSample context;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.list_item);
        context = this;
        // 実行
        AsyncHttpRequestGet task = new AsyncHttpRequestGet(context);
        task.execute("http://localhost:8000/get_shop");
        Log.d(TAG, "created");
    }

    // 他のクラスでContextを使用するための関数
    public static ExSample getInstance(){
        return context;
    }
}

AsyncHttpRequestGetのポイント

  • ExSampleはこいつを実行だけなので割とこのクラスでの分量は多め。
  • AsyncTaskを継承して非同期処理を行っている。
  • コンストラクタでXMLのパーツのIDを指定して渡すものを初期化している。
  • URL url = new URL(params[0]);でメインクラスで定義したURLを取得することができる。
  • setRequestPropertyでヘッダを指定(今回は認証がかかっているけど永続化するトークンを作成したのでベタ書き)
  • JSONArrayに受け取った値を入れると簡単に操作することができるようになる。
  • ArrayAdapterの第一引数にメインクラスで定義したメソッドで得たcontextを代入する。(メインのクラスのみでリストビューを定義する場合には直接thisと書けばいい)
AsyncHttpRequestGet.java
package es.exsample;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.util.Log;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.net.ssl.HttpsURLConnection;

public class AsyncHttpRequestGet extends AsyncTask<String, Void, String> {
    // ロガーのタグ
    private static final String TAG = "AsyncHttpRequestGet";

    // UI スレッドから操作するビュー
    private TextView titleView;
    private TextView beforUserContent;
    private TextView doneUserContent;
    private ListView beforUser;
    private ListView doneUser;
    public static Map<String, String> data;
    public static List<Map<String, String>> dataList;
    public static ListView listView;
    public static ListAndView adapter;

    // コンストラクタ、Viewをセットする
    public AsyncHttpRequestGet(Context context) {
        super();
        ExSample sample = (ExSample) context;
        titleView = (TextView)sample.findViewById(R.id.title);
        beforUserContent = (TextView)sample.findViewById(R.id.befor);
        doneUserContent = (TextView)sample.findViewById(R.id.done);
        beforUser = (ListView)sample.findViewById(R.id.beforUser);
        doneUser = (ListView)sample.findViewById(R.id.doneUser);

        listView = (ListView)sample.findViewById(R.id.beforUser);
    }

    // ここでリクエスト、レスポンスを捌く
    @Override
    protected String doInBackground(String... params) {
        StringBuilder sb = new StringBuilder();
        InputStream inputStream = null;
        HttpsURLConnection connection = null;

        try {
            // URLをセット、またここでヘッダの値を管理、tokenはログインした時に取得することができるようにしてある
            URL url = new URL(params[0]);
            connection = (HttpsURLConnection)url.openConnection();
            connection.setRequestProperty("Content-Type", "application/json");
            connection.setRequestProperty("Authorization", "JWT Djangoで取得したキーを指定する");
            connection.setConnectTimeout(3000);
            connection.setReadTimeout(3000);

            // GET
            connection.setRequestMethod("GET");
            connection.connect();

            // レスポンスコードを確認
            int responseCode = connection.getResponseCode();
            if(responseCode != HttpsURLConnection.HTTP_OK) {
                throw new IOException("HTTP responseCode: " + responseCode);
            }

            // 文字列化
            inputStream = connection.getInputStream();
            if(inputStream != null) {
                BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
                String line;
                while ((line = reader.readLine()) != null) {
                    sb.append(line);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(connection != null) {
                connection.disconnect();
            }
        }
        return sb.toString();
    }

    // ここでViewを作成する
    @RequiresApi(api = Build.VERSION_CODES.O)
    @Override
    protected void onPostExecute(String result) {

        List<String> userListOrderDone = new ArrayList<String>();
        List<String> userListBeforOrder = new ArrayList<String>();
        List<String> idList = new ArrayList<String>();

        // ExSampleのthisを返すだけの関数(意味があるかはわからない)
        Context context = ExSample.getInstance().getApplicationContext();
        ArrayAdapter<String> beforUserList = new ArrayAdapter<String>(context, android.R.layout.simple_list_item_1, userListBeforOrder);
        ArrayAdapter<String> doneUserList = new ArrayAdapter<String>(context, android.R.layout.simple_list_item_1, userListOrderDone);


        Log.d(TAG, result);
        titleView.setText("モトデリ");
        beforUserContent.setText("配達前");
        doneUserContent.setText("配達完了");
        beforUser.setAdapter(beforUserList);
        doneUser.setAdapter(doneUserList);


        try{
            // JSONArrayで全てのレスポンスを取得
            JSONArray all = new JSONArray(result);
            dataList = new ArrayList<Map<String, String>>();
            for(int i = 0; i < all.length(); i++){
                data = new HashMap<String, String>();
                // それぞれの欲しい値を取得することができる
                JSONObject json = all.getJSONObject(i);
                String email = json.getString("user");
                String total = json.getString("total");
                String status = json.getString("status");
                String id = json.getString("id");
                // ユーザの状態によって配達済みか身配達かを判定(Javaの文字列の比較でハマる初心者多いイメージ)
                if(status.equals("true")){
                    data.put("text1", email);
                    data.put("text2", id);
                    dataList.add(data);
                }
                else{
                    userListOrderDone.add(String.format("%s: %s円", email, total));
                }
            }
        }
        catch (JSONException e){
            System.out.println(e);
        }

        // 先ほど取り出したデータを元に値を入れる
        adapter = new ListAndView(
                context,
                dataList,
                R.layout.row,
                new String[] {
                        "text1",
                        "text2",
                    },
                new int[] {
                        android.R.id.text1,
                        android.R.id.text2,
                });
        // リストビューに反映
        listView.setAdapter(adapter);
        listView.setTextFilterEnabled(false);
    }
}

AsyncHttpRequestPostのポイント

  • ほぼAsyncHttpRequestGetと同じ。
  • post requestなので値を成型する必要がない。
  • バックエンドの変更点はあとで記述。
AsyncHttpRequestPost.java
package es.exsample;
import android.content.Context;
import android.os.AsyncTask;
import android.widget.TextView;
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.URL;
import javax.net.ssl.HttpsURLConnection;

public class AsyncHttpRequestPost extends AsyncTask<String, Void, String> {
    private static final String TAG = "AsyncHttpRequestPost";
    private TextView textView;

    public AsyncHttpRequestPost(Context context) {
        super();
    }

    @Override
    protected String doInBackground(String... params) {
        StringBuilder sb = new StringBuilder();
        InputStream inputStream = null;
        HttpsURLConnection connection = null;

        try {
            // URL 文字列をセットします。
            URL url = new URL(params[0]);
            connection = (HttpsURLConnection)url.openConnection();
            connection.setConnectTimeout(3000); // タイムアウト 3 秒
            connection.setReadTimeout(3000);

            // POST
             connection.setRequestMethod("POST");
            connection.setRequestProperty("Content-Type", "application/json");
            connection.setRequestProperty("Authorization", "JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo2LCJ1c2VybmFtZSI6ImIxODA2NDI5QHBsYW5ldC5rYW5hemF3YS1pdC5hYy5qcCIsImV4cCI6MTU5NTIzMTIzNiwiZW1haWwiOiJiMTgwNjQyOUBwbGFuZXQua2FuYXphd2EtaXQuYWMuanAifQ.18LotiLgemUmSXTqmdcjjD3eKLSL1B13N87msbQswoE");
            OutputStream outputStream = connection.getOutputStream();
            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream));
            writer.write(params[1]);
            writer.close();
            connection.connect();

            // レスポンスコード。
            int responseCode = connection.getResponseCode();
            if(responseCode != HttpsURLConnection.HTTP_OK) {
                throw new IOException("HTTP responseCode: " + responseCode);
            }

            // 文字列化
            inputStream = connection.getInputStream();
            if(inputStream != null) {
                BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
                String line;
                while ((line = reader.readLine()) != null) {
                    sb.append(line);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(connection != null) {
                connection.disconnect();
            }
        }
        return sb.toString();
    }

}

ListAndViewのポイント

  • SimpleAdapterを継承して作成。
  • AsyncHttpRequestPostを使用してpostを実行。
  • onClickメソッドを使用してボタンが押されたら実行されるようになっている。
ListAndView.java
package es.exsample;

import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.SimpleAdapter;
import android.widget.TextView;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static android.content.ContentValues.TAG;

public class ListAndView extends SimpleAdapter {

    private LayoutInflater inflater;
    private List<? extends Map<String, ?>> listData;

    // 各行が保持するデータ保持クラス
    public class ViewHolder {
        TextView text1;
        TextView text2;
    }

    public ListAndView(Context context, List<? extends Map<String, ?>> data, int resource, String[] from, int[] to) {
        super(context, data, resource, from, to);
        this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        this.listData = data;
    }


    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        final ViewHolder holder;

        // ビューを受け取る
        View view = convertView;

        if (view == null) {
            view = inflater.inflate(R.layout.row, parent, false);

            holder = new ViewHolder();
            holder.text1 = (TextView) view.findViewById(android.R.id.text1);
            holder.text2 = (TextView) view.findViewById(android.R.id.text2);
            view.setTag(holder);
        } else {
            holder = (ViewHolder) view.getTag();
        }

        // holderにデータをセットする
        String text1 = ((HashMap<?, ?>) listData.get(position)).get("text1").toString();
        String text2 = ((HashMap<?, ?>) listData.get(position)).get("text2").toString();
        holder.text1.setText(text1);
        holder.text2.setText(text2);

        // ボタンを押したら実行
        Button btn = (Button) view.findViewById(R.id.rowbutton);
        btn.setTag(position);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View arg0) {
                String id = holder.text2.getText().toString();
                Context context = ExSample.getInstance().getApplicationContext();
                AsyncHttpRequestPost task = new AsyncHttpRequestPost(context);
                String url = "http://localhost:8000/post_shop/" + id;
                task.execute(url, "hi");
                Log.d(TAG, "created");
            }
        });

        return view;
    }

}

XML

  • list_item.xml
    • 全体の構成を担っている。
    • リニアレイアウトで実装。

image.png

list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/cardView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="10dp"
    card_view:cardBackgroundColor="#ffffff"
    card_view:cardCornerRadius="7dp"
    card_view:cardElevation="5dp">

    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="1542dp">

        <TextView
            android:id="@+id/title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="TextView"
            android:textSize="24sp"
            card_view:layout_constraintBottom_toBottomOf="parent"
            card_view:layout_constraintEnd_toEndOf="parent"
            card_view:layout_constraintHorizontal_bias="0.498"
            card_view:layout_constraintStart_toStartOf="parent"
            card_view:layout_constraintTop_toTopOf="parent"
            card_view:layout_constraintVertical_bias="0.01"
            />

        <TextView
            android:id="@+id/befor"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="12dp"
            android:text="TextView"
            card_view:layout_constraintTop_toBottomOf="@+id/title"
            tools:layout_editor_absoluteX="189dp"
            />

        <ListView
            android:id="@+id/beforUser"
            android:layout_width="402dp"
            android:layout_height="130dp"
            android:background="#7A7A7A"
            card_view:layout_constraintEnd_toEndOf="parent"
            card_view:layout_constraintStart_toStartOf="parent"
            card_view:layout_constraintTop_toBottomOf="@+id/befor" />



        <TextView
            android:id="@+id/done"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:text="TextView"
            card_view:layout_constraintTop_toBottomOf="@+id/beforUser"
            tools:layout_editor_absoluteX="189dp" />

        <ListView
            android:id="@+id/doneUser"
            android:layout_width="402dp"
            android:layout_height="364dp"
            android:layout_marginTop="12dp"
            android:background="#777777"
            card_view:layout_constraintEnd_toEndOf="parent"
            card_view:layout_constraintStart_toStartOf="parent"
            card_view:layout_constraintTop_toBottomOf="@+id/done" />

        <TextView
            android:id="@+id/tag"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="60dp"
            android:text="TextView"
            card_view:layout_constraintTop_toBottomOf="@+id/doneUser"
            tools:layout_editor_absoluteX="98dp" />

        <TextView
            android:id="@+id/desc"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="100dp"
            android:layout_marginTop="60dp"
            android:text="TextView"
            card_view:layout_constraintStart_toEndOf="@+id/tag"
            card_view:layout_constraintTop_toBottomOf="@+id/doneUser" />

    </android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>
  • row.xml
    • 1つ1つのリストの中身を作っている

image.png

row.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">


    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="80dp"
        tools:ignore="MissingConstraints">

        <TextView
            android:id="@android:id/text1"
            android:layout_width="278dp"
            android:layout_height="80dp"
            android:layout_weight="1"
            android:gravity="center_vertical"
            android:text="Text1"
            android:textSize="18dp" />

        <TextView
            android:id="@android:id/text2"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:gravity="center_vertical|right"
            android:text="Text2"
            android:textSize="18dp" />

        <Button
            android:id="@+id/rowbutton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:focusable="false"
            android:focusableInTouchMode="false"
            android:text="配達した!" />

    </LinearLayout>

</android.support.constraint.ConstraintLayout>

ちょっと説明が雑ですが、これは実際にコピペして使ってもらえるといいと思います!

Djangoのバックエンドの変更点(postの実行)

  • 管理者が配達を完了したら通知をする機能を作成。
  • AsyncHttpRequestPostのボタンを押したらそのエンドポイントに処理がとび、選択してるカートを所有しているユーザーのカートを空にしてメールを飛ばす。(メールの部分は省略)
  • 引数のpkでカートのIDを指定しています。
shop/views.py
######
### 省略 ###

# 以下を追加

from rest_framework.decorators import api_view

@api_view(['POST'])
def order_done_post_request(request, pk):
    user_info = UserInfomation.objects.get(id=pk)
    user = User.objects.get(email=user_info)
    if request.method == 'POST':
        cart = Cart.objects.get(cart_id=user_info.cart.cart_id)

        order_done(request, str(cart))

        user = user_info
        # カートの管理は論理削除にではなく負の値を入れることで実装(謎)
        cart.cart_id = -cart.cart_id
        user.status = None
        cart.save()
        user.save()
        return Response(None) # 何も返さなくていいよ

### 省略 ###

int型のpkを指定することでカートIDを指定

shop/urls.py
urlpatterns = [
    ### 省略 ###
    path('post_shop/<int:pk>', views.order_done_post_request,),  # 追加
]

以上がAndroid Studioが復活してから1日で頑張って実装した内容です。こうして記事にしてみると割愛してる部分が多いのにも関わらず、文量はとても多くなってしまいました。

まとめ

こんなにも簡単なアプリケーションを作成するだけでも1日かかってしまい、環境構築もくっそめんどくさいのでエンジニアの方はすごいなあと毎回感心させられます。今回はAndroidでしたが、そのうちiOSにも手を出したいなと思います。パソコン買い換えないと無理ですが。
結構雑な記事になってしまったので、質問はコメント欄またはTwitterのDMあたりに送ってくれれば答えます!あとは、間違ってる点や抜けている点がございましたらコメントください!

それでは学生の皆さん良い夏休みを〜!!!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした