四畳半の秘密基地

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

MENU

CentoOSにJDKが入れらないとき

wget http://download.oracle.com/otn-pub/java/jdk/8u73-b02/jdk-8u73-linux-x64.rpm


参考サイトによくあるように上記のようなコマンドを打つとダウンロードに失敗します。

--2016-03-18 08:57:30--  http://download.oracle.com/otn-pub/java/jdk/8u73-b02/jdk-8u73-linux-x64.rpm
Resolving download.oracle.com... 165.254.42.40, 165.254.42.51
Connecting to download.oracle.com|165.254.42.40|:80... connected.
HTTP request sent, awaiting response... 302 Moved Temporarily
Location: https://edelivery.oracle.com/otn-pub/java/jdk/8u73-b02/jdk-8u73-linux-x64.rpm [following]
--2016-03-18 08:57:30--  https://edelivery.oracle.com/otn-pub/java/jdk/8u73-b02/jdk-8u73-linux-x64.rpm
Resolving edelivery.oracle.com... 23.218.8.217
Connecting to edelivery.oracle.com|23.218.8.217|:443... connected.
HTTP request sent, awaiting response... 302 Moved Temporarily
Location: http://download.oracle.com/errors/download-fail-1505220.html [following]
--2016-03-18 08:57:31--  http://download.oracle.com/errors/download-fail-1505220.html
Connecting to download.oracle.com|165.254.42.40|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 5307 (5.2K) [text/html]

こんな感じです。
いろいろ調べてたら答えが書いてあるサイトがありました。

blog.tottokug.com

正解は

wget --no-cookies --no-check-certificate --header "Cookie: oraclelicense=accept-securebackup-cookie" http://download.oracle.com/otn-pub/java/jdk/8u73-b02/jdk-8u73-linux-x64.rpm

たしかにブラウザではAcceptのラジとボタン選択するけどCUIでダウンロードするときは必要ないのかなとか思ってたけど、そんなことありませんでしたorz
おそらくまたハマると思われるのでメモ。

TDDBCに行ってきた

久しぶりの更新になります。
本日、TDDBC in Tokyo 2016-02 - TDDBC | Doorkeeperに参加させて頂きました。
TDD自体は興味があって趣味の範囲でちょこちょこJUnit触ったりとか最近だとEspresso触ったりしていたんですが、
業務ではまったくやっていないため入門書をかじった程度で止まっていました。
そこから先に進んでみたいなと思い今回参加に踏み切りました。

午前の部(導入)

午前中は@setoazusaさんによるTDDとはなんぞや?という解説(基調講演)を聞きました。
TDDの知識はネットと本でしか得ておらず、実践している方からお話が聞けたのはとてもためになりました。
特に自分に刺さったのは

  • 不安をテストで表現する
  • TDDをやれば偉いわけじゃない
  • なぜTDDするのか

です。特になぜTDDするのか?は考えさせられました。一番最初に興味を持ったのは当時担当していたAndroidアプリのリリース頻度が短くて毎回人力テストをしていたときに辛みが増してきて、リリース前のテストを自動化できたらいいのにとか思ってたときですね。最近では、Espressoかっけー!テストやってる感じがする!テスト書いてる自分偉い!ってなってました。開発効率全然考えてない・・・。プログラマの三大美徳がおざなりになってました。

その後は、運営チームの方がペアプロのデモをしてくれました。
デモを見ながら、あーこんなペースで話しながら開発するなんて無理だわって思ってました。
手慣れてる感がすごい。

午後の部(ペアプロでTDD)

午後は実際にペアプロしてみましょうということで早速ペア決めを。自分はJavaを選択しました。ここでKotlinとか言えたらかっこよかったのかな。
その後、お題が出されそのお題をTDDしながら2人で開発していくことに。ペアプロ自体もあまりやったことがなかったので新鮮でした。というかペアでキーボードを叩いていないときは何をすればいいかわからなかったのでとりあえず、スペルミスがないかチェックと実装に悩んでたら一緒に悩む的なことをしてました。最終的には課題4?まで終わってフィニッシュ。後半は頭の回転が鈍くなりペアの方の実装を目で追うので精一杯に・・・

ここで疑問が。ペアプロの場合、組む相手が自分の知らないライブラリやクラスを使い始めたらどうすればいいんだろう。
ペアでする以上片方がついていけなくなるとペアプロの意味がなくなるような気もする。かといって一々解説してもらってたら開発効率が下がるだろうなーと思ったり。

ためになったことは仮実装という概念です「。座標(1, 2)を与えたらgetXは1を返す」というテストをするとき、一番最初に実装するgetXの戻り値は1というハードコーディングでも良いというのは驚きでした。あるテストがあったときにそのテストを最低限の労力でグリーンにするということを念頭に置いて実装していくといいのかなと思いました。

後はレビューという時間があるのも新鮮でした。コードについて議論の経験に乏しい自分では皆の言ってることに納得しかできませんでした。もっと議論できるようになりたい。

改善点とか

終了間際に運営にフィードバックあったらお願いしますって言われましたが、TDDにより頭がパンクしていてその場ではまったく出てきませんでした。帰宅して、ブログに起こすにあたり思い当たる節があったので箇条書きでまとめます。

  • デモのときにペアプロの解説が欲しかった(ドライバー・オブザーバーの役割等)
  • (個人のレベルもありますが)課題が途中から急に難易度が高くなり実装を考える時間が大幅に増えたので、問題をもう少し簡単にしてもらえると実装を考える時間を減らしTDDに費やせる時間が増やせるのかなと思いました。

次のステップ

勉強会に行って満足するときが多々あるのでに次のアクションにつなげるようにしています。今回のTDDBCで得た情報を元に何をしようかと考えましたがこんな感じかな。

  • JUnit実践入門読み返す
  • プライベートで開発してるプロジェクトにJUnit導入
  • 書籍:リファクタリング―既存のコードを安全に改善する― を買う

最後に、とても有意義な場を提供していただいてありがとうございました。
時間がなくて懇親会には参加できませんでしたが、次回があれば参加したいです。

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をあまり作らない+実験的に作る気にもなれないので放置中です。。。