Retrofitで実装したAPIにRxJavaを混ぜてみる
前回はRetrofitでグルメAPIを叩く処理を実装しました。
今回はそれにRxJavaを加えてみたいと思います。正直API1つだけで単純にデータを取得してるだけの処理だとあまり恩恵を感じないかもしれません。
あるAPIを叩いて戻り値を受け取ってその戻り値を元にまた別のAPIを叩くとかPromise的な処理をしたいときには便利そう。
app/build.gradle
compile 'com.squareup.retrofit2:adapter-rxjava:2.0.0-beta3' compile 'io.reactivex:rxandroid:1.0.1'
まずは定義。次に、MainActivity部分。
Retrofit retrofit = new Retrofit.Builder() .baseUrl("http://webservice.recruit.co.jp") .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .addConverterFactory(GsonConverterFactory.create()) .build();
RxJavaを使用するときは上のように定義しないとだめらしいです。
Observable<GourmetData> observable = service.getGourmetData(Environment.API_KEY, "Z012", "json"); observable .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .map(new Func1<GourmetData, List<Shop>>() { @Override public List<Shop> call(GourmetData gourmetData) { return gourmetData.results.getShop(); } }) .subscribe(new Subscriber<List<Shop>>() { @Override public void onCompleted() { System.out.println("Completed!"); } @Override public void onError(Throwable e) { System.out.println("Error:" + e.getMessage()); } @Override public void onNext(List<Shop> shops) { StringBuilder builder = new StringBuilder(); for (Shop shop : shops) { builder.append(shop.getName() + "\n"); } ((TextView) findViewById(R.id.textview)).setText(new String(builder)); } });
後はAPIのコール部分です。CallをObservableに変更して、通信は非同期・TextViewへの反映はUIスレッドで行おうとするとこんな感じの記述になりました。RxJavaは触り始めたばっかでまだまだ理解できていません・・・。精進せねば
Retrofit2を試してみる
最近RxJavaについて勉強中なんですが、RxJavaで非同期通信を行うときに相性の良いライブラリとしてRetrofitというものがあるという話を聞いて試してみようと思いました。
今回はリクルートさんのグルメサーチAPIを使ってみたいと思います。
まずはbuild.gradleに記述します。
compile 'com.squareup.retrofit2:retrofit:2.0.0-beta3' compile 'com.squareup.retrofit2:converter-gson:2.0.0-beta3' compile 'com.google.code.gson:gson:2.4'
パーミッションも忘れずに
<uses-permission android:name="android.permission.INTERNET"/>
次にAPIのリファレンスやら参考サイトを読みながらPOJOを作っていきます。
リファレンスに書いてあるhttp://webservice.recruit.co.jp/hotpepper/gourmet/v1/?format=json
にAPIキーを指定して読み込みJSONデータを取得します。
その後、jsonschema2pojoを使ってPOJOを作成します。
Retrofitを使って天気情報を取得してみる【Android】
に詳しく記載があります。
作成したクラスはmodelsパッケージ内に格納しました。
次にRetrofitで通信部分を作成します。
public interface HotPepperService { @GET("/hotpepper/gourmet/v1/") Call<GourmetData> getGourmetData(@Query("key") String key, @Query("large_area") String area, @Query("format") String format); }
通信結果を入れるオブジェクトはGourmetDataです。
最初、レスポンスのJSONを見てみるとルートがresultsとなってるのでレスポンスもResultsにしたら動きませんでした。
レスポンスを受け取るオブジェクトはJSONのルートのオブジェクトを持つクラスにしてあげると良いみたいです。
ここからはMainActivityへの記述になります。
まずは、APIのインスタンスを作ります。
Retrofit retrofit = new Retrofit.Builder() .baseUrl("http://webservice.recruit.co.jp") .addConverterFactory(GsonConverterFactory.create()) .build(); HotPepperService service = retrofit.create(HotPepperService.class);
Call<GourmetData> call = service.getGourmetData(Environment.API_KEY, "Z012", "json"); call.enqueue(new Callback<GourmetData>() { @Override public void onResponse(Response<GourmetData> response) { List<Shop> shopList = response.body().results.getShop(); StringBuilder builder = new StringBuilder(); for (Shop shop : shopList) { builder.append(shop.getName() + "\n"); } ((TextView) findViewById(R.id.textview)).setText(new String(builder)); } @Override public void onFailure(Throwable t) { Toast.makeText(getApplicationContext(), t.getMessage(), Toast.LENGTH_LONG).show(); } });
結果はこんな感じです。とても簡単にできますね。
クエリパラメータをStringBuilderを使ってごにょごにょしてたのがアホらしくなってきました。
https://github.com/kseito/RetrofitPractice/tree/first
ソースはGitHubに置いておきます。
Androidでテスト入門してみた②
前回は、基本的なEspressoの使い方を学びました。
今回は試合回数を保存する機能をSharedPreferencesを使って実装します。
その前にまずは、UIが余りにもひどいので整えます。
ソースはこちらを参照。
UI改修後はこんな感じです。カラーもスプラトゥーンを意識して入れました。
それではSharedPreferencesの実装をしていきたいと思います。
実装自体は
・呼び出し時に保存している値を取得して表示
・カウントアップ・ダウンボタンを押されたら値を変更してその値を保存
しているだけです。
public void initCounter() { int count = mSharedPreferences.getInt(COUNT, 0); mTextCounter.setText(String.valueOf(count)); } @OnClick(R.id.count_up_button) public void clickCountUp(View view) { int count = Integer.parseInt(mTextCounter.getText().toString()) + 1; mTextCounter.setText(String.valueOf(count)); saveCount(count); } @OnClick(R.id.count_down_button) public void clickCountDown(View view) { int count = Integer.parseInt(mTextCounter.getText().toString()) - 1; if (count < 0) { return; } mTextCounter.setText(String.valueOf(count)); saveCount(count); } private void saveCount(int count) { mSharedPreferences.edit().putInt(COUNT, count).commit(); }
そしてテストの方はなかなか良いプラクティスが見つからなくて迷った結果、Dagger2を使うことになりました。
Dagger自体使うのが初めてであまり理解できてないので勘違いしていたらすいません。
まず、やってることはテスト時にSharedPreferencesの参照先を変えて通常起動時とカウントの保存先を分けてるだけです。
言ってしまえば簡単なんですが、ここまでたどり着くのに色々試行錯誤しました・・・。その結果、既にTDDが破綻して記事のタイトルを変えることにしました(泣)
まずは定義
app/build.gradle
apt "com.google.dagger:dagger-compiler:2.0" compile 'com.google.dagger:dagger:2.0' compile 'org.glassfish:javax.annotation:10.0-b28'
次に本番で使用するSharedPreferencesを生成するモジュール
@Module public class PreferencesModule { Context context; public PreferencesModule(Context context) { this.context = context; } @Singleton @Provides SharedPreferences providePreferences() { return context.getSharedPreferences(MyApplication.PREF, Context.MODE_PRIVATE); } }
テスト時は下のモジュールを使用します。
@Module public class MockPreferencesModule { public static final String POSTFIX = "_test"; Context context; public MockPreferencesModule(Context context) { this.context = context; } @Singleton @Provides SharedPreferences provideSharedPreferences() { return context.getSharedPreferences(MyApplication.PREF + POSTFIX, Context.MODE_PRIVATE); } }
上記Moduleを束ねる?Componentのベースとなるインターフェースを準備します。
public interface PrefsComponent { void inject(MainActivity mainActivity); }
これをまた本番用・テスト用に分けます。
本番用
@Singleton @Component(modules = PreferencesModule.class) public interface AppPrefsComponent extends PrefsComponent { }
テスト用
@Singleton @Component(modules = MockPreferencesModule.class) public interface TestComponent extends PrefsComponent { void inject(MainActivityTest mainActivityTest); }
Componentのinject内で定義しているクラスの@Injectがついてるメンバ変数にModule内の@Provideをつけてるメソッドの戻り値を返すって感じでしょうか。文章にすると分かりづらいですね・・・
あとはテストの@Beforeで使用するComponentを差し替えることでテスト用のSharedPreferencesを使うようにしました。
保存機能を入れると前回書いたテストにも影響が出てしまったのですべてのテストを修正する必要がありました。最終系は下記のようになりました。
@RunWith(AndroidJUnit4.class) @LargeTest public class MainActivityTest { @Inject SharedPreferences mPrefs; @Singleton @Component(modules = MockPreferencesModule.class) public interface TestComponent extends PrefsComponent { void inject(MainActivityTest mainActivityTest); } @Rule public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class, true, false); @Before public void setUp() { Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); MyApplication application = (MyApplication) instrumentation.getTargetContext().getApplicationContext(); TestComponent component = DaggerMainActivityTest_TestComponent.builder() .mockPreferencesModule(new MockPreferencesModule(instrumentation.getContext())) .build(); application.setmPrefsComponent(component); component.inject(this); } @Test public void checkDefaultText() { mActivityRule.launchActivity(new Intent()); onView(withId(R.id.text_counter)).check(matches(withText("0"))); } @Test public void clickCountUp() { mActivityRule.launchActivity(new Intent()); onView(withId(R.id.count_up_button)).perform(click()); onView(withId(R.id.text_counter)).check(matches(withText("1"))); } @Test public void clickCountDown() { mActivityRule.launchActivity(new Intent()); onView(withId(R.id.count_up_button)).perform(click()); onView(withId(R.id.text_counter)).check(matches(withText("1"))); onView(withId(R.id.count_down_button)).perform(click()); onView(withId(R.id.text_counter)).check(matches(withText("0"))); onView(withId(R.id.count_down_button)).perform(click()); onView(withId(R.id.text_counter)).check(matches(withText("0"))); } @Test public void checkSaveCount() { mActivityRule.launchActivity(new Intent()); onView(withId(R.id.count_up_button)).perform(click()); onView(withId(R.id.count_up_button)).perform(click()); onView(withId(R.id.count_up_button)).perform(click()); int count = mPrefs.getInt(MainActivity.COUNT, 0); Assert.assertEquals(3, count); } }
次回はお気に入りのTodoistのAPIを使ってTODOをこなすとポイント追加みたいなことをしてみたいです。
いつになるのか。
一応、ソース置いておきます
Androidでテスト入門してみた①
早速ですが、僕はスプラトゥーンが大好きです。
新武器が出るとギアをこの組み合わせにすればこういう戦法が取れるのではないかと考えはじめます。
夜に考えはじめると眠れなくなります。
プレイし始めると1時間のつもりが2時間、3時間とやってしまい際限なくやって休日が潰れます。そして日曜日の夕方に
「ああ、今週何もできなかった・・・」と悔やむことがあります。
そんな日々に終止符を打ちたいと思い、スプラトゥーンを自分へのご褒美としてTODOをやるとできるような仕組みを作ろうと思い
「スプラカウンターというアプリを考えました。」
中身はただのカウントアップ・ダウンをしてその結果を表示するアプリです。
TODOをやったらカウントアップ、試合をやったらカウントダウンします。人力です。完全にテストの勉強で作ってます。
早速作っていきます。
<開発環境>
・Android Studio 1.5.1
まずは空のプロジェクトを作成して実行します。
次にテストを作っていきます。テスト内容は
1. カウントを表示するビュー(以下カウンター)がデフォルトで0になっている
2. カウントアップを押すとカウンターが1増える
3. カウントダウンを押すとカウンターが1減る
です。
今回はテストにEspressoを使用するので./app/gradleで定義します。
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'
次にandroidTest内のApplicationTestクラスと同じ階層にMainActivityTestクラスを作成します。
MainActivityTest.java
@RunWith(AndroidJUnit4.class) @LargeTest public class MainActivityTest { @Rule public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<MainActivity>(MainActivity.class); } }
中身はこんな感じ。ActivityTestRuleは理解できてません。
特定のAcitivtyを起動しておいてくれる便利処理という認識です。
ここにテストを書いていきます。
1. カウントを表示するビュー(以下カウンター)がデフォルトで0になっている
ここでテストコード書いているときに気づいた。EspressoでViewの参照を取ってくるときにID指定するけど、レイアウトxmlに書いてないとエラーになりますね。仕方ないのでxmlを作ります。これすでにTDDじゃないのでは・・・
みんなどうやってテスト書いてるのか知りたい。
activity_main.xml
<RelativeLayout 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" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="kztproject.jp.splacounter.MainActivity"> <TextView android:id="@+id/text_counter" android:layout_width="match_parent" android:layout_height="wrap_content" /> <Button android:id="@+id/count_up_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" /> <Button android:id="@+id/count_down_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_alignParentRight="true" /> </RelativeLayout>
テストがメインなのでレイアウトは雑に作ります。
テストは下記のようになります。
MainActivityTest.java
@Test public void testCounterShow() { onView(withId(R.id.text_counter)).check(matches(withText("0"))); }
Android StudioのProjectにあるMainActivityTest.javaを右クリックし Run 'MainActivityTest'を選択してテストを実行します。
あれ、テストがない?ちゃんと書いたはずなんですが・・・
調べてみるとテストランナーの指定がなかったのでAndroidJUnitRunnerの記述を./app/build.gradleに追記しました。
build.gradle
apply plugin: 'com.android.application' android { compileSdkVersion 23 buildToolsVersion "23.0.2" defaultConfig { applicationId "kztproject.jp.splacounter" minSdkVersion 14 targetSdkVersion 23 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:23.0.1' androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1' androidTestCompile 'com.android.support.test:runner:0.4.1' }
その後に、実行ボタン隣のドロップダウンリスト?をクリックしてEdit Configurationを選択
Specific instrumentaion runnerにAndroidJUnitRunnerの記述を加えます。
再度実行してみます。
いい感じに失敗したので、TextViewにデフォルト0を入れてあげます。
android:text="0"
通りました。
これで1は完了です!
2. カウントアップを押すとカウンターが1増える
テストの土台ができたのでサクサク行きます。
@Test public void clickCountUp() { onView(withId(R.id.count_up_button)).perform(click()); onView(withId(R.id.text_counter)).check(matches(withText("1"))); }
実行すると失敗します。
カウントアップ処理を作ります。
Viewの操作が入るのでButter Knifeを使います。
@OnClick(R.id.count_up_button) public void clickCountUp(View view) { int count = Integer.parseInt(mTextCounter.getText().toString()); mTextCounter.setText(String.valueOf(count + 1)); }
こんな感じの処理になりました。
再度テストを実行すると全テストがパスしました。
3. カウントダウンを押すとカウンターが1減る
最後です。試合ができる回数がマイナスになるのはおかしいので
カウントダウンのテストと0のときに押しても0のままになることを確認するテストを書きます。
@Test public void clickCountDown() { onView(withId(R.id.count_up_button)).perform(click()); onView(withId(R.id.text_counter)).check(matches(withText("1"))); onView(withId(R.id.count_down_button)).perform(click()); onView(withId(R.id.text_counter)).check(matches(withText("0"))); onView(withId(R.id.count_down_button)).perform(click()); onView(withId(R.id.text_counter)).check(matches(withText("0"))); }
メソッド分けなくてもいい気がしますが、分けたほうがわかりやすいかも?
@OnClick(R.id.count_down_button) public void clickCountDown(View view) { int count = Integer.parseInt(mTextCounter.getText().toString()); mTextCounter.setText(String.valueOf(count - 1)); }
カウントダウン処理を入れました。これで実行します。
エラーになります。今のソースではマイナスの値も入力できてしまいます。
なので
@OnClick(R.id.count_down_button) public void clickCountDown(View view) { int count = Integer.parseInt(mTextCounter.getText().toString()); if (count == 0) { return; } mTextCounter.setText(String.valueOf(count - 1)); }
マイナス値回避処理を追記します。
これで実行すると全てのテストが通ります。
簡単ではありますがこれで終わりです。
終わってから気づいたんですが、カウントの保存機能がないという致命傷が・・・
まだ実用段階ではないですね。
もう疲れたので次回以降実装したいと思います。
頑張ってブログ書いたのでスプラトゥーンしてきます。ではでは<追記>
需要があるかわかりませんがGitHubにあげておきます。
スプラカウンター
GridView in RecyclerViewとかListViewとか
スクロールできるView内にGridViewを配置するとうまく表示できないときの対処方法です。
以前にもはまって今回思い出すのに時間がかかってしまったので個人的にメモ。
Add a GridView to a ListView in Android - Stack Overflow
GridViewを継承してonMeasure()内で高さを可能な限り大きくする。
親がスクロール可能なViewなのでGridViewのすべての子が見えるような配置になるってことでしょうか。
onMeasure(), onLayout()はチラッチラッと見てますが、業務で特殊なViewをあまり作らない+実験的に作る気にもなれないので放置中です。。。
イカの一生は短い
スプラトゥーンめっちゃはまってます。
最近、B+になりました+ガチヤグラが実装されてすごい熱い展開になってます。
ガチヤグラ・・・やばいですね。
敵味方合計8人が小さいヤグラに乗る為に一点に集まる。ガチエリアと比べものにならないくらいキル・デスが増えます。
ガチヤグラの場合、少しでも前に進めたほうが勝ちなので、特攻も1つの戦法として有効になるためです。
味方が敵陣ど真ん中でヤグラに乗ってたら死に戻り後即ジャンプしてヤグラに飛び乗り少しでも前へ!って感じです。
なので、味方全員が只管ヤグラにジャンプを繰り返しごり押しでヤグラをゴールに押し込むという
試合があったりします。全員キル数<デス数だったけど勝つという意味不明な展開です。B+になってからは冷静に対処されてそんな展開はありませんが、、
特攻しすぎて13デスとかしてると、生きてる時間より死んでる時間のほうが長いんじゃないかって思います。イカの一生短すぎ。
やっぱりゲームが好きだ
ついに買いました!
先日、スプラトゥーンを買いました。しかもWiiUごと。最初は買うつもりはなかったんですが、
奥さんが早めの誕生日プレゼントならいいよということだったので、お言葉に甘えて買ってしまいました。
なぜ買ったのか
ニコニコ動画で試写会の実況動画を見たときから、これ絶対面白いじゃん!って思ってました。
さらに発売後もニコニコ動画のランキングにスプラトゥーンの動画が常時上がっている+TwitterのTLで楽しそうにスプラトゥーンをプレイする奴らの声が・・・
そして動画を指を咥えて見ている私に見かねて奥さんが買ってくれました。寛大すぎワロタ。