パルカワ2

最近はFlutterをやっています

最近読んだ本

1分間リーダーシップ―能力とヤル気に即した4つの実践指導法

1分間リーダーシップ―能力とヤル気に即した4つの実践指導法

読み返した。
新1分間リーダーシップというのが、新しく出たっぽいのでそれも読んでおきたい。

新1分間リーダーシップ

新1分間リーダーシップ


10分の面談で部下を伸ばす法

10分の面談で部下を伸ばす法

寝る前にざっくり読み返した。

酒に酔った勢いでかはわからないけど、いつの間にか買っていて、読んでみるととても良かった。「伝える力」というのも一つの技術力であるな、と感じる。

5巻以内の最高の漫画知りたいとか言ってたら教えてもらった。やれやれだぜという気持ちで読み進めた。

最近読んだ本

途中までしか読んだことがなかったので最後まで読めてよかった。

仲の良い友達とともに大金のかかったゲームに挑戦! ゲームクリアのコツは“友達を疑わないこと”。だが、一度の裏切りが疑念を生み、ゲームは息づまる心理戦となる! 金か友か? 人の心をゆさぶる究極の頭脳ゲーム漫画、誕生!

マンガボックスで読んでいて、続きが気になったので買った。

「この世界はマンガ、僕が主人公なんだ」

中学二年生の津乃峰アリスは、クラスメイトの星野宇宙から「自分達がいる世界はマンガで、僕が主人公である」と告白された。そう言われてみると、確かに見える「フキダシ」。常に“何者か”に“読まれている”状態での学校生活…アリスのプライベートも、心の中も、すべて丸見えに? そしてその先にある「真相」とは? IKKI新人賞・イキマン出身の実力派新人作家、超待望の初単行本!

面白かった。紙で読んだほうがよかったかもしれない。2巻完結。

MVPアーキテクチャを使いたいという話をチームにした。

最近Androidアプリを開発していて、チームメンバーみんなでテストを書いたりしているわけなのですが、いかんせんUIテストを書くことになりがちで自分も含めみんなが疲弊しているなぁと感じていました。

せっかくみんながテストを書こうとしていて、非常に良い状態なのにこのままだとテストを書くことが疲れること(=やりたくないこと)になると思えたので、色々考えた結果、チームメンバーにMVPアーキテクチャを使いたすぎる!とプレゼンをしたので、資料を公開します。

チーム向けに話したので、補足です。

  • 全てのアプリで、MVPアーキテクチャを必ず使うべきという話ではない
  • Roboletric 3.xは、Activityを起動出来たりするわけですが、今のプロジェクトではだいぶ頑張らないとそれが動かないので、その機能は使っていません
  • hisaichi5518/minne-android は、僕が勝手にMVPアーキテクチャでminneの一部機能を書き直したやつなので公開できません
  • 資料を作って話すのだいぶめんどいけど、伝わりやすいと思うので良さそう

以下、参考にしたリンクです。

https://github.com/rallat/EffectiveAndroid
http://tomoima525.hatenablog.com/entry/2015/08/13/190731
http://konifar.hatenablog.com/entry/2015/04/17/010606

こういう感じの話をもう少し膨らませてDroidKaigi 2016で話したかったんですが、CFPの提出に間に合わなかったので諦めました。こちらからは以上です。

Retrofitでテストするときは、MockClientを作る

RestAdapter.BuildersetClient()できるので、テスト時はそこにテスト用のClientをセットしてあげる。そのテスト用のClientをMockClientとしました。

本当は、Clientをわざわざ作るのがめんどくさいなと思って作らない方針で行こうとしたんですが、基盤チームの人にsetClientする方法があるでと改めて指摘されたあと考えなおして、Clientを作るのはだるいけど作ったほうが後々楽でわかりやすいなと思えてきたので作りました。

流れとしては、以下のような感じを想定しています。

  • @BeforeでMockClientをセットする(正確にはRestAdapterをセットする)
  • @Testで なにをどうモックするか指定する
  • @Afterで指定したモックをクリアする
// MockClientTest.java
public class MockClientTest {
    MockClient mMockClient = new MockClient();
    @Before
    public void setUp() throws Exception {
        // RestAdapterを作ってセット
        ApiClient.getInstance().setRestAdapter(RestAdapterFactory.newMocking(mMockClient));
    }

    @After
    public void tearDown() throws Exception {
        // 次のテストで引き継がないようにclear
        mMockClient.clear();
    }

    @Test
    public void testMock() throws Exception {
        // ここでAPIのbodyはなにを返すか指定する
        mMockClient.mock("/v1/reviews/100.json").to(200, "reviews/ok.json");
        Review review = ApiClient.getInstance().create(ReviewsService.class).fetch(100);

        assertThat(review.getStar()).isEqualTo(5);
    }

    @Test
    public void testMock_route() throws Exception {
        MockClient.Route route = mMockClient.mock("/v1/reviews/100.json").to(200, "reviews/ok.json");
        ApiClient.getInstance().create(ReviewsService.class).fetch(100);

        assertThat(route.called()).isEqualTo(true); // mockは呼ばれたか?
        assertThat(route.times()).isEqualTo(1); // 呼ばれた回数
    }

    @Test
    public void testMock_regex() throws Exception {
        // 正規表現でも可能
        MockClient.Route route = mMockClient.mock("/v1/reviews/\\d.json").to(200, "reviews/ok.json");
        ApiClient.getInstance().create(ReviewsService.class).fetch(100);

        assertThat(route.called()).isEqualTo(true);
    }
}
// MockClient.java
public class MockClient implements Client {
    List<Route> mRoutes = new ArrayList<>();
    Route notFoundRoute = new Route("404").to(404, "response/not_found.json");

    @Override
    public Response execute(Request request) throws IOException {
        for (Route route : mRoutes) {
            Response response = route.match(request.getUrl());
            if (response == null) {
                continue;
            }
            return response;
        }

        return notFoundRoute.newResponse(request.getUrl());
    }

    public Route mock(String urlPath) {
        Route route = new Route(urlPath);
        mRoutes.add(route);

        return route;
    }

    public void clear() {
        mRoutes = new ArrayList<>();
    }

    public class Route {
        Pattern mUrlPathRegex;
        int mStatus;
        int mTimes;
        String mJsonPath;

        Route(String urlPath) {
            mUrlPathRegex = Pattern.compile(urlPath);
        }

        public Route to(int status, String jsonPath) {
            mStatus = status;
            mJsonPath = jsonPath;

            return this;
        }

        public int times() {
            return mTimes;
        }

        public boolean called() {
            return mTimes >= 1;
        }

        private Response match(String url) throws IOException {
            Matcher matcher = mUrlPathRegex.matcher(url);
            if (!matcher.find()) {
                return null;
            }

            return newResponse(url);
        }

        private String getContentType() {
            return "application/json";
        }

        private Response newResponse(String url) throws IOException {
            // https://github.com/gfx/Android-Helium/blob/master/app/src/test/java/com/github/gfx/helium/TestUtils.java#L22
            byte[] body = TestUtils.getAssetFileInBytes(mJsonPath);

            mTimes++;
            return new Response(
                    url,
                    mStatus,
                    "mocked-response",
                    new ArrayList<Header>(),
                    new TypedByteArray(getContentType(), body)
            );
        }
    }
}

テストが並列で走ることとかはまだ考えてないです。
Clientを作るのめんどいと思ったけど、案外すぐ出来たので、そういうの良くないなって思いました。

RecyclerViewでスクロールすると次のページを取得する

やってみる。扱うデータ量が多いので、無限ということにした。Pageの数制限してもいいかも。またリストの一番下に付いたら次のページをロードするのでは遅く感じるので、下の近くにきたらロードにしてみる。

// ScrollPagerListener.java
public abstract class ScrollPagerListener extends RecyclerView.OnScrollListener {

    GridLayoutManager mLayoutManager;
    int mPage = 1;
    int mCount, mPreTotalCount;
    boolean mIsLoading;

    public ScrollPagerListener(GridLayoutManager layoutManager, int count) {
        mLayoutManager = layoutManager;
        mCount = count;
    }

    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);

        // ロードするときに走るのは無駄なので、先にreturnする
        if (dx == 0 && dy == 0) {
            return;
        }

        int totalItemCount = mLayoutManager.getItemCount();
        int lastVisibleItem = mLayoutManager.findLastVisibleItemPosition() + 1;

        // 前回スクロールした時よりアイテム数に変化があれば更新が終わった事とする
        if (totalItemCount != mPreTotalCount) {
            mIsLoading = false;
        }

        mPreTotalCount = totalItemCount;

        // load()が走るべき箇所までスクロールしたか
        int loadLine = totalItemCount - mCount;
        if (lastVisibleItem >= loadLine && !mIsLoading) {
            mIsLoading = true;
            load(++mPage);
        }
    }

    public abstract void load(int page);
}

// HogeFragment.java
View onCreateView(...) {
    mRecyclerView.addOnScrollListener(new ScrollPagerListener(layoutManager, 10) {
        @Override
        public void load(int page) {
            mPresenter.showCategoryProducts(page);
        }
    });
}

ちゃんとテスト書いてないから不安だけど、それっぽく動いてるように見える。

RecyclerView使ってみてる

便利
Master of Recycler View見れば大体の事書いてそう。
GridLayoutManager使ってて、真ん中の幅整えたいとかは RecyclerView.ItemDecoration で出来そうな感じがした。

class CategoryProductDecoration extends RecyclerView.ItemDecoration {
    private int mHorizontal;
    private int mVertical;

    public CategoryProductDecoration(int horizontal, int vertical) {
        mHorizontal = horizontal;
        mVertical = vertical;
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        GridLayoutManager.LayoutParams layoutParams = (GridLayoutManager.LayoutParams) view.getLayoutParams();

        int position = layoutParams.getViewLayoutPosition();
        if (position == RecyclerView.NO_POSITION) {
            outRect.set(0, 0, 0, 0);
            return;
        }

        int spanIndex = layoutParams.getSpanIndex();
        outRect.left = spanIndex == 0 ? 0 : mHorizontal; // 一番左は設定しない
        outRect.top = spanIndex == position ? 0 : mVertical; // 一行目は設定しない
        outRect.right = 0;
        outRect.bottom = 0;
    }
}

techbooster.booth.pm