DMM.comの、一番深くておもしろいトコロ。

Support Library 28.0.0 alpha新APIのrecyclerview-selectionを使ってみた

Support Library 28.0.0 alpha新APIのrecyclerview-selectionを使ってみた

f:id:dmminside:20180315181752p:plain

はじめまして、この3月に入社したAndroidエンジニアの kgmyshin です。

Androidより、2018年3月初旬に Support Library 28.0.0 alpha がリリースされましたね。 ReleaseNoteを見てみると、新しいAPIとしてrecyclerview-selectionが追加されました。

今回は、早速その新しいAPIを触ってみた所感として、Android開発者向けに「どういうものなのか」「どう使えば良いのか」を紹介していきたいと思います。

recyclerview-selection とは

まずはこちらを見てください。

RecyclerViewでこの挙動を自力で実装しようとすると少し大変です。 タップで複数選択する機能であれば難しくないですが、ドラッグ中に選択状態にしたり、選択中のみオートスクロールも実装したりとなると少し苦労します。

この〈複数選択をドラッグでやりつつオートスクロールなども〉を良い感じにやってくれるのが recyclerview-selection です。

使い方

すごく大まかには、2ステップで実装できます。

  1. SelectionTracker インスタンスをつくる
  2. RecyclerView.Adapter#onBindViewHolder で選択状態を View に反映する

早速実装してみましょう。

今回は、下記のBook クラスのリストを一覧表示した RecyclerView に対して複数選択機能を実装していきます。

data class Book(
        val id: Long,
        val title: String,
        val subTitle: String
)

選択状態中に getSelection を呼び出した時に、どのリストを返却してほしいか。 選択中の Book そのものを返してほしいのか、 選択中の Bookid を返すのかで実装が少し変わってきます。

今回は後者の方法で実装していきます。前者の方法での実装例はgithubにあげてありますので、興味あるの方はそちらをご覧ください。

前準備

まずはstableIdbook.idを使うようにします。

stableId とは RecyclerView に設定する各アイテムのIDのことです。自分で有効にしない限りは NO_ID が設定されています。これを有効にして適切に設定してあげることで RecyclerView のパフォーマンスに効くことがあります。

下記のように RecyclerView.Adapter#setHasStableIdstrueをセットし、getItemId をオーバーライドして book.id を返却するようにすれば前準備の完了です。

class BookAdapter(
        context: Context,
        private val bookList: List<Book>
) : RecyclerView.Adapter<BookViewHolder>() {
    :
    init {
        setHasStableIds(true)
    }
    :
    override fun getItemId(position: Int): Long = bookList[position].id
    :
}

SelectionTrackerインスタンスをつくる

次に SelectionTracker インスタンスを作りましょう。 Builder を用いて下記のように作ります。

selectionTracker = SelectionTracker.Builder<Long>(
        "my-selection-id",
        binding.recyclerView,
        StableIdKeyProvider(binding.recyclerView),
        BookIdDetailsLookup(binding.recyclerView),
        StorageStrategy.createLongStorage()
).build()

Builderのコンストラクタの各引数についての説明は下記です。

第n引数 説明
第1引数 "my-selection-id" 使用する activtyfragment でユニークになるように指定します。 onRestoreInstanceState などで Bundle から取得する keyとして使ってるようです
第2引数 recyclerView 該当の RecyclerView を指定してください。
第3引数 StableIdKeyProvider(binding.recyclerView) IdKeyProvider を設定します。IdKeyProvideritem (選択対象) と key (選択時に保持するもの) の対応関係を解決するためのものです。
第4引数 BookIdDetailsLookup(binding.recyclerView) MotionEventを元に今どこの item (選択対象) の上にいるのかを検索する ItemDetailsLookup を実装したものを指定します。
第5引数 StorageStrategy.createLongStorage() savedState に何を保存するのかという情報を持った StorageStrategy インスタンスを設定します。

今回は選択時に保持するものとして stableId ( book.id ) を使用するので、IdKeyProvider には標準で用意されている StableIdKeyProvider を指定します。

自作する必要があるのは ItemDetailsLookup だけで、こちらは ItemDetailsLookupのSampleを参考に BookIdDetailsLookup は下記のように実装しました。

class BookIdDetailsLookup(
        private val recyclerView: RecyclerView
) : ItemDetailsLookup<Long>() {

    override fun getItemDetails(e: MotionEvent): ItemDetails<Long>? = recyclerView.findChildViewUnder(
            e.x,
            e.y
    )?.let {
        (recyclerView.getChildViewHolder(it) as? BookViewHolder)?.getItemIdDetails()
    }
}

RecyclerView.Adapter#onBindViewHolderで選択状態をViewに反映する

ここまで実装をしたものを動かしてみました。

見てのとおり(しっかり複数選択機能自体は動いているものの)どれが選択されているのかがまったくわかりません。 View への選択状態かどうかの反映は自分で実装していきましょう。

recyclerview-selection では選択状態になった時、最終的には RecyclerView.Adapter#onBindViewHolder が呼ばれるので、ここで背景色の変更をします。 実際には selector を作ってあげて、選択されているか否かを元に View#setActivated を呼び出します。 ( setSelected ではなく setActivated を呼ぶ理由は こちら) を参照ください。 )

選択されているか否かはSelectionTracker#isSelectedでわかるので、下記のようにして Adapter を作ります。

class BookAdapter(
        context: Context,
        private val sectionTracker: SelectionTracker<Long>,
        private val bookList: List<Book>
) : RecyclerView.Adapter<BookViewHolder>() {
  :
  override fun onBindViewHolder(
          holder: BookViewHolder,
          position: Int
  ) {
      val item = bookList[position]
      holder.bind(
              sectionTracker.isSelected(item.id), // setActivated は holder.bindの中で
              position,
              bookList[position]
      )
  }
  :
}

ただ、このコードをそのまま動かすと クラッシュします 。 なぜかというと SelectionTracker の生成の時にセットする RecyclerViewAdapter がないと IllegalArgumentException が投げられるようになっているからです。 ( Adapter の生成には SectionTrackerがいるが、 SectionTrackerの生成には逆にAdapterが必要になってしまっているからです。)

public abstract class SelectionTracker<K> {
  :
  public static final class Builder<K> {
    :
    public Builder(
      @NonNull String selectionId,
      @NonNull RecyclerView recyclerView,
      @NonNull ItemKeyProvider<K> keyProvider,
      @NonNull ItemDetailsLookup<K> detailsLookup,
      @NonNull StorageStrategy<K> storage) {
        :
        mAdapter = recyclerView.getAdapter();
        :
        checkArgument(mAdapter != null); // ← throw IllegalArgumentException
        :
    }
    :  
  }
  :
}

そのためSectionTrackerは下記のように Adapter作成後に外部から設定する必要があります。

class BookAdapter(
        context: Context,
        private val bookList: List<Book>
) : RecyclerView.Adapter<BookViewHolder>() {
  :
  var sectionTracker: SelectionTracker<Long>? = null // Adapterを作成後に、SectionTrackerを作成してそのあとにadapterにセットする
  :
  override fun onBindViewHolder(
          holder: BookViewHolder,
          position: Int
  ) {
      val item = bookList[position]
      holder.bind(
              sectionTracker?.isSelected(item.id) ?: false, // setActivated は holder.bindの中で
              position,
              bookList[position]
      )
  }
  :
}

依存関係が複雑でスッキリした実装とは言いにくいのですが(一応SampleであげているコードはSectionTrackerを直接メンバーに持つのではなくインタフェースを噛ませてますが、正直あまり納得のいく実装はできていません)、これで完成です!

所感

まだalpha版なのでバグをちらほら見かけます。ただし、冒頭にも書きましたが〈複数選択をドラッグでやりつつオートスクロール〉する処理を実装するのはひと苦労なので、そういったケースを実装する場合は新APIを使ったほうが良いなと感じました。

サンプルコードは下記に置いております。ご興味ある方はご覧ください。

採用情報

現在、DMM.com Groupでは、アプリ開発のエンジニアメンバーを募集しております! 興味のある方はぜひ下記募集ページをご確認下さい! dmm-corp.com

www.wantedly.com