Bird
Raised Fist0
Android Kotlinmobile~20 mins

Sticky headers in Android Kotlin - Mini App: Build & Ship

Choose your learning style10 modes available
Build: Sticky Headers List
A screen that shows a list of items grouped by category. Each category has a header that stays visible at the top while scrolling through its items.
Target UI
-------------------------
| Fruits               |
|  - Apple             |
|  - Banana            |
|  - Orange            |
|-----------------------
| Vegetables           |
|  - Carrot            |
|  - Broccoli          |
|  - Spinach           |
|-----------------------
| Dairy                |
|  - Milk              |
|  - Cheese            |
|  - Yogurt            |
-------------------------
Display a vertical list grouped by category headers
Headers stick to the top when scrolling through their group
Use RecyclerView with a custom adapter
Headers have a distinct background color and bold text
List items show the item name with normal text
Support smooth scrolling with sticky headers
Starter Code
Android Kotlin
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

class StickyHeadersActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_sticky_headers)

        val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
        recyclerView.layoutManager = LinearLayoutManager(this)

        // TODO: Set adapter with grouped data and implement sticky headers
    }
}
Task 1
Task 2
Task 3
Task 4
Solution
Android Kotlin
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

sealed class ListItem {
    data class Header(val title: String) : ListItem()
    data class Item(val name: String) : ListItem()
}

class StickyHeadersActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_sticky_headers)

        val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
        recyclerView.layoutManager = LinearLayoutManager(this)

        val data = listOf(
            ListItem.Header("Fruits"),
            ListItem.Item("Apple"),
            ListItem.Item("Banana"),
            ListItem.Item("Orange"),
            ListItem.Header("Vegetables"),
            ListItem.Item("Carrot"),
            ListItem.Item("Broccoli"),
            ListItem.Item("Spinach"),
            ListItem.Header("Dairy"),
            ListItem.Item("Milk"),
            ListItem.Item("Cheese"),
            ListItem.Item("Yogurt")
        )

        val adapter = StickyAdapter(data)
        recyclerView.adapter = adapter

        recyclerView.addItemDecoration(StickyHeaderDecoration(adapter))
    }
}

class StickyAdapter(private val items: List<ListItem>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    companion object {
        private const val TYPE_HEADER = 0
        private const val TYPE_ITEM = 1
    }

    override fun getItemViewType(position: Int): Int = when(items[position]) {
        is ListItem.Header -> TYPE_HEADER
        is ListItem.Item -> TYPE_ITEM
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return if (viewType == TYPE_HEADER) {
            val view = LayoutInflater.from(parent.context).inflate(android.R.layout.simple_list_item_1, parent, false)
            HeaderViewHolder(view)
        } else {
            val view = LayoutInflater.from(parent.context).inflate(android.R.layout.simple_list_item_1, parent, false)
            ItemViewHolder(view)
        }
    }

    override fun getItemCount(): Int = items.size

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when(val item = items[position]) {
            is ListItem.Header -> (holder as HeaderViewHolder).bind(item)
            is ListItem.Item -> (holder as ItemViewHolder).bind(item)
        }
    }

    fun isHeader(position: Int): Boolean = items[position] is ListItem.Header

    fun getHeaderTitle(position: Int): String {
        for (i in position downTo 0) {
            if (isHeader(i)) {
                return (items[i] as ListItem.Header).title
            }
        }
        return ""
    }

    class HeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val textView: TextView = itemView.findViewById(android.R.id.text1)
        fun bind(header: ListItem.Header) {
            textView.text = header.title
            textView.setBackgroundColor(0xFFE0E0E0.toInt())
            textView.setTypeface(null, android.graphics.Typeface.BOLD)
        }
    }

    class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val textView: TextView = itemView.findViewById(android.R.id.text1)
        fun bind(item: ListItem.Item) {
            textView.text = item.name
            textView.setBackgroundColor(0xFFFFFFFF.toInt())
            textView.setTypeface(null, android.graphics.Typeface.NORMAL)
        }
    }
}

class StickyHeaderDecoration(private val adapter: StickyAdapter) : RecyclerView.ItemDecoration() {
    private val headerHeight = 100
    private val paint = Paint().apply {
        color = 0xFFE0E0E0.toInt()
    }
    private val textPaint = Paint().apply {
        color = 0xFF000000.toInt()
        textSize = 40f
        isAntiAlias = true
        typeface = android.graphics.Typeface.create(android.graphics.Typeface.DEFAULT, android.graphics.Typeface.BOLD)
    }

    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        val topChild = parent.getChildAt(0) ?: return
        val topChildPosition = parent.getChildAdapterPosition(topChild)
        if (topChildPosition == RecyclerView.NO_POSITION) return

        val headerTitle = adapter.getHeaderTitle(topChildPosition)

        var contactPoint = headerHeight
        for (i in 1 until parent.childCount) {
            val child = parent.getChildAt(i)
            val position = parent.getChildAdapterPosition(child)
            if (position == RecyclerView.NO_POSITION) continue
            if (adapter.isHeader(position)) {
                if (child.top <= contactPoint) {
                    contactPoint = child.top
                }
                break
            }
        }

        c.save()
        c.translate(0f, (contactPoint - headerHeight).toFloat())
        c.drawRect(0f, 0f, parent.width.toFloat(), headerHeight.toFloat(), paint)
        c.drawText(headerTitle, 20f, 70f, textPaint)
        c.restore()
    }

    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
        val position = parent.getChildAdapterPosition(view)
        if (position == RecyclerView.NO_POSITION) return
        if (adapter.isHeader(position)) {
            outRect.top = headerHeight
        } else {
            outRect.top = 0
        }
    }
}

This solution uses a RecyclerView with a custom adapter that supports two types of list items: headers and normal items. The adapter shows headers with bold text and a gray background, and items with normal text and white background.

To create sticky headers, a custom ItemDecoration is added to the RecyclerView. This decoration draws the header at the top of the list and moves it up when the next header approaches, creating the sticky effect.

The data is a simple list mixing header and item objects. The adapter provides helper methods to identify headers and get the current header title for the sticky header drawing.

This approach keeps the UI smooth and visually clear, making it easy for users to see which group they are browsing.

Final Result
Completed Screen
-------------------------
| Fruits               |
|  - Apple             |
|  - Banana            |
|  - Orange            |
|-----------------------
| Vegetables           |
|  - Carrot            |
|  - Broccoli          |
|  - Spinach           |
|-----------------------
| Dairy                |
|  - Milk              |
|  - Cheese            |
|  - Yogurt            |
-------------------------
When user scrolls down, the category header stays pinned at the top until the next header pushes it up.
Headers have a distinct gray background and bold text to separate groups visually.
Items scroll normally under their sticky header.
Stretch Goal
Add a smooth fade animation when the sticky header changes to the next category.
💡 Hint
Use RecyclerView.ItemDecoration with alpha animation on the header drawing or animate the header view in onDrawOver.

Practice

(1/5)
1. What is the main purpose of sticky headers in an Android RecyclerView?
easy
A. To keep section titles visible while scrolling
B. To add animations to list items
C. To load images asynchronously
D. To sort the list alphabetically

Solution

  1. Step 1: Understand sticky header behavior

    Sticky headers keep section titles visible at the top as you scroll through grouped list items.
  2. Step 2: Compare with other options

    Animations, image loading, and sorting are unrelated to sticky headers.
  3. Final Answer:

    To keep section titles visible while scrolling -> Option A
  4. Quick Check:

    Sticky headers = visible section titles [OK]
Hint: Sticky headers keep titles visible on scroll [OK]
Common Mistakes:
  • Confusing sticky headers with animations
  • Thinking sticky headers sort the list
  • Assuming sticky headers load images
2. Which Android component is commonly used to implement sticky headers in Kotlin?
easy
A. RecyclerView with ItemDecoration
B. ListView with Adapter
C. ScrollView with LinearLayout
D. ViewPager with FragmentStateAdapter

Solution

  1. Step 1: Identify components for sticky headers

    RecyclerView supports ItemDecoration which allows custom drawing for sticky headers.
  2. Step 2: Eliminate other options

    ListView and ScrollView do not support sticky headers natively; ViewPager is for paging, not lists.
  3. Final Answer:

    RecyclerView with ItemDecoration -> Option A
  4. Quick Check:

    Sticky headers use RecyclerView + ItemDecoration [OK]
Hint: Sticky headers need RecyclerView + ItemDecoration [OK]
Common Mistakes:
  • Choosing ListView instead of RecyclerView
  • Using ScrollView which lacks sticky header support
  • Confusing ViewPager with list components
3. Given this Kotlin code snippet for a RecyclerView ItemDecoration:
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
  val child = parent.getChildAt(0) ?: return
  val position = parent.getChildAdapterPosition(child)
  val header = getHeaderViewForItem(position)
  c.save()
  c.translate(0f, 0f)
  header.draw(c)
  c.restore()
}
What is the visible effect of this code when scrolling?
medium
A. The header blinks on every scroll event
B. The header stays fixed at the top while list scrolls underneath
C. The header appears at the bottom of the list
D. The header scrolls away with the list items

Solution

  1. Step 1: Analyze onDrawOver behavior

    onDrawOver draws the header on top of the RecyclerView content, fixed at the top (0f, 0f).
  2. Step 2: Understand scrolling effect

    Since header is drawn at fixed position, it stays visible while list scrolls underneath.
  3. Final Answer:

    The header stays fixed at the top while list scrolls underneath -> Option B
  4. Quick Check:

    onDrawOver + translate(0,0) = sticky header visible [OK]
Hint: onDrawOver draws sticky header at fixed top position [OK]
Common Mistakes:
  • Thinking header scrolls away with items
  • Assuming header appears at bottom
  • Believing header blinks on scroll
4. You implemented sticky headers using ItemDecoration but headers flicker or disappear during fast scroll. What is the likely cause?
medium
A. Using LinearLayoutManager instead of GridLayoutManager
B. Setting RecyclerView height to wrap_content
C. Forgetting to call super.onDrawOver() in override
D. Not caching header views causing repeated layout calls

Solution

  1. Step 1: Identify flicker cause in sticky headers

    Repeatedly creating and measuring header views without caching causes flicker during fast scroll.
  2. Step 2: Check other options

    Layout manager type or calling super.onDrawOver() usually don't cause flicker; wrap_content affects layout size but not flicker.
  3. Final Answer:

    Not caching header views causing repeated layout calls -> Option D
  4. Quick Check:

    Missing header caching = flicker [OK]
Hint: Cache header views to avoid flicker [OK]
Common Mistakes:
  • Ignoring header view caching
  • Blaming layout manager type
  • Forgetting super call but unrelated to flicker
5. You want to implement sticky headers that change smoothly when scrolling between sections in a RecyclerView. Which approach best achieves this effect?
hard
A. Use a separate fixed TextView outside RecyclerView for headers
B. Replace header view completely on every scroll event
C. Draw the current header at top and translate it up as next header approaches
D. Disable RecyclerView animations to prevent flicker

Solution

  1. Step 1: Understand smooth sticky header transition

    Drawing current header at top and moving it up as next header scrolls in creates smooth push effect.
  2. Step 2: Evaluate other options

    Replacing header fully causes flicker; separate TextView won't sync scrolling; disabling animations doesn't create smooth transition.
  3. Final Answer:

    Draw the current header at top and translate it up as next header approaches -> Option C
  4. Quick Check:

    Smooth sticky header = translate current header up [OK]
Hint: Translate header up for smooth sticky effect [OK]
Common Mistakes:
  • Replacing header view causing flicker
  • Using fixed TextView outside RecyclerView
  • Disabling animations instead of translating header
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard