paint-brush
Android의 강력한 빈티지 목록: 기초로서의 위임~에 의해@6hundreds
330 판독값
330 판독값

Android의 강력한 빈티지 목록: 기초로서의 위임

~에 의해 Sergey Opivalov9m2023/07/01
Read on Terminal Reader
Read this story w/o Javascript

너무 오래; 읽다

많은 프로젝트가 여전히 View 프레임워크를 사용하고 있으므로 목록을 보유하기 위해 RecyclerView를 사용하고 있습니다. 이 글은 RecylerView에 대한 나의 경험을 종합한 결과입니다. RecylderView를 중심으로 프레임워크를 구축하려는 나의 비전에 대해 이야기하겠습니다.
featured image - Android의 강력한 빈티지 목록: 기초로서의 위임
Sergey Opivalov HackerNoon profile picture
0-item
1-item

요즘에는 Jetpack Compose가 채택되고 있으며 확실히 그럴 가치가 있습니다. 이는 Android UI 개발의 진정한 패러다임 변화입니다. 목록 생성 상용구가 너무 많이 제거되었습니다. 그리고 그것은 아름답습니다.


나는 많은 프로젝트가 여전히 View 프레임워크를 사용하고 있으므로 목록을 작성하기 위해 RecyclerView를 사용하고 있다고 생각합니다.


내가 본 것처럼 여기에는 몇 가지 이유가 있습니다.

  • RecyclerView와 관련된 일부 내부 독점 솔루션은 Compose와 쉽게 일치하지 않으므로 마이그레이션 비용이 높습니다.
  • 개발자는 아직 Compose에 충분히 익숙하지 않습니다.
  • Compose의 성능에 대해서는 (아직) 확신이 없습니다.


하지만 어쨌든 Compose는 미래이기 때문에 제목에 '빈티지'라는 단어를 사용하는 것입니다.


빈티지(Vintage)는 오래되었지만 금(Gold)이라는 뜻입니다. 이 기사는 RecyclerView에 대한 내 경험을 편집한 결과이며, 아직 이 단계에 있는 사람들을 위해 RecyclerView를 중심으로 프레임워크를 구축하려는 내 비전에 대해 설명하겠습니다.

요구사항

프레임워크는 정상적인 수준의 확장성을 제공해야 합니다. 따라서 주요 시나리오는 최소한의 상용구로 처리되어야 하지만 사용자가 사용자 정의 항목을 생성하려는 경우 방해가 되어서는 안 됩니다.


RecyclerView를 사용하는 동안 기본 시나리오 세트를 다음과 같이 정의하겠습니다.

  • 효과적인 데이터 업데이트.
    • RecyclerView 작업 중 가장 좋은 방법은 목록의 데이터를 업데이트하기 위해 DiffUtil API를 사용하는 것입니다. 여기서는 바퀴를 다시 발명하지 않을 것이므로 DiffUtil 활용하는 데 중점을 두고 구현을 위한 행사를 줄이려고 노력할 것입니다.


  • 이기종 목록 정의.
    • 목록에 여러 유형의 뷰가 있는 것은 당연합니다. 모바일 장치의 화면은 높이에 따라 제한되며 장치마다 화면 크기가 다릅니다. 따라서 콘텐츠가 스크롤하지 않고 한 장치에서 화면에 맞더라도 다른 장치에서는 스크롤해야 할 수도 있습니다.


      그렇기 때문에 RecyclerView 상단에 일부 화면을 개발하는 것이 좋은 아이디어일 수 있습니다.



  • 목록 항목 꾸미기
    • 장식은 목록의 매우 일반적인 경우입니다. 장식을 항목 레이아웃에 병합할 수 있지만 이는 간단해 보일 수 있지만 그다지 유연하지는 않습니다. 동일한 항목 유형에 대해 다른 화면에서 다른 장식이 필요할 수 있으며 목록의 항목 위치에 따라 다른 장식이 필요할 수도 있습니다.


기초 옵션

첫 번째 옵션은 모든 것을 처음부터 만드는 것입니다. 구현에 대한 완전한 제어권을 부여하지만 언제든지 그렇게 할 수 있습니다.


더 적은 상용구를 사용하여 RecyclerView를 사용할 수 있는 타사 오픈 소스 프레임워크가 많이 있습니다. 모두 열거하지는 않겠지만, 그 중 두 가지가 완전히 반대되는 접근 방식을 보여주고 싶습니다.


에폭시

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 otherthis 의 신원을 확인합니다.

isContentTheSame otherthis 의 데이터를 확인합니다.


안정적이고 고유한 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 별도로 유지하는 것이 여전히 합리적입니다. 예를 들어 목록의 항목으로 바닥글과 머리글이 있거나 특정 유형의 단일 항목이 있는 경우:


 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)

이기종 목록

AdapterDelegates 라이브러리는 특정 뷰 유형에 대한 대리자를 선언하는 편리한 방법을 제공합니다. 우리가 개선할 수 있는 유일한 것은 상용구를 약간 줄이는 것입니다.

 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)


쇼케이스 목적으로 하나의 텍스트 필드를 포함하는 일부 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 입니다.


항목에는 단일 장식이 있는 경우가 많기 때문에 단일 항목을 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 ) }

보시다시피, 단 하나의 차이점만 제외하고 ItemDecoration 완전히 반복합니다. 이제 장식 유형과 인스턴스가 다가오고 있음을 인식합니다.


DecorationDrawer 에 대한 가능한 구현은 두 번째 기사의 주제입니다. 이제 인터페이스와 장식을 표시하기 위해 인터페이스를 처리하는 방법에만 중점을 두겠습니다.

 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 } }
  1. 생성된 서랍에 대한 캐시
  2. getAdapterPositionForView() 데코레이션 적용에 특정한 것이 아니며, 주어진 뷰에 대한 어댑터 위치를 올바르게 해결하기 위한 방법일 뿐입니다.
  3. HasDecorations 인스턴스의 장식을 통해 반복
  4. 이 작품은 두 번째 기사 “Decorations à la carte”의 주제입니다.

결론

이 기사에서는 RecyclerView 중심으로 목록 프레임워크를 구축하는 접근 방식을 발견했습니다. 목록 항목 래핑 및 목록 장식의 기본 원칙으로 사용되는 위임입니다.


제안된 솔루션의 주요 장점은 다음과 같습니다.

  1. 원주민. 구현은 향후 유연성을 감소시킬 수 있는 타사 라이브러리에 의존하지 않습니다. AdapterDelegates는 이러한 기준을 정확하게 고려하기 위해 선택되었습니다. 이는 매우 간단하고 자연스러우며 모든 동작을 손쉽게 수행할 수 있습니다.

  2. 확장성. 위임 및 관심 분리 원칙 덕분에 항목 위임 또는 장식 서랍의 구현을 분리할 수 있습니다.

  3. 기존 프로젝트에 점진적으로 통합하는 것이 쉽습니다.


두 번째 부분에서는 Android 애플리케이션에서 볼 수 있을 것으로 예상되는 모든 유형의 목록 장식에 대해 논의하겠습니다.