現在、Jetpack Compose は採用されるようになり、間違いなくそれに値します。これはAndroid UI 開発における真のパラダイム シフトです。そこでは、定型的なリストの作成が大幅に排除されました。そしてそれは美しいです。
多くのプロジェクトが依然として View フレームワークを使用しているため、リストを作成するために RecyclerView を使用していると思います。
しかし、とにかく、Compose は未来であり、これが私がタイトルに「ヴィンテージ」という言葉を使用している理由です。
ヴィンテージとは古いけれど金という意味です。この記事は、RecyclerView に関する私の経験をまとめたものであり、まだ計画を進めている人のために、RecyclerView を中心としたフレームワークを構築する私のビジョンについて説明します。
フレームワークは、適切なレベルのスケーラビリティを提供する必要があります。したがって、主要なシナリオは最小限の定型文で処理する必要がありますが、ユーザーが何かカスタムのものを作成したい場合には、それが邪魔になるべきではありません。
RecyclerView を使用する際のベスト プラクティスは、リスト内のデータを更新するためにDiffUtil
API を使用することであることが知られています。ここでは車輪を再発明するつもりはありません。そのため、 DiffUtil
活用に焦点を当て、その実装のための儀式を減らすことも試みます。
リスト内に複数のタイプのビューが存在するのは自然なことです。モバイル デバイスの画面は高さによって制限され、デバイスによって画面サイズも異なります。そのため、あるデバイスではスクロールせずにコンテンツが画面に収まっていても、別のデバイスではスクロールが必要になる場合があります。
そのため、RecyclerView の上部にいくつかの画面を開発することをお勧めします。
装飾はリストの非常に一般的なケースです。また、アイテムのレイアウトに装飾をマージすることもできますが、単純そうに見えて柔軟性があまりありません。同じアイテム タイプでも、異なる画面で異なる装飾が必要になる場合や、リスト内のアイテムの位置に応じて異なる装飾が必要になる場合もあります。
最初のオプションは、すべてを最初から作成することです。実装を完全に制御できるようになりますが、いつでも実行できます。
定型文を少なくして RecyclerView を操作できるサードパーティのオープンソース フレームワークが多数あります。すべてを列挙するつもりはありませんが、まったく反対のアプローチを持つ 2 つを紹介したいと思います。
RecyclerView を使用して複雑な画面を作成するための AirBnb のソリューション。多数の新しい API が追加され、定型文をできる限り減らすためにコード生成も追加されています。
しかし、結果として得られるコードが異質に見えるという感覚を取り除くことはできません。また、注釈処理により、さらに複雑さが増します。
小さなライブラリ。実際には、異種リストを委任されたアダプタのセットに分割する方法を提案するだけの、簡潔な関数とインターフェイスの束を提供します。実際、このような独自のソリューションは比較的短時間で作成できます。
私は、すべての動作をすぐに把握できるほどシンプルで、実装の詳細がすべてカーペットの下に隠れない、小規模なネイティブ ソリューションが好きです。だからこそ私は、AdapterDelagates とその原則がフレームワークの基礎として適していると信じています。
最初の非常に基本的なことは、リスト項目の共通宣言です。現実世界のリスト項目は大きく異なりますが、唯一自信があるのは、それを比較できることです。
DiffUtil.ItemCallback
のAPIを参照することをお勧めします
interface ListItem { fun isItemTheSame(other: ListItem): Boolean fun isContentTheSame(other: ListItem): Boolean { return this == other } }
これは、フレームワーク内のリスト項目の最小限の (したがって堅牢な IMO) 宣言です。メソッドのセマンティクスは、以下を使用する DiffUtil と同じになるように意図されています。
isItemTheSame
other
とthis
の同一性をチェックします
isContentTheSame
other
とthis
のデータをチェックします
安定した一意の ID を提供する最も一般的な方法は、サーバーから取得した ID を使用することです。
そこで、考えられる定型文を少し減らすために抽象実装をしてみましょう。
abstract class DefaultListItem : ListItem { abstract val id: String override fun isItemTheSame(other: ListItem): Boolean = when { other !is DefaultListItem -> false this::class != other::class -> false else -> this.id == other.id } }
ListItem の実装がdata class
である場合、ほとんどの場合、 isContentTheSame
非常に単純になります (等価性チェック)。
サーバー データの投影ではなく、正当な ID を持たない項目がリスト内にある場合には、 ListItem
分離しておくことは依然として合理的です。たとえば、リスト内の項目としてフッターとヘッダーがある場合、または何らかのタイプの項目が 1 つある場合は、次のようになります。
data class HeaderItem(val title: String) : ListItem { override fun isItemTheSame(other: ListItem): Boolean = other is HeaderItem }
推奨されるアプローチでは、非常に自然で単一のDiffUtil
コールバック実装が可能になります。
object DefaultDiffUtil : DiffUtil.ItemCallback<ListItem>() { override fun areItemsTheSame(oldItem: ListItem, newItem: ListItem): Boolean = oldItem.isItemTheSame(newItem) override fun areContentsTheSame(oldItem: ListItem, newItem: ListItem): Boolean = oldItem.isContentTheSame(newItem) }
そして最後のステップは、AdapterDelegates からAsyncListDifferDelegationAdapter
を拡張するデフォルト アダプターの宣言です。
class CompositeListAdapter : AsyncListDifferDelegationAdapter<ListItem>(DefaultDiffUtil)
AdaptorDelegates ライブラリは、特定のビュー タイプのデリゲートを宣言する便利な方法を提供します。私たちが改善できる唯一のことは、定型文を少し減らすことです。
inline fun <reified I : ListItem, V : ViewBinding> defaultAdapterDelegate( noinline viewBinding: (layoutInflater: LayoutInflater, parent: ViewGroup) -> V, noinline block: AdapterDelegateViewBindingViewHolder<I, V>.() -> Unit ) = adapterDelegateViewBinding<I, ListItem, V>(viewBinding = viewBinding, block = block)
ショーケースの目的で、1 つのテキスト フィールドを含むTitleListItem
の宣言がどのようになるか例を考えてみましょう。
data class TitleListItem( override val id: String, val title: String, ) : DefaultListItem()
そしてそれを代表する
fun titleItemDelegate(onClick: ((String) -> Unit)) = defaultAdapterDelegate< TitleListItem, TitleListItemBinding >(viewBinding = { inflater, root -> TitleListItemBinding.inflate(inflater,root,false) }) { itemView.setOnClickListener { it(item.id) } bind { binding.root.text = item.title } } }
本来、 RecyclerView
の API の都合上、リストへのデータの設定とは別に装飾の設定が行われます。次のような装飾用のロジックがある場合: all items should have offset at the bottom except the last one
、またはall items should have a divider at the bottom, but headers should have an offset at bottom
。
多くの場合、チームには構成可能な「神」デコレーターが付属しています。
class SmartDividerItemDecorator( val context: Context, val skipDividerFor: Set<Int> = emptySet(), val showDividerAfterLastItem: Boolean = false, val showDividerBeforeFirstItem: Boolean = false, val dividerClipToPadding: Boolean = true, val dividerPaddingLeft: Int = 0, val dividerPaddingRight: Int = 0 ) : ItemDecoration()
実装の詳細と、それがいかに脆弱でスケーラブルではないかは想像することしかできません。
一部のRecyclerView
API を模倣し、実装を委任することで、古くから実績のあるアプローチを使用できます。
interface Decoration
今後発表予定の装飾用インターフェースマーカー。
interface HasDecorations { var decorations: List<Decoration> }
このインターフェイスは、この特定の項目が指定された装飾リストで装飾されることを要求していることを宣言するために、データ項目によって実装される必要があります。必要に応じて実行時に変更できるよう、装飾はvar
になっています。
多くの場合、項目には 1 つの装飾が含まれるため、単一の項目をlistOf()
にラップする定型文を減らすために、次のような操作を行うことができます。
interface HasDecoration : HasDecorations { var decoration: Decoration override var decorations: List<Decoration> get() = listOf(decoration) set(value) { decoration = when { value.isEmpty() -> None value.size == 1 -> value.first() else -> throw IllegalArgumentException("Applying few decorations to HasDecoration instance is prohibited. Use HasDecorations") } } }
次の段階は、 ItemDecoration
API を模倣します。
interface DecorationDrawer<T : Decoration> { fun getItemOffsets( outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State, decoration: T ) fun onDraw( c: Canvas, view: View, parent: RecyclerView, state: RecyclerView.State, decoration: T ) }
ご覧のとおり、これは 1 つの違いを除いて、 ItemDecoration
完全に繰り返しています。現在、装飾タイプとインスタンスが認識されていることがわかります。
DecorationDrawer
の可能な実装は 2 番目の記事のトピックですが、ここではインターフェイスと、装飾を表示するためにインターフェイスをどのように処理するかにのみ焦点を当てましょう。
class CompositeItemDecoration( private val context: Context ) : RecyclerView.ItemDecoration() { private val drawers = mutableMapOf<Class<Decoration>, DecorationDrawer<Decoration>>() // [1] private fun applyDecorationToView(parent: RecyclerView, view: View, applyDecoration: (Decoration) -> Unit) { val position = getAdapterPositionForView(parent, view) // [2] when { // [3] position == RecyclerView.NO_POSITION -> return adapter.items[position] is HasDecorations -> { val decoration = (adapter.items[position] as HasDecorations).decorations decoration.forEach(applyDecoration) } else -> return } } override fun getItemOffsets( outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State ) { applyDecorationToView(parent, view) { decoration -> val drawer = getDrawerFor(decoration) drawer.getItemOffsets(outRect, view, parent, state, decoration) } } override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { parent.forEach { child -> applyDecorationToView(parent, child) { decoration -> val drawer = getDrawerFor(decoration) drawer.onDraw(c, child, parent, state, decoration) } } } private fun getDrawerFor(decoration: Decoration): DecorationDrawer<Decoration> { // [4] } private fun getAdapterPositionForView(parent: RecyclerView, view: View): Int { var position = parent.getChildAdapterPosition(view) if (position == RecyclerView.NO_POSITION) { val oldPosition = parent.getChildViewHolder(view).oldPosition if (oldPosition in 0 until (parent.adapter?.itemCount ?: 0)) { position = oldPosition } } return position } }
getAdapterPositionForView()
装飾の適用に固有のものではなく、指定されたビューのアダプターの位置を正しく解決するための単なるメソッドですHasDecorations
インスタンスの装飾を反復処理するこの記事では、 RecyclerView
を中心にリスト フレームワークを構築するアプローチを発見しました。委任は、リスト項目とリスト装飾をラッピングするための基本原則として使用されます。
土着性。実装はサードパーティのライブラリに依存しないため、将来的に柔軟性が低下する可能性があります。 AdaptorDelegate は、まさにこれらの基準を考慮して選択されました。これは非常にシンプルかつ自然で、すべての動作を簡単に実行できます。
スケーラビリティ。委任と関心の分離の原則のおかげで、アイテムのデリゲートまたは装飾ドロワーの実装を分離することができます。
既存のプロジェクトに段階的に統合するのは簡単です
2 番目のパートでは、Android アプリケーションでおそらく見られると思われるすべての種類のリスト装飾について説明します。