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

はじめに

はじめまして、この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 そのものを返してほしいのか、 選択中のBook id を返すのかで実装が少し変わってきます。

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

前準備

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

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

下記のようにRecyclerView.Adapter#setHasStableIds trueをセットし、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" 使用するactivty fragment でユニークになるように指定します。onRestoreInstanceState などでBundle から取得するkeyとして使ってるようです
第2引数 recyclerView 該当のRecyclerView を指定してください。
第3引数 StableIdKeyProvider(binding.recyclerView) IdKeyProvider を設定します。IdKeyProvider item (選択対象) と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 の生成の時にセットするRecyclerView Adapter がないと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 では、エンジニアメンバーを募集しております! 興味のある方はぜひ下記募集ページをご確認下さい!

dmm-corp.com