Views, Layouts & RecyclerView
Layouts, ConstraintLayout, ViewBinding, custom views, measure/layout/draw, RecyclerView, DiffUtil
- 01
Your
match_parentTextView inside a ConstraintLayout stretches straight under the button it was supposed to stop at — what width should it have?Easy0dp— in ConstraintLayout that isMATCH_CONSTRAINT, "fill exactly the space my start and end constraints leave";match_parentignores 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" … - 02
Your Fragment keeps its ViewBinding in a
lateinit var bindingfield and LeakCanary flags it — what is wrong, and what does ViewBinding actually generate?EasyA Fragment outlives its view: after
onDestroyViewthe 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 … - 03
A screen laid out entirely in
dplooks perfect until a user sets font size to 200% and the labels get cut in half — what was misunderstood aboutsp?Easydpscales with screen density only, whilespscales with density and the user's font-scale setting — so text sized indpsilently ignores the accessibility setting, and a box sized indparoundsptext 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( … - 04
An
<include>of a two-view header adds a whole extra ViewGroup to the tree — what domerge,ViewStubandtools: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 … - 05
What does a
MeasureSpecactually carry intoonMeasure, and when isrequestLayout()the wrong call andinvalidate()the right one?MediumA
MeasureSpecis a single packedIntholding a mode plus a size — the parent's constraint on that child for one axis — and the child answers it by callingsetMeasuredDimension.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() … - 06
Your custom View draws over the
android:paddingset on it in XML and forgets its value on rotation — what is missing inonDrawand in the state handling?MediumNothing applies padding for you —
onDrawhands you the whole0..widthcanvas and you must confine yourself topaddingLeft..width - paddingRight— and a View saves state only if it has anandroid:idand you overrideonSaveInstanceState.// res/values/attrs.xml // <declare-styleable name="RatingView"> // <attr name="rating" format="integer" /> // <attr name="starColor" format="color" /> // </declare-styleable> … - 07
Who owns what between Adapter, ViewHolder, LayoutManager and RecycledViewPool, and what must
onBindViewHoldernever do?MediumThe Adapter owns the data and turns an item into a bound row, the ViewHolder is a cache of
findViewByIdresults 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>() … - 08
Every refresh calls
notifyDataSetChanged(), the list flickers and jumps back to the top — what replaces it, and what does eachDiffUtilcallback decide?MediumnotifyDataSetChanged()declares everything invalid, so RecyclerView rebinds every visible row, runs no item animations and cannot preserve positional state;ListAdapter(orAsyncListDifferdirectly) 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() … - 09
A recycled row shows the previous item's photo, the wrong checkbox state and a half-finished fade — what causes each one?
MediumAll three are one bug in three costumes: a ViewHolder is a view that gets reused, so its old state survives, and
onBindViewHolderhas 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
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?MediumLet the model carry the type —
getItemViewTypebecomes an exhaustivewhenover a sealed row type — and letConcatAdapterhold 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
A horizontal carousel inside a vertical list keeps stealing the vertical drag — walk me through where that gesture actually gets decided?
MediumA touch walks down the tree through
dispatchTouchEvent, giving every parent a veto inonInterceptTouchEventon the way down and every child a chance to consume inonTouchEventon the way back up — and whoever consumesACTION_DOWNowns 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
The design calls for 12dp gaps, a divider between rows and swipe-to-delete — why does none of that belong in the row layout?
MediumSpacing 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 anItemTouchHelper— 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
The toolbar collapses when you scroll a RecyclerView but not when you scroll a plain ScrollView — what contract makes the first one work?
HardNested scrolling is a cooperation protocol: before a scrolling child moves a single pixel it offers the delta to its parents, and
CoordinatorLayoutuses that first refusal to collapse theAppBarLayout.// 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
A feed drops frames while scrolling on a mid-range phone — how do you decide whether it is inflation, binding, layout or overdraw?
HardYou 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
A panel that grows by animating its
layoutParamsheight stutters, while a card sliding ontranslationXstays smooth — why the difference?HardTranslation, 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
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?
HardAndroid 15 draws every app targeting SDK 35 edge-to-edge whether it asked or not, so the window no longer pads itself: you read
WindowInsetsCompatand 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) …