ゴマちゃんフロンティア

アザラシが大好きなエンジニアの開発日記です

アザラシの最新情報を閲覧するAndroid用自作アプリの作成

time 2018/09/17

というわけで、今回はアザラシを愛して止まない私の「ゴマフアザラシの赤ちゃんが見たい」という私欲を達成すべく作ったAndroidアプリに関するお話です。
話としては以前作った「アザラシの最新情報を取得するAPI」の続きです。

「アザラシの赤ちゃんの最新情報を取得するAPI」を作ってみたお話

私はAndroidアプリを全く作ったことがない上、Javaという言語自体も苦手で実務経験もありません。
しかしアザラシ好きとして引くわけにはいかないので勢いで作ってみました!

開発環境について

当初はC#が得意ということもあって「Xamarin」で行こうと思ったのですが、日本語ドキュメントに乏しいこと、UWPやiOSの知識が全くないこと、純粋に重いことから挫折。
以前少しだけ触ったことのある「AndroidStudio」で行くことにしました。

そのAndroidStudioですが、ただデフォルト設定では日本語化されていなかったり、Javaのハイライト表示が微妙だったり、ショートカットキーが使いにくかったりと厳しかったです。
このままでは作業効率がよろしくないため、以下のサイトを見ていくつか設定を変えました。
http://asnet.hatenablog.com/entry/2015/05/29/094112

開発時に詰まったこと

手探りでやっていていつの間にか解決したこともあるので、ほぼ自分用のメモ書きレベルです。
エラー系はメッセージでぐぐればあれこれ出ますので、上手くいかない場合は検索してみてください。

インストール後の初回ビルドで失敗する

私の場合は「A problem occurred configuring project ‘:app’.」というエラーが出てしまいました。この場合、SDKマネージャから対応するSDKをインストールする必要があります。
SDKマネージャはメニューバーの「ツール→SDKマネージャ」から開くことができます。どれが必要か分からなかったので片っ端からインストールしました。

それでも解決せず「Gradle sync failed: Failed to find Build Tools revision 27.0.3」というエラーがでました。
Stackoverflowによると、SDKマネージャの「SDKツールタブ」を選択→「パッケージ詳細の表示」をチェックし、上記エラーと同じバージョンを確認してインストールすれば通るようになります。

Androidとの接続を認識しない

まずはスマホ側で開発者向けオプションを無効にし、システム→端末情報→ビルド番号を7回タップして再度有効にします。
その状態でAndroidStudioのターミナル上でadb kill-serveradb start-serverと実行します。コマンドが見つからない場合はC:\Users\{ユーザー名}\AppData\Local\Android\Sdk\platform-toolsにパスを通してみてください。

実行画面に機種名が出ていれば上手くいくと思います。

Androidでのネットワークアクセス

普通にAPI実行用クラスを作って実行すると例外が出てしまいます。どうやらAPIなどのネットワークアクセスをメインスレッド上で実行してはならないそうです。
なのでAPI実行クラスをAsyncTaskを継承した形に書き換えて実行しました。書いたコードは後述します。

AsyncTaskのonPostExecute()が呼ばれない

UIスレッド上でCountDownLatchインスタンスを使い非同期の完了を待つようにしましたが、onPostExecute()が呼ばれないためずっと待ち状態が解除されない事態に陥りました。かと言って非同期実行を待たずにAdapterを作るとList参照時にぬるぽしてしまいます。

結論から言うと、メソッドに@override (=正しくオーバーライドされていること) を付けた上で、API実行後の処理をonPostExecute()内に移すことで解決しました。
そのためにAsyncTask継承クラスのコンストラクタでMainActivityAdapterのインスタンスを渡し、onPostExecute()でListViewへのアダプタ設定ができるようにしました。
API実行後の処理をすべて丸投げする形になるので違和感がありますが、ネット上ではコールバック関数を渡して実行するやり方もありました。こちらのほうが読みやすいコードになりそうです。

ビューの作成

Androidでリスト形式の表示を行う場合はListViewを使うと良いようです。またリストに表示する内容もカスタマイズしたいので、ListView内の各項目を表示するためのViewも作りました。

Viewのパラメータや定石は全く分かりませんが勢いで作ります。
レイアウトやデザインを整えるのは後々でいい…というか自分しか使わないので、最低限見えるものができればOKとします。

作成・編集したクラスの紹介

API取得データ操作用クラス

APIから取得したデータを扱うためのNewsListItemクラスを作ります。

package com.gomafrontier.azarashinews;

public class NewsListItem {
    private int id;
    private String title;
    private String url;
    private String description;

    public int getId() {
        return this.id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getTitle() {
        return this.title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getUrl() {
        return this.url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getDescription() {
        return this.description;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}

privateなフィールドとゲッタ・セッタのある至って普通のものです。

API実行用のAsyncTask継承クラス

AsyncTaskクラスを継承したAPI実行用クラスを作成します。APIはJSONの配列で返ってくるので、JSONArray型のインスタンス型で受け取ります。
ListViewへのAdapter設定もこのクラスで行います。

package com.gomafrontier.azarashinews;

import android.content.res.Resources;
import android.os.AsyncTask;
import android.widget.ListView;

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

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

public class AsyncHttpRequest extends AsyncTask<Void, Integer, List<NewsListItem>> {
    private MainActivity mainActivity;
    private NewsAdapter adapter;

    public AsyncHttpRequest(MainActivity context, NewsAdapter adapter) {
        this.mainActivity = context;
        this.adapter = adapter;
    }

    @Override
    protected List<NewsListItem> doInBackground(Void... Void) {
        try {
            return getNewsList();
        } catch (Exception e) {
            System.out.println(e.getMessage());
            System.out.println(e.getStackTrace());
        }

        return null;
    }

    @Override
    protected  void onPreExecute()
    {
    }

    @Override
    protected void onPostExecute(List<NewsListItem> resultList) {
        adapter.setNewsList(resultList);

        // ニュースリスト用Viewの取得と設定
        ListView listView = mainActivity.findViewById(R.id.newsListView);
        listView.setAdapter(adapter);
    }

    private List<NewsListItem> getNewsList() throws IOException, JSONException {
        Resources resources = mainActivity.getResources();
        URL url = new URL(resources.getString(R.string.api_base_url) + resources.getString(R.string.get_news_uri));

        HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
        httpConnection.setRequestMethod("GET");
        httpConnection.connect();

        // InputStreamからJSON取得
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(httpConnection.getInputStream()));
        String json = bufferedReader.readLine();

        // JSONオブジェクトに変換
        JSONArray jsonArray = new JSONArray(json);

        List<NewsListItem> newsList = new ArrayList<>();

        for (int i = 0; i< jsonArray.length(); i++) {
            JSONObject jsonObject = jsonArray.getJSONObject(i);

            NewsListItem news = new NewsListItem();

            news.setTitle(jsonObject.getString("title"));
            news.setUrl(jsonObject.getString("url"));
            news.setDescription(jsonObject.getString("description"));

            newsList.add(news);
        }

        return newsList;
    }
}

自作のBaseAdapter継承クラス

次にBaseAdaptorを継承した自作のAdapterを作ります。getView()内でリスト用のViewにNewsListItemの要素を設定します。

package com.gomafrontier.azarashinews;

import android.content.Context;
import android.content.res.Resources;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;

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.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class NewsAdapter extends BaseAdapter {

    private Context context;
    private LayoutInflater layoutInflater;
    private List<NewsListItem> newsList;

    public NewsAdapter (Context context) {
        this.context = context;
        this.layoutInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    public void setNewsList(List<NewsListItem> newsList) {
        this.newsList = newsList;
    }

    @Override
    public int getCount() {
        return newsList.size();
    }

    @Override
    public Object getItem(int position) {
        return newsList.get(position);
    }

    @Override
    public long getItemId(int position) {
        return newsList.get(position).getId();
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        convertView = layoutInflater.inflate(R.layout.news, parent, false);

        NewsListItem newsItem = newsList.get(position);

        TextView titleTextView = convertView.findViewById(R.id.title);
        titleTextView.setText(newsItem.getTitle());

        TextView urlTextView = convertView.findViewById(R.id.url);
        urlTextView.setText(newsItem.getUrl());

        TextView descriptionTextView = convertView.findViewById(R.id.description);
        descriptionTextView.setText(newsItem.getDescription());

        return convertView;
    }
}

MainActivity

最後にMainActivityで、NewsAdaptorAsyncHttpRequestインスタンス化してAPIを実行します。
(サンプルソースをいくつか変えただけなので、オプションボタン等の使っていない部分が入っています)

package com.gomafrontier.azarashinews;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.ListView;

public class MainActivity extends AppCompatActivity {

    protected ListView listView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        // アダプタ作成
        NewsAdapter adapter = new NewsAdapter(this);

        // APIからニュースリストの取得
        AsyncHttpRequest request = new AsyncHttpRequest(this, adapter);

        try {
            request.execute();
        } catch (Exception e) {
            System.out.println("ニュースリスト取得エラー");
            e.printStackTrace();
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }
}

前述の通りAPI実行後の処理はAsyncHttpRequestクラスに丸投げしているので、メイン上のコードは割りと短めです。

完成後のアプリ画面

あれこれ苦戦しましたが、とりあえず以下のようなアプリができました。

これでアザラシの最新情報を確認しやすくなりました。あとはゴマフアザラシの赤ちゃんの情報が出たら突撃するだけです!
実際に使ってみましたが、青森やら鹿児島やら遠方の情報しかありませんでした。射程内で赤ちゃんが公開されるのを根気強く待つしかないですね。
アプリ自体も「タップでブラウザを起動しURLへアクセス」「バックグラウンドで定期的に取得し、特定のキーワードに反応して通知」など改善したい点は多いので、また気が向いたら挑戦してみます。

コメント

  • わー!すごい!このアプリは公開はされてないですか?

    あざらし  2019年7月5日 1:44 PM

    • 個人的に作ったものなので、公開はしていないです。
      使用しているAPIの関係上、不特定多数の方が使うことを想定していないので、公開は難しいかと思います。

      riberunn  2019年7月6日 10:26 AM

down

コメントする