パルカワ2

PerlとRubyとイチャラブ

RecyclerViewで複雑な画面を作るためのライブラリ Epoxy

この記事は、 Android Advent Calendar 2017 - Qiita 11日目の記事です。


RecyclerViewで複雑な画面を作りたい!そんな欲求を持っている人が多いと思います。僕もそうです。でも、1個や2個のViewTypeならまだしもたくさんのViewTypeがある場合、getItemViewTypeとかで頑張った結果つらいコードになる未来が見えます。というか見てきました。

Epoxy というアプローチ

Airbnbが作ったRecyclerViewで複雑な画面を作るためのライブラリです。

github.com

ModelをCustomViewやDataBinding等から自動生成し、それらを操作するControllerを書き、そのControllerをEpoxyRecyclerViewというクラスにセットする感じです。

具体的に出来ることは、READMEに書かれてある通りなのですが、こういう事が出来ます。

This abstracts the boilerplate of view holders, diffing items and binding payload changes, item types, item ids, span counts, and more, in order to simplify building screens with multiple view types. Additionally, Epoxy adds support for saving view state and automatic diffing of item changes.

ワオ!便利!

使ってみる

サンプルアプリがあったので、それのコードを読んでいきたいと思います。

epoxy/epoxy-sample at master · airbnb/epoxy · GitHub

こういう動きをするアプリです。

f:id:hisaichi5518:20171211183913g:plain

これら全ての動作を全部追いかけると大変なので、今回は、DataBindingを利用しているAdd, Clearなどのボタンにフォーカスを当てたいと思います。 このような動きをする部分です。

f:id:hisaichi5518:20171211184004g:plain

Modelを作る

今回のサンプルアプリでDataBindingは、 button.xml で利用されています。このbutton.xmlは、リスト上にあるAdd, Clearなどを行うボタンを指しています。

epoxy/button.xml at d2e298902e64a5b634e9932ee2f16f56abfa52bd · airbnb/epoxy · GitHub

DataBindingを利用してるViewのModelを自動生成する場合は、package-info.java@EpoxyDataBindingLayoutsを利用して、Modelを自動生成することを宣言する必要があります。

epoxy/package-info.java at a46ed6c6c459f4d102a63d15aceb0e13dc5d246b · airbnb/epoxy · GitHub

@EpoxyDataBindingLayouts(R.layout.button)
@PackageModelViewConfig(rClass = R.class)
package com.airbnb.epoxy.sample;

import com.airbnb.epoxy.EpoxyDataBindingLayouts;
import com.airbnb.epoxy.PackageModelViewConfig;

これで、Modelの生成が行われ、ButtonBindingModel_ という名前のクラスが生成されます。

Controller を作る

ButtonBindingModel_SampleControllerというクラスで利用されています。コードを見ると案外シンプルです。

epoxy/SampleController.java at a46ed6c6c459f4d102a63d15aceb0e13dc5d246b · airbnb/epoxy · GitHub

今回のコードを読むにあたって、僕が重要だなと感じたのは、protected void buildModels(List<CarouselData> carousels)です。

epoxy/SampleController.java at a46ed6c6c459f4d102a63d15aceb0e13dc5d246b · airbnb/epoxy · GitHub

buildModels(List carousels)

このメソッドは、リストのデータが更新されるたびに実行されます。

buildModelsの中では、このようなコードがあり、何を表示するのか?やそのViewを表示するのか?などの操作が行われていることがわかります。

    // 表示文言の指定
    // Clickしたら、onAddCarouselClicked callbackを実行する
    addButton
        .textRes(R.string.button_add)
        .clickListener((model, parentView, clickedView, position) -> {
          callbacks.onAddCarouselClicked();
        });

    clearButton
        .textRes(R.string.button_clear)
        .clickListener(v -> callbacks.onClearCarouselsClicked())
        .addIf(carousels.size() > 0, this); // データの条件があえばViewを表示する

SampleControllerをEpoxyRecyclerViewにセットする

作成したControllerは、EpoxyRecyclerViewにセットしています。

ここは、RecyclerViewにセットも出来るようですが、EpoxyRecyclerViewを使うことで、シンプルになり更にパフォーマンスのアレコレもやってくれるそうです。

EpoxyRecyclerView · airbnb/epoxy Wiki · GitHub

    recyclerView.setController(controller);

セットしているのはこの部分

epoxy/MainActivity.java at a46ed6c6c459f4d102a63d15aceb0e13dc5d246b · airbnb/epoxy · GitHub

あとはデータの更新を行いたい時にcontroller.setData(...)を呼び出すだけです。

  private void updateController() {
    controller.setData(carousels);
  }

所感

コード読んだ部分が簡単過ぎた。

データの更新が行われるたびにループがデータ分実行されるのが気になりました。データが大きくなるとつらそう。 RecyclerViewのAdapterにあるnotifyItemInsertedのようにどこが変わったのか通知する形の方が早そうな印象は受けましたが、実際どうなのかは調べてないです。

その他のアプローチ

Githubを検索すると色々あるんですが、どれがいいのかというのは正直よくわかりませんでした。それらと比較して自分がメンテナンスするアプリへの導入を検討するといいと思います。

まとめ

RecyclerViewを利用して複雑な画面を作るためのライブラリであるEpoxyのサンプルアプリのコードを読んでみました。

複雑な画面はなるべく作りたくないですが、Epoxyには様々な機能があり、複雑な画面を作るのにとても有効だと感じました。次は、実際に開発しているアプリで試してみようと思います。

参考

デスクライト買った

コード書いてる時とか集中してるときは暗い方が集中出来るので、部屋では灯りをつけずに暗くしてる。けど、そのまま夜になったりすると真っ暗の中にモニタの明かりが自分を照らしていて、ツイッターとか見てニヤニヤしてると漫画とかに出てくる引きこもり感がすごいので部屋の電気を付けに歩いていた。だるすぎる。なので机用の灯りでも買うかという感じで買った。

光をこっちに向けると眩しいし目が潰れるので壁のほうに向けてる。想像よりでかかったけど、安いしちょうどいい感じの明るさで便利。


奥に展示されてるのはNature Remoなので、部屋の電気もNature Remoでやれよって話なんだけど、リモコンを捨てたので出来ない。
勧められたシーリングライトあるけどまだ買ってません

おはぎレビュー #2

我々の世界では、人間が作ったおはぎをうんこでないかレビューすることを「おはぎレビュー」と言う。

ohagi day #12

おはぎを作っていたつもりがうんこを作ってたなんて事は人間であればよくある。それは大した問題ではなく問題なのは、うんこをおはぎとして客に出し、客がうんこを食べてしまうことである。他にもおはぎレビューを行う理由はあるが、我々がおはぎレビューを行う理由の1つがそれだ。

毎日のようにおはぎのようなうんこを作り、うんこのようなおはぎを作ってしまう不完全な人間である我々にとって、おはぎレビューはなくてはならない存在だ。
なくてはならない存在だからこそ、我々はおはぎレビューについて考え、議論し、学び、より良くしていく必要がある。

「みんなではじめるデザイン批評」という本がある。その本の「批評を理解する」という章には以下のように書かれている。これは、おはぎレビューも同様だと考える。

私たちは全員がスキルやノウハウを持ち寄って、協力し合う必要があるのだ。そしてそのためには、互いに話をしなければならない。何をデザインしているのか、なぜそれを作っているのか、どのように完成させていくのかを議論しなければならないのである。

みんなではじめるデザイン批評―目的達成のためのコラボレーション&コミュニケーション改善ガイド

みんなではじめるデザイン批評―目的達成のためのコラボレーション&コミュニケーション改善ガイド

おはぎレビューでは、レビュアーとレビュイーがお互いが理解し合う努力が必要であると3年前に書いた。それはおはぎレビューが、レビュアーの指摘によってうんこからおはぎにするのではなく、レビュアーとレビュイーの会話によって、うんこからおはぎに近づけるプロセスであるからだ。

おはぎレビューにおける会話とは、以下のようなものではない。

レビュアー「ここはAが起きるとつらいのでBに直したほうがいいと思います」
レビュイー「修正します」
レビュイー「修正したので確認お願いします」

この場合だとレビュアーの考えしか出ておらず、レビュアーの指摘によってうんこからおはぎにするプロセスと化している。これだとおはぎがうんこになってしまう可能性がある。

おはぎレビューでは、レビュイーが自分の考えを積極的に伝えていく必要がある。

レビュイー「ここはAという理由からBにしました」
レビュアー「なるほど、BだとCになってしまうのでDが良いかと思いましたがどうですか?」
レビュイー「Cに気づいてませんでした。それを考えるとDが良いと思いました!そうします!」

このようにおはぎレビューにおける会話とは、お互いが自分の考えを相手に伝えることである。そうすることで「そうだったのか!」「知らなかった!」を見つけて、うんこからおはぎに近づけていく。

レビュイーとしての自分が最近気をつけているのは、レビュイーの自分から会話を始めるということだ。自分がなぜこの形のおはぎにしたのか・この味にしたのか・また悩んでる事をレビュアーが知る事で、より会話が生まれやすい。
また、言語化することで、自分の考えもまとまるし、コメントとして残す事で後日レビューを見た時に何を考えておはぎを作ったのかわかって良いことだらけだ。

まとめ

  • レビュイーとレビュアーは自分の考えを伝え、相手の考えを聞く事を繰り返す事で、お互い理解しあえて、うんこがおはぎに近づいていく。
  • レビュイーは自分から積極的に会話を促すと良い

ohagi day #16

ここで利用させて頂いた全ての画像は、正常なおはぎです。

FILCO ウッドパームレスト買った

前使ってたゴムみたいなやつがボロボロになったので、家用と会社用に買った。
木なので冷たいのかなと思ったけどそんな事なかった。なんかさわり心地もいいし、もっと早く買っとけばよかった。

ユリゴコロを見た

yurigokoro-movie.jp


丸の内TOEIまで行った










まとめ

吉高由里子は可愛い

minneをAndroid Studio 3.0に対応させた

hisaichi5518.hatenablog.jp

↑の続きです。
Android Studio Release Notes | Android Studio
Android Developers Blog: Android Studio 3.0

com.android.tools.build:gradle:3.0.0 を使う

まあ、そうですよね

assertThat(images).isNotNull が使えなくなったのでやめた

minneではテストでKotlinとassertjを利用しているんですが、 .isNotNull()ではなく .isNotNullと書けてた。一応.isNotNull()を利用するようにしましょうという感じだったんだけど、残念なことにいくつか漏れがあってAndroid Studio 3.0にしたタイミングで.isNotNullコンパイルができなくなったので、修正した。

testCompile "org.robolectric:httpclient:3.4.2"

まあまあ昔からあるアプリなので、 org.apache.http.NameValuePairに依存してたりする。今まではエラーになってなかったけど、Android Studio 3.0にしたタイミングでテストでClassNotFoundExceptionを吐くようになったので、robolectricのhttpclientを追加した。

このあたりは、消したい気持ちをこらえる必要がありました。

Annotation Processorがオフになってるよと言ってきたのでONにした

Android Studioが警告を出してきたのでポチポチした。
前も出てたけど、クリックしても設定画面に飛ばなかったんですが、今回は設定画面に飛んで設定出来るようになりました。

まとめ

というわけで、minneを運営するGMOペパボは本日めでたくリリースされたAndroid Studio 3.0 stable対応企業です。よろしくお願いいたします。
【minne】Androidアプリエンジニア / GMOペパボ株式会社

ActivityOptionsCompat.makeSceneTransitionAnimation(...) を使う

Activity間で共通の画像があって、それをシューンと移動させるのがマテリアルしてんじゃん!という感じなのでやってみた。
試したのは、エミュレータAndroid 7.0です。全部のコードはここにある。

github.com

f:id:hisaichi5518:20170927234954g:plain

このような感じに動くシンプルなやつ

public class MainActivity extends AppCompatActivity {

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);

        GlideApp.with(this)
                .load(R.drawable.sample)
                .into(binding.view1);

        RxView.clicks(binding.view1).subscribe(__ -> {
            ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(
                    this, binding.view1, "image");

            ContextCompat.startActivity(this, new NextActivity.IntentBuilder(this).build(), options.toBundle());
        });
    }
}

遷移先のActivity

public class NextActivity extends AppCompatActivity {

    private ActivityNextBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_next);

        GlideApp.with(this)
                .load(R.drawable.sample)
                .into(binding.view1);

        RxView.clicks(binding.view1).subscribe(__ -> {
            ActivityCompat.finishAfterTransition(this);
        });
    }


    static class IntentBuilder {
        private final Context context;

        IntentBuilder(Context context) {
            this.context = context;
        }

        Intent build() {
            return new Intent(context, NextActivity.class);
        }
    }
}

あとは遷移先のImageViewにandroid:transitionName="image"を指定してあげると動く。
案外簡単だった。windowに値をセットして〜〜〜とか説明してる記事がいくつかあったけど、特にいらなかった。試したのがAndroid 7.0だからかもしれない。またtransitionNameはstrings.xmlとかに書くのが本当はいいと思う。

時々変に移動する時がある。これはよくわからなかった。