四畳半の秘密基地

さあ、今日はどんな実験をしよう

MENU

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は触り始めたばっかでまだまだ理解できていません・・・。精進せねば

github.com

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);

後は、作成したインスタンスAPIを呼び出すだけです。

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();
    }
});

f:id:k-seito:20160130220915p:plain


結果はこんな感じです。とても簡単にできますね。
クエリパラメータをStringBuilderを使ってごにょごにょしてたのがアホらしくなってきました。

https://github.com/kseito/RetrofitPractice/tree/first
ソースはGitHubに置いておきます。

Androidでテスト入門してみた②

前回は、基本的なEspressoの使い方を学びました。
今回は試合回数を保存する機能をSharedPreferencesを使って実装します。

その前にまずは、UIが余りにもひどいので整えます。

ソースはこちらを参照。

f:id:k-seito:20160123111037p:plain

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


まずは空のプロジェクトを作成して実行します。

f:id:k-seito:20160116120312p:plain


次にテストを作っていきます。テスト内容は
1. カウントを表示するビュー(以下カウンター)がデフォルトで0になっている
2. カウントアップを押すとカウンターが1増える
3. カウントダウンを押すとカウンターが1減る
です。


今回はテストにEspressoを使用するので./app/gradleで定義します。

androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'


次にandroidTest内のApplicationTestクラスと同じ階層にMainActivityTestクラスを作成します。

f:id:k-seito:20160116134541p:plain

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'を選択してテストを実行します。

f:id:k-seito:20160116134519p:plain

あれ、テストがない?ちゃんと書いたはずなんですが・・・
調べてみるとテストランナーの指定がなかったので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を選択

f:id:k-seito:20160116134945p:plain

Specific instrumentaion runnerにAndroidJUnitRunnerの記述を加えます。

f:id:k-seito:20160116135024p:plain

再度実行してみます。

f:id:k-seito:20160116135159p:plain

いい感じに失敗したので、TextViewにデフォルト0を入れてあげます。

android:text="0"

f:id:k-seito:20160116135541p:plain

通りました。
これで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で楽しそうにスプラトゥーンをプレイする奴らの声が・・・
そして動画を指を咥えて見ている私に見かねて奥さんが買ってくれました。寛大すぎワロタ。

プレイした感想

ナワバリバトルしかしてませんが、非常にバランスの良いゲームだなと感じました。マッチングシステムが優秀で同じレベルの人たちとプレイできるので大差で負けることが余りないです。
後、エイミング方法にジャイロセンサーを採用しており今までにないやり方で新鮮な感じがしました。
レベリングも良い感じで今出てる武器を一通り使って慣れてきた頃に新たな武器が出てくるので飽きない。

ゲームとの付き合い方

大学の頃にネトゲ廃人やってたのでかなり警戒してました。FEZとかゲットアンプドとか対人系は特にはまると止まらなくなるので、スプラトゥーンも危ういと思いました。ので1日5試合を宣言し、越えたら罰的なものを用意してます。
今は罰しかないですが、参考書読み終わったら追加でX試合OKとか使えば良い感じのインセンティブになるのではないかと今思いついた。


そんな感じでプレイしてます。まずは目指せランク20!
あ、ちなみにシオカラーズはアオリ派です。