The second part of my vision is to develop a RecyclerView-based framework for building lists in Android applications. While the focuses on unveiling foundation decisions, this part is focused on implementation details. first part À la carte means “according to menu“. So, I’m going to suggest solutions for the most popular list scenarios in mobile applications, and then you can choose. Public API of like a RecyclerView.ItemDecoration fun getItemOffsets(...) fun onDraw(canvas : Canvas, ...) Hints for us: Iems can be decorated by an offset (not necessary in between items), and by drawing something on the canvas of the screen. So in the framework, we’ll present implementations for the most common decorations: . Decoration is based on applying an offset to list items. Gap . Decoration is based on drawing a drawable in between items. Divider . Decoration is based on drawing canvas primitives. Shape At the same time, the framework will be extensible enough to allow users to have their own decoration types with arbitrary implementations. Drawing foundation As a foundation for an efficient framework for drawing decorations, we are introducing a simple implementation. This implementation aims to tackle potential performance concerns while also providing a high-level API for implementors. DecorationDrawer Offset abstract class OffsetDecorationDrawer<T : Decoration> : DecorationDrawer<T> { private val decorationsToOffsets: MutableMap<Decoration, Int> = mutableMapOf() override fun onDraw(c: Canvas, view: View, parent: RecyclerView, state: RecyclerView.State, decoration: T) { //No op } protected fun getOffsetFor(decoration: T, ifEmpty: () -> Int): Int = decorationsToOffsets.getOrPut(decoration, ifEmpty) } This offset-based drawer has a caching map of decorations to offsets. This is especially useful when offsets are declared as a , so we are not extracting a dimen size from over and over. DimenRes Resources Drawable abstract class DrawableDecorationDrawer<T : Decoration> : DecorationDrawer<T> { private val decorationsToDrawables: MutableMap<Decoration, Drawable> = mutableMapOf() @CallSuper override fun onDraw(c: Canvas, view: View, parent: RecyclerView, state: RecyclerView.State, decoration: T) { val drawable = getDrawableFor(decoration) setDrawableBounds(view, drawable) drawable.draw(c) } abstract fun createDrawable(decoration: T): Drawable abstract fun setDrawableBounds(view: View, drawable: Drawable) private fun getDrawableFor(decoration: T): Drawable = decorationsToDrawables.getOrPut(decoration) { createDrawable(decoration) } } This Drawable-based drawer has a similar caching mapper as well, and exposes some related API. Drawable Note, that these drawers are abstract enough, not declaring any constraints for . It allows users to use goodies of these drawers in their own implementations for custom decorations. T Gap First, I'm declaring data, representing Gap decoration: abstract class GapDecoration : Decoration { @get:DimenRes abstract val gap: Int } Each gap decoration keeps the size of the gap. Now let’s have something more specific: data class TopGap(override val gap: Int) : GapDecoration() { companion object { val gridHalf = TopGap(R.dimen.grid_0_5) val grid1 = TopGap(R.dimen.grid_1) val grid2 = TopGap(R.dimen.grid_2) } } Data class is presenting top gap decoration in a list. It’s handy to have some set of the most common sizes. The rest directions of gaps are pretty straightforward: data class BottomGap(override val gap: Int) : GapDecoration() { companion object { // defaults } } data class LeftGap(override val gap: Int) : GapDecoration() { companion object { // defaults } } data class RightGap(override val gap: Int) : GapDecoration() { companion object { // defaults } } Drawing class BottomGapDrawer(private val context: Context) : OffsetDecorationDrawer<BottomGap>() { override fun getItemOffsets( outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State, decoration: BottomGap ) { outRect.bottom += getOffsetFor(decoration) { context.resources.getDimensionPixelSize(decoration.gap) } } } Just adding an offset to the appropriate side of . Implementations for , and are done in the same manner. outRect TopGap LeftGap RightGap Utility Now let’s pay closer attention to end users. Whereas putting a vertical gap in between items is simple, gapping of the grid items(RecyclerView with ) is verbose and error-prone. GridLayoutManager So we could develop some utility functions for this: fun <T : HasDecorations> List<T>.applyHorizontalGridGap( spanCount: Int, leftGap: LeftGap, rightGap: RightGap ): List<T> = mapIndexed { index, item -> item.decorations += when (index % spanCount) { 0 -> listOf(rightGap) spanCount.dec() -> listOf(leftGap) else -> listOf(leftGap, rightGap) } item } Setting horizontal gaps to grid items according to logic: the first item in a row shouldn’t have a left gap, and the last item in a row shouldn’t have a right gap. fun <T : HasDecorations> List<T>.applyVerticalGridGap(spanCount: Int, topGap: TopGap = TopGap.grid1): List<T> = mapIndexed { index, item -> if (index >= spanCount) item.decorations += topGap item } The first row shouldn’t have a top gap. The rest of the rows should. And a composite function that allows one to apply a gap to the grid in an elegant way: fun <T : HasDecorations> List<T>.applyGridGap(spanCount: Int, @DimenRes gapSize: Int) { applyHorizontalGridGap(spanCount, LeftGap(gapSize), RightGap(gapSize)) applyVerticalGridGap(spanCount, TopGap(gapSize)) } Eventually, let’s add these drawers to : CompositeItemDecoration # CompositeItemDecoration.kt private val drawers = mutableMapOf<Class<Decoration>, DecorationDrawer<Decoration>>() // unrelated content is omitted private fun getDrawerFor(decoration: Decoration): DecorationDrawer<Decoration> = drawers.getOrPut(decoration.javaClass) { val drawer = when (decoration) { is BottomGap -> BottomGapDrawer(context) is TopGap -> TopGapDrawer(context) is LeftGap -> LeftGapDrawer(context) is RightGap -> RightGapDrawer(context) else -> NoneDecorationDrawer } drawer as DecorationDrawer<Decoration> } Note the as well: NoneDecorationDrawer object NoneDecorationDrawer : DecorationDrawer<None> { override fun getItemOffsets( outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State, decoration: None ) { // no op } override fun onDraw( c: Canvas, view: View, parent: RecyclerView, state: RecyclerView.State, decoration: None ) { // no op } } Divider abstract class DividerDecoration : Decoration { @get:ColorRes abstract val color : Int } All s will use the same drawable as a divider, but it’s possible to have a different color for drawable tinting. DividerDecoration Now let’s have decorations that draw a divider at the top and at the bottom of the item: data class BottomDivider(override val color: Int) : DividerDecoration() { companion object { val default = BottomDivider(R.color.dividerDefaultColor) } } data class TopDivider(override val color: Int) : DividerDecoration() { companion object { val default = TopDivider(R.color.dividerDefaultColor) } } Drawing abstract class DividerDecorationDrawer<T : DividerDecoration>(private val context: Context) : DrawableDecorationDrawer<T>() { protected val divider = ContextCompat.getDrawable(context, R.drawable.list_divider)!! override fun createDrawable(decoration: T): Drawable = divider.constantState?.newDrawable()?.apply { setTint(ContextCompat.getColor(context, decoration.color)) } ?: divider } Common drawer for divider decorations. It’s declaring a drawable for dividers, and implementing the method in a way that allows tint to be applied to the drawable correctly. createDrawable : Implementation for BottomDivider class BottomDividerDrawer(private val context: Context) : DividerDecorationDrawer<BottomDivider>(context) { override fun getItemOffsets( outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State, decoration: BottomDivider ) { outRect.bottom += divider.intrinsicHeight } override fun setDrawableBounds(view: View, drawable: Drawable) { drawable.setBounds( view.left, view.bottom, view.right, view.bottom + drawable.intrinsicHeight ) } } We need to offset an item for the height of the drawable because we want not to overlap an item with a divider, but place said divider in between items. And then set the correct bounds for the drawable. Implementation for is similar. TopDivider Finally, add these drawers to : CompositeItemDecoration # CompositeItemDecoration.kt private val drawers = mutableMapOf<Class<Decoration>, DecorationDrawer<Decoration>>() // unrelated content is omitted private fun getDrawerFor(decoration: Decoration): DecorationDrawer<Decoration> = drawers.getOrPut(decoration.javaClass) { val drawer = when (decoration) { is BottomGap -> BottomGapDrawer(context) is TopGap -> TopGapDrawer(context) is LeftGap -> LeftGapDrawer(context) is RightGap -> RightGapDrawer(context) is TopDivider -> TopDividerDrawer(context) is BottomDivider -> BottomDividerDrawer(context) else -> NoneDecorationDrawer } drawer as DecorationDrawer<Decoration> } Transformations Whereas uses r for animating changes in a list, drawable-based decorations are not animated accordingly by default. RecyclerView DefaultItemAnimato Let’s see: To address this, we should synchronize the state of item’s with state of a decoration: View interface ViewAwareDrawableTransformer { fun transform(drawable: Drawable, view: View) } This intends to transform the given drawable according to view properties. Let’s see what most useful transformers could look like: class AlphaTransformer : ViewAwareDrawableTransformer { override fun transform(drawable: Drawable, view: View) { drawable.alpha = (view.alpha * 255).toInt() } } or class TranslationYTransformer : ViewAwareDrawableTransformer { override fun transform(drawable: Drawable, view: View) { drawable.apply { updateBounds( top = bounds.top + view.translationY.toInt(), bottom = bounds.bottom + view.translationY.toInt(), ) } } } It’s worth remembering that since method going to be invoked in , we should avoid doing any additional allocations there. transform onDraw Now we can use the composition of transformers in : DrawableDecorationDrawer abstract class DrawableDecorationDrawer<T : Decoration> : DecorationDrawer<T> { open val transformers: List<ViewAwareDrawableTransformer> = emptyList() // unrelated content is omitted @CallSuper override fun onDraw(c: Canvas, view: View, parent: RecyclerView, state: RecyclerView.State, decoration: T) { val drawable = getDrawableFor(decoration) setDrawableBounds(view, drawable) transformers.forEach { it.transform(drawable, view) } drawable.draw(c) } // unrelated content is omitted } We are allowing inheritors to overload transformations that apply to the drawable before drawing. For instance will declare it like this: DividerDecorationDrawer abstract class DividerDecorationDrawer<T : Decoration>(context: Context) : DrawableDecorationDrawer<T>() { // unrelated content is omitted override val transformers: List<ViewAwareDrawableTransformer> = listOf(AlphaTransformer(), TranslationYTransformer()) } Now, the result of applying such transformations to drawable-based decorations makes the updating of list data much smoother. Shape It’s worth demonstrating, that we can have drawable-based decorations be a bit more complex, in that simple divider line. Under the I’’ll mean decoration around list items like this: Shape sealed class RoundedShape : Decoration { @get:ColorRes abstract val color: Int @get:DimenRes abstract val radius: Int abstract val style: Style enum class Style { STROKE, FILL } data class Top( @ColorRes override val color: Int, @DimenRes override val radius: Int, override val style: Style ) : RoundedShape() { companion object { val default = Top(defaultColor, defaultRadius, Style.STROKE) } } data class Bottom( @ColorRes override val color: Int, @DimenRes override val radius: Int, override val style: Style ) : RoundedShape() { companion object { val default = Bottom(defaultColor, defaultRadius, Style.STROKE) } } data class None( override val color: Int, override val style: Style ) : RoundedShape() { override val radius: Int = 0 companion object { val default = None(defaultColor, Style.STROKE) } } data class All( @ColorRes override val color: Int, @DimenRes override val radius: Int, override val style: Style ) : RoundedShape() { companion object { val default = All(defaultColor, defaultRadius, Style.STROKE) } } companion object { @DimenRes val defaultRadius = R.dimen.grid_2 @ColorRes val defaultColor = R.color.dividerDefaultColor const val width = 1 } } : and are using for the very first and very last items, is used in the middle, is useful when a list contains only one item. Also, we have two styles of rounded shape: and . Declaring a few types of decorations Top Bottom None All Stroke Fill As before, let’s pay some a little bit of attention to the end users and provide a handy way to apply a rounded shape to the list: fun <T : HasDecorations> List<T>.applyRoundedShape( @ColorRes color: Int = RoundedShape.defaultColor, @DimenRes radius: Int = RoundedShape.defaultRadius, style: RoundedShape.Style = RoundedShape.Style.STROKE ): List<T> = mapIndexed { index, item -> item.decorations = when { index == 0 && size > 1 -> item.decorations + listOf(RoundedShape.Top(color, radius, style)) index == size - 1 && size > 1 -> item.decorations + listOf(RoundedShape.Bottom(color, radius, style)) size == 1 -> item.decorations + listOf(RoundedShape.All(color, radius, style)) else -> item.decorations + listOf(RoundedShape.None(color, style)) } item } Drawing Before drawing, I want to look a bit ahead and remember, that list items are very likely to be clickable. So having rounded shape decoration around an item forces us to properly outline the reveal effect of the click: abstract class RoundedShapeDrawer<T : RoundedShape>(private val context: Context) : DrawableDecorationDrawer<T>() { // 1 private val frameWidth = RoundedShape.width.dipToPixels(context.resources).toFloat() protected val frameHalfWidth = (frameWidth / 2).roundToInt() // 2 private val decorationsToOutlineProviders: MutableMap<Decoration, ViewOutlineProvider> = mutableMapOf() // 3 override val transformers: List<ViewAwareDrawableTransformer> = listOf( AlphaTransformer(), TranslationYTransformer() ) override fun getItemOffsets( outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State, decoration: T ) { // No op } // 4 override fun onDraw(c: Canvas, view: View, parent: RecyclerView, state: RecyclerView.State, decoration: T) { super.onDraw(c, view, parent, state, decoration) val outlineProvider = getOutlineProviderFor(decoration) { createOutlineProvider(decoration) } if (view.outlineProvider == outlineProvider) return view.outlineProvider = outlineProvider if (view.clipToOutline) return view.clipToOutline = true } abstract fun createOutlineProvider(decoration: T): ViewOutlineProvider // 5 protected fun MaterialShapeDrawable.setupShape(decoration: T): Drawable = this .apply { val color = decoration.color val style = decoration.style fillColor = if (style == RoundedShape.Style.STROKE) ColorStateList.valueOf(Color.TRANSPARENT) else ColorStateList.valueOf(color) strokeColor = if (style == RoundedShape.Style.STROKE) ColorStateList.valueOf(color) else ColorStateList.valueOf(Color.TRANSPARENT) strokeWidth = if (style == RoundedShape.Style.STROKE) frameWidth else 0f } private fun getOutlineProviderFor(decoration: T, ifEmpty: () -> ViewOutlineProvider): ViewOutlineProvider = decorationsToOutlineProviders.getOrPut(decoration, ifEmpty) This is a bit massive, so here is a step-by-step explanation: and is intended to be used heavily during a setting of drawable bound, so we calculate it once and keep it for further use. frameWidth frameWidthHalf As I mentioned at the beginning of the section – we need to properly outline of list item to support the reveal effect of the click. The approach for working with is the same as we're having for ‘s – we are caching it, and avoiding any extra work. View ViewOutlineProvider Drawable We want to support transformations according to changes. View In added a logic for setting . onDraw ViewOutlineProvider is intended to for use by inheritors to set up the final shape according to that came. setupShape RoundedShape Let’s see an implementations: RoundedShapeDrawer class AllRoundedShapeDrawer(private val context: Context) : RoundedShapeDrawer<RoundedShape.All>(context) { override fun createOutlineProvider(decoration: RoundedShape.All): ViewOutlineProvider = object : ViewOutlineProvider() { val radius = context.resources.getDimension(decoration.radius) override fun getOutline(view: View, outline: Outline) { outline.setRoundRect( 0, 0, view.width, view.height, radius ) } } override fun createDrawable(decoration: RoundedShape.All): Drawable { val radius = context.resources.getDimension(decoration.radius) return MaterialShapeDrawable( ShapeAppearanceModel() .toBuilder() .setAllCornerSizes(radius) .build() ).setupShape(decoration) } override fun setDrawableBounds(view: View, drawable: Drawable) { drawable.setBounds( view.left, view.top, view.right, view.bottom ) } } Simplest one. No extra logic, just creating and According to bound of . ViewOutlineProvider MaterialShapeDrawable View class NoneRoundedShapeDrawer(context: Context) : RoundedShapeDrawer<RoundedShape.None>(context) { override fun createDrawable(decoration: RoundedShape.None): Drawable = MaterialShapeDrawable(ShapeAppearanceModel()).setupShape(decoration) override fun setDrawableBounds(view: View, drawable: Drawable) { drawable.setBounds( view.left, view.top - frameHalfWidth, // Overlapping neighbour frames to keep divider width not doubled view.right, view.bottom + frameHalfWidth // Overlapping neighbour frames to keep divider width not doubled ) } override fun createOutlineProvider(decoration: RoundedShape.None): ViewOutlineProvider = object : ViewOutlineProvider() { override fun getOutline(view: View, outline: Outline) { outline.setRect( 0, 0, view.width, view.height, ) } } } Here is is addressing an issue, when two neighbour items are decorated by : setDrawableBounds RoundedShape.None class TopRoundedShapeDrawer(private val context: Context) : RoundedShapeDrawer<RoundedShape.Top>(context) { override fun createDrawable(decoration: RoundedShape.Top): Drawable { val radius = context.resources.getDimension(decoration.radius) return MaterialShapeDrawable( ShapeAppearanceModel() .toBuilder() .setTopLeftCornerSize(radius) .setTopRightCornerSize(radius) .build() ).setupShape(decoration) } override fun setDrawableBounds(view: View, drawable: Drawable) { drawable.setBounds( view.left, view.top, view.right, view.bottom + frameHalfWidth // Overlapping neighbor frames to keep divider width not doubled ) } override fun createOutlineProvider(decoration: RoundedShape.Top): ViewOutlineProvider = object : ViewOutlineProvider() { val radius = context.resources.getDimension(decoration.radius) override fun getOutline(view: View, outline: Outline) { outline.setRoundRect( 0, 0, view.width, // Only rect/round rect/oval is fully supported by rendering pipeline. // We need rounded rect with different corners. This is only workaround for having it. view.height + radius.roundToInt(), radius ) } } } Here is is performing a trick to have an outline provider in a rect with only two rounded corners. Here is on such . createOutlineProvider issue is omitted because of its similarity with . BottomRoundedShapeDrawer TopRoundedShapeDrawer Finally, add drawers to : RoundedShape CompositeItemDecoration # CompositeItemDecoration.kt private val drawers = mutableMapOf<Class<Decoration>, DecorationDrawer<Decoration>>() // unrelated content is omitted private fun getDrawerFor(decoration: Decoration): DecorationDrawer<Decoration> = drawers.getOrPut(decoration.javaClass) { val drawer = when (decoration) { is BottomGap -> BottomGapDrawer(context) is TopGap -> TopGapDrawer(context) is LeftGap -> LeftGapDrawer(context) is RightGap -> RightGapDrawer(context) is TopDivider -> TopDividerDrawer(context) is BottomDivider -> BottomDividerDrawer(context) is RoundedShape.Top -> TopRoundedShapeDrawer(context) is RoundedShape.Bottom -> BottomRoundedShapeDrawer(context) is RoundedShape.None -> NoneRoundedShapeDrawer(context) is RoundedShape.All -> AllRoundedShapeDrawer(context) else -> NoneDecorationDrawer } drawer as DecorationDrawer<Decoration> } Decorations composition So as soon we have all default decorations in our framework, it’s time to remember about basis – interface. It declares multiple decorations for each item. I think it’s useful for scenarios when you need to combine some divider and offset after: HasDecorations However, it has limited support in the current framework implementation. For example, it doesn’t support ordering or applying decorations, and it does not considering “shift” of canvas after previously applied offset-based decoration. So multiple decorations should be used carefully and without any extra expectations, it works only for primitive scenarios. Conclusion The final product of this article series is a framework around that focused on scalability and high standards for UI. It suggests to users a list of out-of-box decorations, and this list is not limited and can easily be extended according to your requirements. Here are a number of highlights that I’d like to mention: RecyclerView Decorations are data now Originally, supposes that decorations of list items is intended to be aside to list items data. This leads to mind “splitting“ for developers and getting in to have some generic solution for decorations in the project. Also, this approach is making your decorations kind of static – it’s very hard to control where decorations should be applied with updated list data. RecyclerView Our framework is merging decorations and item data. Take a look: private var items = listOf<ListItem>( TitleListItem( id = "1", decorations = listOf(BottomGap.grid1), title = "Title1" ), TitleListItem( id = "2", decorations = listOf(BottomGap.grid1), title = "Title2" ), TitleListItem( id = "3", decorations = listOf(BottomDivider.default, BottomGap.grid1), title = "Title3" ) ) Now it’s declarative, you directly see which items will be decorated and how. This gives much more control to developers. What is more important – now it’s easy to unit test the application of decorations. It is just assertions on the content of list data. Decorations delegation This framework is heavily based on the delegation principle. It makes it very flexible. In this article, I’ve described different and most popular use cases for decorations and suggested an abstract foundation like a and for your future implementations. But again – if you need something very custom, it’s easy to integrate it into a framework. DrawableDecorationDrawer OffsetsDecorationDrawer Forget about fragile “god“ implementations. Now, the application decorations are a split extendable list of individual isolated pieces. RecyclerView.ItemDecorators UI quality Despite the flexibility of the proposed approach, the framework does not forget to care about the quality of the final UI. It’s supporting – aware transformations after list data updates, and these transformations are easy to reuse. It’s even giving out-of-box care about clipping reveal effect on item click to achieve high-quality UI standards. View