RecyclerView で動的リストを作成する   Android Jetpack の一部。

Compose をお試しください
Jetpack Compose は、Android で推奨される UI ツールキットです。Compose でレイアウトを操作する方法について学習します。

RecyclerView を使用すると、大規模なデータセットを簡単かつ効率的に表示できます。データを指定し、各アイテムの表示方法を定義すると、必要に応じて、RecyclerView ライブラリが動的に要素を作成します。

RecyclerView は、その名のとおり、これらの個々の要素をリサイクルします。アイテムが画面外にスクロールされても、RecyclerView はビューを破棄せず、代わりに、RecyclerView は画面上にスクロールされた新しいアイテムにビューを再利用します。RecyclerView を使用すると、パフォーマンスとアプリの応答性が向上し、消費電力が削減されます。

主なクラス

動的リストを作成するには、複数のクラスを連携させます。

  • RecyclerView は、データに対応するビューを含む ViewGroup です。RecyclerView はビューそのものであるため、他の UI 要素を追加する場合と同じ方法でレイアウトに追加します。

  • リスト内の各要素はビューホルダー オブジェクトによって定義されます。ビューホルダーは、作成されるときにデータが関連付けられることはありません。ビューホルダーは、作成された後で RecyclerView によってそのデータにバインドされます。ビューホルダーを定義するには、RecyclerView.ViewHolder を拡張します。

  • RecyclerView は、アダプターのメソッドを呼び出すことで、ビューをリクエストし、ビューをデータにバインドします。アダプターを定義するには、RecyclerView.Adapter を拡張します。

  • レイアウト マネージャーによって、リスト内の個々の要素が配置されます。RecyclerView ライブラリによって提供されるレイアウト マネージャーを使用することも、独自のレイアウト マネージャーを定義することもできます。レイアウト マネージャーはいずれも、ライブラリの LayoutManager 抽象クラスに基づいています。

すべての要素がどのように組み合わされているかについては、RecyclerView サンプルアプリ(Kotlin)または RecyclerView サンプルアプリ(Java)をご覧ください。

RecyclerView を実装する手順

RecyclerView を使用するには、いくつかの作業が必要です。それらの作業については、以降のセクションで詳しく説明します。

  1. リストまたはグリッドの表示方法を決定します。通常は、RecyclerView ライブラリの標準レイアウト マネージャーのいずれかを使用できます。

  2. リスト内の各要素の外観と動作を設計します。この設計に基づいて、ViewHolder クラスを拡張します。ViewHolder のバージョンでは、リスト項目のすべての機能が提供されます。ビューホルダーは View のラッパーであり、そのビューは RecyclerView によって管理されます。

  3. データを ViewHolder ビューに関連付ける Adapter を定義します。

また、RecyclerView をニーズに合わせて的確に調整できる高度なカスタマイズ オプションも用意されています。

レイアウトの計画

RecyclerView のアイテムは LayoutManager クラスによって並べ替えられます。RecyclerView ライブラリには 3 つのレイアウト マネージャーがあり、レイアウトの標準的な状況を処理します。

  • LinearLayoutManager は 1 次元のリスト内にアイテムを配置します。
  • GridLayoutManager は、アイテムを 2 次元のグリッドに配置します。
    • グリッドが垂直方向に配置されている場合、GridLayoutManager は各行のすべての要素が同じ幅と高さになるようにしますが、行ごとに異なる高さを指定することもできます。
    • グリッドが水平方向に配置されている場合、GridLayoutManager は各列のすべての要素の幅と高さが同じになるようにしますが、列ごとに異なる幅を指定することもできます。
  • StaggeredGridLayoutManager は GridLayoutManager と似ていますが、行のアイテムの高さが同じである必要もなく(垂直グリッドの場合)、同じ列のアイテムの幅が同じである必要もありません(水平グリッドの場合)。結果として、行または列内のアイテムが互いにオフセットされることになります。

また、個々のアイテムのレイアウトを設計する必要もあります。このレイアウトは、次のセクションで説明するように、ビューホルダーを設計するときに必要になります。

アダプターとビューホルダーを実装する

レイアウトを決定したら、Adapter と ViewHolder を実装する必要があります。これら 2 つのクラスが連携して、データの表示方法を定義します。ViewHolder は、リスト内の各アイテムのレイアウトを含む View のラッパーです。Adapter は、必要に応じて ViewHolder オブジェクトを作成し、それらのビューのデータも設定します。ビューをこれらのデータに関連付けるプロセスは、バインディングと呼ばれます。

アダプターを定義する際は、次の 3 つの主要なメソッドをオーバーライドします。

  • onCreateViewHolder(): RecyclerView は、新しい ViewHolder を作成する必要があるたびにこのメソッドを呼び出します。このメソッドは、ViewHolder とそれに関連する View を作成して初期化しますが、ビューのコンテンツは埋めません(ViewHolder はこの時点で特定のデータにバインドされていません)。

  • onBindViewHolder(): RecyclerView はこのメソッドを呼び出して、ViewHolder をデータに関連付けます。このメソッドは適切なデータを取得し、そのデータを使用してビューホルダーのレイアウトを埋めます。たとえば、RecyclerView が名前のリストを表示する場合、このメソッドはリスト内で適切な名前を見つけて、ビューホルダーの TextView ウィジェットに入力します。

  • getItemCount(): RecyclerView は、このメソッドを呼び出してデータセットのサイズを取得します。たとえば、アドレス帳アプリでは、アドレスの合計数になります。RecyclerView は、これを使用して、表示できるアイテムがなくなったことを判断します。

次に、ネストされ、データのリストを表示する ViewHolder を持つ簡単なアダプターの典型的な例を示します。この例では、RecyclerView はテキスト要素の簡単なリストを表示しています。アダプターには、ViewHolder 要素のテキストを含む文字列の配列が渡されます。

Kotlin

class CustomAdapter(private val dataSet: Array<String>) :
        RecyclerView.Adapter<CustomAdapter.ViewHolder>() {

    /**
     * Provide a reference to the type of views that you are using
     * (custom ViewHolder)
     */
    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val textView: TextView

        init {
            // Define click listener for the ViewHolder's View
            textView = view.findViewById(R.id.textView)
        }
    }

    // Create new views (invoked by the layout manager)
    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
        // Create a new view, which defines the UI of the list item
        val view = LayoutInflater.from(viewGroup.context)
                .inflate(R.layout.text_row_item, viewGroup, false)

        return ViewHolder(view)
    }

    // Replace the contents of a view (invoked by the layout manager)
    override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {

        // Get element from your dataset at this position and replace the
        // contents of the view with that element
        viewHolder.textView.text = dataSet[position]
    }

    // Return the size of your dataset (invoked by the layout manager)
    override fun getItemCount() = dataSet.size

}

Java

public class CustomAdapter extends RecyclerView.Adapter<CustomAdapter.ViewHolder> {

    private String[] localDataSet;

    /**
     * Provide a reference to the type of views that you are using
     * (custom ViewHolder)
     */
    public static class ViewHolder extends RecyclerView.ViewHolder {
        private final TextView textView;

        public ViewHolder(View view) {
            super(view);
            // Define click listener for the ViewHolder's View

            textView = (TextView) view.findViewById(R.id.textView);
        }

        public TextView getTextView() {
            return textView;
        }
    }

    /**
     * Initialize the dataset of the Adapter
     *
     * @param dataSet String[] containing the data to populate views to be used
     * by RecyclerView
     */
    public CustomAdapter(String[] dataSet) {
        localDataSet = dataSet;
    }

    // Create new views (invoked by the layout manager)
    @Override
    public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
        // Create a new view, which defines the UI of the list item
        View view = LayoutInflater.from(viewGroup.getContext())
                .inflate(R.layout.text_row_item, viewGroup, false);

        return new ViewHolder(view);
    }

    // Replace the contents of a view (invoked by the layout manager)
    @Override
    public void onBindViewHolder(ViewHolder viewHolder, final int position) {

        // Get element from your dataset at this position and replace the
        // contents of the view with that element
        viewHolder.getTextView().setText(localDataSet[position]);
    }

    // Return the size of your dataset (invoked by the layout manager)
    @Override
    public int getItemCount() {
        return localDataSet.length;
    }
}

各ビューアイテムのレイアウトは、通常どおり XML レイアウト ファイルで定義されます。この場合、アプリの text_row_item.xml ファイルは次のようになります。

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="@dimen/list_item_height"
    android:layout_marginLeft="@dimen/margin_medium"
    android:layout_marginRight="@dimen/margin_medium"
    android:gravity="center_vertical">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/element_text"/>
</FrameLayout>

次のステップ

次のコード スニペットは、RecyclerView の使用方法を示しています。

Kotlin

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val dataset = arrayOf("January", "February", "March")
        val customAdapter = CustomAdapter(dataset)

        val recyclerView: RecyclerView = findViewById(R.id.recycler_view)
        recyclerView.layoutManager = LinearLayoutManager(this)
        recyclerView.adapter = customAdapter

    }

}

Java

RecyclerView recyclerView = findViewById(R.id.recycler_view);
recyclerView.layoutManager = new LinearLayoutManager(this)
recyclerView.setAdapter(customAdapter);

このライブラリには、実装をカスタマイズするためのさまざまな方法も用意されています。詳細については、RecyclerView の高度なカスタマイズをご覧ください。

エッジ ツー エッジ表示を有効にする

RecyclerView でエッジ ツー エッジ表示を有効にする手順は次のとおりです。

  • enableEdgeToEdge() を呼び出して、下位互換性のあるエッジ ツー エッジ表示を設定します。
  • リストアイテムが最初にシステムバーと重なる場合は、RecyclerView にインセットを適用します。これを行うには、android:fitsSystemWindows を true に設定するか、ViewCompat.setOnApplyWindowInsetsListener を使用します。
  • RecyclerView で android:clipToPadding を false に設定することで、スクロール中にリストアイテムがシステムバーの下に描画されるようにします。

次の動画は、エッジ ツー エッジ表示が無効(左)と有効(右)の RecyclerView を示しています。

インセット コードの例:

Kotlin

ViewCompat.setOnApplyWindowInsetsListener(
  findViewById(R.id.my_recycler_view)
  ) { v, insets ->
      val innerPadding = insets.getInsets(
          WindowInsetsCompat.Type.systemBars()
                  or WindowInsetsCompat.Type.displayCutout()
          // If using EditText, also add
          // "or WindowInsetsCompat.Type.ime()" to
          // maintain focus when opening the IME
      )
      v.setPadding(
          innerPadding.left,
          innerPadding.top,
          innerPadding.right,
          innerPadding.bottom)
      insets
  }
  

Java

ViewCompat.setOnApplyWindowInsetsListener(
  activity.findViewById(R.id.my_recycler_view),
  (v, insets) -> {
      Insets innerPadding = insets.getInsets(
              WindowInsetsCompat.Type.systemBars() |
                      WindowInsetsCompat.Type.displayCutout()
              // If using EditText, also add
              // "| WindowInsetsCompat.Type.ime()" to
              // maintain focus when opening the IME
      );
      v.setPadding(
              innerPadding.left,
              innerPadding.top,
              innerPadding.right,
              innerPadding.bottom
      );
      return insets;
  }
);
  

RecyclerView XML:

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/my_recycler_view"
    android:clipToPadding="false"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

参考情報

Android でのテストの詳細については、次のリソースをご覧ください。

サンプルアプリ