Views, Layouts & RecyclerView
Irbisa · cheatsheetSeptember 13, 2026

Views, Layouts & RecyclerView

Layouts, ConstraintLayout, ViewBinding, custom views, measure/layout/draw, RecyclerView, DiffUtil

Middle Developer16 itemscompressed for a skim
  1. 01

    Your match_parent TextView inside a ConstraintLayout stretches straight under the button it was supposed to stop at — what width should it have?

    Easy

    0dp — in ConstraintLayout that is MATCH_CONSTRAINT, "fill exactly the space my start and end constraints leave"; match_parent ignores the constraints and takes the parent's width instead.

    <!-- WRONG: match_parent ignores the constraints and slides under @id/action -->
    <TextView
        android:id="@+id/title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
    …
  2. 02

    Your Fragment keeps its ViewBinding in a lateinit var binding field and LeakCanary flags it — what is wrong, and what does ViewBinding actually generate?

    Easy

    A Fragment outlives its view: after onDestroyView the Fragment instance can sit on the back stack for a long time, and a binding field keeps the destroyed view tree — every View, every drawable, every bitmap in it — reachable.

    // build.gradle.kts:  android { buildFeatures { viewBinding = true } }
    // res/layout/fragment_profile.xml  ->  FragmentProfileBinding
    
    class ProfileFragment : Fragment(R.layout.fragment_profile) {
    
      // WRONG: this field outlives onDestroyView and pins the whole view tree
    …
  3. 03

    A screen laid out entirely in dp looks perfect until a user sets font size to 200% and the labels get cut in half — what was misunderstood about sp?

    Easy

    dp scales with screen density only, while sp scales with density and the user's font-scale setting — so text sized in dp silently ignores the accessibility setting, and a box sized in dp around sp text clips it.

    // WRONG: hand-rolled pixel math, and a text size in dp
    val px = (16 * resources.displayMetrics.density).toInt()
    title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16f)   // ignores the user's font scale entirely
    
    // RIGHT: let the framework convert, and size text in sp
    val padding = TypedValue.applyDimension(
    …
  4. 04

    An <include> of a two-view header adds a whole extra ViewGroup to the tree — what do merge, ViewStub and tools: attributes each remove from what gets inflated?

    Medium

    <include> inlines another layout file but keeps that file's root ViewGroup; making that root a <merge> attaches its children straight to the including parent, so the extra level disappears entirely.

    <!-- res/layout/view_header.xml — <merge> so these two attach to the INCLUDING parent -->
    <merge xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        tools:parentTag="android.widget.LinearLayout">
    
      <ImageView
    …
  5. 05

    What does a MeasureSpec actually carry into onMeasure, and when is requestLayout() the wrong call and invalidate() the right one?

    Medium

    A MeasureSpec is a single packed Int holding a mode plus a size — the parent's constraint on that child for one axis — and the child answers it by calling setMeasuredDimension.

    class MeterView @JvmOverloads constructor(
      context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
    ) : View(context, attrs, defStyleAttr) {
    
      private val paint = Paint(Paint.ANTI_ALIAS_FLAG)   // allocated once, not in onDraw
      private val bar = RectF()
    …
  6. 06

    Your custom View draws over the android:padding set on it in XML and forgets its value on rotation — what is missing in onDraw and in the state handling?

    Medium

    Nothing applies padding for you — onDraw hands you the whole 0..width canvas and you must confine yourself to paddingLeft..width - paddingRight — and a View saves state only if it has an android:id and you override onSaveInstanceState.

    // res/values/attrs.xml
    // <declare-styleable name="RatingView">
    //   <attr name="rating" format="integer" />
    //   <attr name="starColor" format="color" />
    // </declare-styleable>
    …
  7. 07

    Who owns what between Adapter, ViewHolder, LayoutManager and RecycledViewPool, and what must onBindViewHolder never do?

    Medium

    The Adapter owns the data and turns an item into a bound row, the ViewHolder is a cache of findViewById results for one reusable row, the LayoutManager decides where rows go and when they stop being visible, and the RecycledViewPool holds the detached holders keyed by view type.

    class OrderAdapter(
      private val onClick: (Order) -> Unit
    ) : RecyclerView.Adapter<OrderAdapter.Holder>() {
    
      private val items = mutableListOf<Order>()
    …
  8. 08

    Every refresh calls notifyDataSetChanged(), the list flickers and jumps back to the top — what replaces it, and what does each DiffUtil callback decide?

    Medium

    notifyDataSetChanged() declares everything invalid, so RecyclerView rebinds every visible row, runs no item animations and cannot preserve positional state; ListAdapter (or AsyncListDiffer directly) computes a real diff and emits granular insert, remove, move and change events instead.

    class OrderAdapter : ListAdapter<Order, OrderAdapter.Holder>(DIFF) {
    
      override fun onBindViewHolder(holder: Holder, position: Int) {
        val item = getItem(position)
        holder.binding.title.text = item.title
        holder.binding.unread.text = item.unread.toString()
    …
  9. 09

    A recycled row shows the previous item's photo, the wrong checkbox state and a half-finished fade — what causes each one?

    Medium

    All three are one bug in three costumes: a ViewHolder is a view that gets reused, so its old state survives, and onBindViewHolder has to overwrite everything and cancel everything.

    class RowVH(private val b: ItemRowBinding) : RecyclerView.ViewHolder(b.root) {
    
      // ❌ every stale-row symptom in five lines
      fun bindBad(row: Row, onToggle: (Int) -> Unit) {
        if (row.avatarUrl != null) b.avatar.load(row.avatarUrl)   // else the old photo stays
        b.check.setOnCheckedChangeListener { _, _ -> onToggle(bindingAdapterPosition) }
    …
  10. 10

    A feed mixes headers, ads and three kinds of card, and the adapter is full of position - 1. How would you build that list instead?

    Medium

    Let the model carry the type — getItemViewType becomes an exhaustive when over a sealed row type — and let ConcatAdapter hold the header and the footer as separate adapters, so no offset ever enters the arithmetic.

    sealed interface Row {
      val id: Long
      data class Header(override val id: Long, val title: String) : Row
      data class Article(override val id: Long, val headline: String) : Row
      data class Ad(override val id: Long, val slot: String) : Row
    }
    …
  11. 11

    A horizontal carousel inside a vertical list keeps stealing the vertical drag — walk me through where that gesture actually gets decided?

    Medium

    A touch walks down the tree through dispatchTouchEvent, giving every parent a veto in onInterceptTouchEvent on the way down and every child a chance to consume in onTouchEvent on the way back up — and whoever consumes ACTION_DOWN owns the rest of that gesture.

    class CarouselHost(ctx: Context, attrs: AttributeSet?) : FrameLayout(ctx, attrs) {
      private val slop = ViewConfiguration.get(ctx).scaledTouchSlop
      private var downX = 0f
      private var downY = 0f
    
      // Steal the gesture only once it is clearly horizontal — deciding on DOWN is the bug
    …
  12. 12

    The design calls for 12dp gaps, a divider between rows and swipe-to-delete — why does none of that belong in the row layout?

    Medium

    Spacing and dividers are properties of the list, not of the row, so they belong in an ItemDecoration; the swipe is a gesture over the list, so it belongs in an ItemTouchHelper — and neither one needs the adapter to know about it.

    class SpacingDecoration(private val gap: Int) : RecyclerView.ItemDecoration() {
      override fun getItemOffsets(
        outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State,
      ) {
        val position = parent.getChildAdapterPosition(view)
        if (position == RecyclerView.NO_POSITION) return       // a view being removed
    …
  13. 13

    The toolbar collapses when you scroll a RecyclerView but not when you scroll a plain ScrollView — what contract makes the first one work?

    Hard

    Nested scrolling is a cooperation protocol: before a scrolling child moves a single pixel it offers the delta to its parents, and CoordinatorLayout uses that first refusal to collapse the AppBarLayout.

    // activity_feed.xml
    // <androidx.coordinatorlayout.widget.CoordinatorLayout ...>
    //   <com.google.android.material.appbar.AppBarLayout ...>
    //     <com.google.android.material.appbar.MaterialToolbar
    //         app:layout_scrollFlags="scroll|enterAlways|snap" />
    //   </com.google.android.material.appbar.AppBarLayout>
    …
  14. 14

    A feed drops frames while scrolling on a mid-range phone — how do you decide whether it is inflation, binding, layout or overdraw?

    Hard

    You read a trace instead of guessing: RecyclerView emits its own slices — RV CreateView, RV OnBindView, RV Scroll, RV Prefetch — and whichever one is fat on the janky frames tells you which fix is the right one.

    // 1. Name your own work so it lines up with RecyclerView's own trace slices
    override fun onBindViewHolder(holder: RowVH, position: Int) = trace("bindOrderRow") {
      holder.bind(getItem(position))
    }
    
    // 2. Bind assigns; it never computes
    …
  15. 15

    A panel that grows by animating its layoutParams height stutters, while a card sliding on translationX stays smooth — why the difference?

    Hard

    Translation, scale, rotation and alpha are RenderNode properties the render thread can re-composite without redrawing anything, while a height change invalidates layout and forces a measure and layout pass over that whole branch on every single frame.

    // ❌ every frame calls requestLayout(): measure + layout of the whole branch, on the main thread
    ValueAnimator.ofInt(collapsedHeight, expandedHeight).apply {
      duration = 250
      addUpdateListener { a -> panel.updateLayoutParams { height = a.animatedValue as Int } }
    }.start()
    …
  16. 16

    After raising targetSdk to 35 the toolbar sits under the status bar and the keyboard covers the input — what do you fix, and what stopped working?

    Hard

    Android 15 draws every app targeting SDK 35 edge-to-edge whether it asked or not, so the window no longer pads itself: you read WindowInsetsCompat and apply the insets yourself, to the specific views that need them.

    class ChatActivity : AppCompatActivity() {
      override fun onCreate(savedInstanceState: Bundle?) {
        enableEdgeToEdge()          // required below API 35; already the behaviour at 35+
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
    …