Hatena::Groupxn--272ax3f

Android アプリ開発勉強会 #5

Android アプリ開発勉強会 #5

ハイクの public タイムラインを表示する

Connecting to the Network

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

HTTP クライアント

Android SDK に含まれる HTTP クライアントは 2 つ

Gingerbread (Android 2.3) 以降なら HttpURLConnection がオススメらしい。

が、Apache HttpClient の方が使いやすい気がする。

HttpURLConnection を使うサンプルはドキュメントにあるので、この資料では Apache HttpClient を使う。

ネットワークへの接続の確認

ネットワーク接続の前にネットワーク接続が使用できるかどうか確認すると良い。

public void myClickHandler(View view) {
    ...
    ConnectivityManager connMgr = (ConnectivityManager) 
        getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
    if (networkInfo != null && networkInfo.isConnected()) {
        // fetch data
    } else {
        // display error
    }
    ...
}

Apache HttpClient を使う例

    // リクエスト成功時に生成されるべきオブジェクトを返り値にして失敗時に例外を発生させるよりも
    // 成功時の結果と失敗時の結果のどちらかを表すオブジェクトを返すようにした方が扱いやすい
    private HaikuTimelineResponse getHaikuPublicTimeline() {
        // リクエスト生成
        HttpUriRequest req = new HttpGet("http://h.hatena.ne.jp/api/statuses/public_timeline.json");
        // クライアント生成
        DefaultHttpClient httpClient = new DefaultHttpClient();
        try {
            // HTTP リクエストの実行
            // 今回の例では常に不正でない URI であることが保証されているので大丈夫だが
            // 不正な URI (空文字列) の HttpUriRequest だとここで実行時例外
            HttpResponse res = httpClient.execute(req);
            // レスポンスの処理
            if (res.getStatusLine().getStatusCode() == 200) {
                String resStr = EntityUtils.toString(res.getEntity(), "UTF-8");
                List<String> statuses = createStatusesFromJson(resStr); // ここら辺は後で
                return HaikuTimelineResponse.createSuccessResponse(statuses);
            } else {
                return HaikuTimelineResponse.createErrorResponse("リクエストに失敗");
            }
        } catch (JSONException err) {
            return HaikuTimelineResponse.createErrorResponse(err.getMessage());
        } catch (ClientProtocolException err) {
            return HaikuTimelineResponse.createErrorResponse(err.getMessage());
        } catch (IOException err) {
            return HaikuTimelineResponse.createErrorResponse(err.getMessage());
        } finally {
            // 終了処理
            httpClient.getConnectionManager().shutdown();
        }
    }

    // 手抜きだけどこんな感じ (無名内部クラスを使ってるのは手抜きなだけで特に意味はない)
    private static abstract class HaikuTimelineResponse {
        public abstract boolean isSuccess();
        public List<String> getTimeline() {
            throw new UnsupportedOperationException();
        }
        public String getErrorMessage() {
            throw new UnsupportedOperationException();
        }
        public static HaikuTimelineResponse createSuccessResponse(final List<String> statuses) {
            return new HaikuTimelineResponse() {
                @Override public boolean isSuccess() { return true; }
                @Override public List<String> getTimeline() { return statuses; }
            };
        }
        public static HaikuTimelineResponse createErrorResponse(final String errMsg) {
            return new HaikuTimelineResponse() {
                @Override public boolean isSuccess() { return false; }
                @Override public String getErrorMessage() { return errMsg; }
            };
        }
    }

スレッドの話 (ネットワーク接続は別スレッドで行うこと)

  • AndroidUI 処理は単一スレッド (“main” thread) で行われる
    • JavaScript と同じ感覚: for ループ回してるとボタン押せない、みたいな感じ
    • main スレッドネットワーク通信を行うと UI 処理がブロックされる
    • 時間がかかる処理は別スレッドで行うこと
      • JS で時間がかかる処理は Worker でやるみたいな感じ

よくない例

    <Button
        android:id="@+id/timeline_update_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/update_timeline" />
    private void onUpdateButtonClicked(Button b) {
        CharSequence oldText = b.getText();
        b.setText("progress...");
        b.setEnabled(false);
        // ここで main スレッドをブロックしてしまうので表示更新などもされない
        try {
            Thread.sleep(5000); // ここでは単に 5 秒待つだけだが、実際にはネットワーク通信など
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        b.setText(oldText);
        b.setEnabled(true);
    }

    @Override
    public void onResume() {
        super.onResume();
        Button updateButton = (Button) findViewById(R.id.timeline_update_button);
        updateButton.setOnClickListener(new OnClickListener() {
            @Override public void onClick(View v) {
                onUpdateButtonClicked((Button) v);
            }
        });
    }

スレッドで処理する例

    private void onUpdateButtonClicked(final Button b) {
        final CharSequence oldText = b.getText();
        b.setText("progress...");
        b.setEnabled(false);
        // 別スレッドでの処理が終わった後に main スレッドで実行される処理
        final Runnable postTask = new Runnable() {
            @Override public void run() {
                b.setText(oldText);
                b.setEnabled(true);
            }
        };
        // 時間がかかる処理は別スレッドで
        Thread t = new Thread() {
            @Override public void run() {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // main スレッド (UI スレッド) にタスクを投げる
                MainActivity.this.runOnUiThread(postTask);
            }
        };
        t.start();
    }

Handler を使った例は以下のよう。

    private void onUpdateButtonClicked(final Button b) {
        final CharSequence oldText = b.getText();
        b.setText("progress...");
        b.setEnabled(false);
        // 別スレッドからのメッセージを main スレッド (より正確には Handler オブジェクトが
        // インスタンス化されたスレッド) で処理するためのハンドラ
        final Handler handler = new Handler(new Handler.Callback() {
            @Override public boolean handleMessage(Message msg) {
                b.setText(oldText);
                b.setEnabled(true);
                Log.d("thread-sample", "渡されたオブジェクト: " + (String) msg.obj);
                return false;
            }
        });
        // 時間がかかる処理は別スレッドで
        Thread t = new Thread() {
            @Override public void run() {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // main スレッド (UI スレッド) にメッセージを送る
                Message msg = new Message();
                msg.obj = "渡したいオブジェクト";
                handler.sendMessage(msg);
            }
        };
        t.start();
    }

Handler を使えばオブジェクトも渡せて便利!!

AsyncTask を使う

スレッドで何か処理をして、その後に main スレッドで処理をする、という流れを行うためのクラス。

    private void onUpdateButtonClicked(final Button b) {
        final CharSequence oldText = b.getText();
        b.setText("progress...");
        b.setEnabled(false);
        // ここでは Handler を使った場合などと比べやすいように匿名内部クラスにしているけど
        // 普通にクラスを定義した方がいいかも
        AsyncTask<Void, Void, String> t = new AsyncTask<Void, Void, String>() {
            // 別のスレッドで実行される
            // (毎回新しいスレッドができるわけではなくてプールされているスレッドが使われる)
            @Override protected String doInBackground(Void... noargs) {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return "渡したいオブジェクト";
            }
            // main スレッドで実行される
            @Override protected void onPostExecute(String result) {
                b.setText(oldText);
                b.setEnabled(true);
                Log.d("thread-sample", "渡されたオブジェクト: " + result);
            }
        };
        t.execute();
    }

マルチスレッドでの注意

ここでの例では複数スレッドから同時に触り得る変数がないので特に気にしてないが、複数スレッドから同時に 1 つの変数を扱う場合は atomic 系のクラス を使ったりロックしたりする必要がある。 同一のオブジェクトに対して複数スレッドからメソッド呼び出しを行う場合は、マルチスレッド対応のクラスであれば気にしなくてよいが、そうでない場合はやはりロックするなど呼び出し側で気を付ける必要がある。

はてなハイクへのリクエストのレスポンスを処理する

    private List<String> createStatusesFromJson(String statusesJsonStr)
    throws JSONException {
        List<String> statuses;
        JSONArray statusesJson = (JSONArray) new JSONTokener(statusesJsonStr).nextValue();
        statuses = new ArrayList<String>(statusesJson.length());
        for (int i = 0; i < statusesJson.length(); ++i) {
            JSONObject statusJson = statusesJson.getJSONObject(i);
            String haikuText = statusJson.getString("text"); // text は古いけどとりあえず使っておく
            String userId = statusJson.getJSONObject("user").getString("id");
            statuses.add("id:" + userId + " : " + haikuText);
        }
        return statuses;
    }

ListView を使う

  • リスト形式で動的に項目を配置する場合は ListView を使うと便利
  • 今回は単に文字列を表示するだけだが画像を表示したり複雑なレイアウトにしたりもできる

XML

    <ListView
        android:id="@+id/haiku_timeline_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </ListView>

表示する項目は Adapter で決定する

    private ArrayAdapter<String> timelineAdapter;

    // Adapter オブジェクトを生成し、ListView に結び付ける
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ListView timelineView = (ListView) findViewById(R.id.haiku_timeline_view);
        timelineAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);
        timelineView.setAdapter(timelineAdapter);
    }

    // Adapter オブジェクトに要素を追加することで自動的に表示が更新される
    private void setStatuses(List<String> statuses) {
        timelineAdapter.clear();
        // addAll を使いたいけど API 11 から
        for (String s : statuses) timelineAdapter.add(s);
    }

Volley

f:id:nobuoka:20130609001808p

基本的な使い方

画像周り

  • ImageLoader
  • NetworkImageView
    • 読み込み完了時に勝手に表示してくれる View
    • インスタンス生成後に URL を指定する (そのとき ImageLoader も指定)