androidinterview.com

Android UI Interview Questions

33 questions

Tier
Difficulty
Level

Showing all 33 questions

Views & ViewGroups

What is a View in Android?

Tier: EssentialDifficulty: Easy

A View is the base class for every UI element on screen, a Button, a TextView, an ImageView, anything the user can see or touch is a View or a subclass of it. It's responsible for its own measuring, drawing, and handling of input events.

Every View goes through the same core lifecycle to get on screen, it is measured through onMeasure(), which figures out how much space it needs given the constraints its parent hands it, positioned through onLayout(), which places it at its final coordinates, and painted through onDraw(), which is where its actual pixels get rendered onto a Canvas.

val button = Button(context).apply {
    text = "Submit"
    setOnClickListener { submitForm() }
}

ViewGroup is the other half of this system, a View subclass that acts as an invisible container holding other Views and ViewGroups, LinearLayout and ConstraintLayout are both ViewGroups. A screen is ultimately a tree built out of these two kinds of nodes, ViewGroups arranging their children, and Views at the leaves actually rendering content. In Jetpack Compose there is no View class at all for the UI you write yourself, a composable function describes what to draw and Compose's own renderer paints it directly, though a Compose screen can still host a legacy View through AndroidView when it needs to.

What is the difference between View.GONE and View.INVISIBLE?

Tier: EssentialDifficulty: Easy

View.GONE removes the view from layout entirely, it takes up no space and neighboring views collapse into where it used to be. View.INVISIBLE hides the view but keeps its space reserved, everything around it stays exactly where it was.

errorText.visibility = View.GONE      // layout reflows around it
loadingSpinner.visibility = View.INVISIBLE  // space stays reserved

The practical rule is whether you want the layout to shift. Use GONE when a view is conditionally not part of the screen at all, an optional error message that only sometimes shows, so nothing should reserve space for it when it's absent. Use INVISIBLE when you want the view to stop being seen or interacted with but everything else on screen should stay put, a common example being a placeholder that toggles with a loading spinner in the exact same spot, where you don't want surrounding content jumping around every time it toggles.

Both skip drawing and skip touch handling, the difference is entirely about whether onMeasure() and onLayout() still account for that view's space, GONE is excluded from measurement, INVISIBLE still gets measured and laid out as if it were showing.

What are ViewGroups and how are they different from Views?

Tier: CommonDifficulty: Easy

A ViewGroup is an invisible container that holds and arranges other Views and ViewGroups, View is the base class for a single visible, interactive element like a Button or a TextView.

ViewGroup itself extends View, so it inherits the same drawing and measuring machinery, but its job is different, it does not draw content of its own, it measures and positions its children. LinearLayout, ConstraintLayout, and RecyclerView are all ViewGroups, each with its own strategy for arranging whatever Views you put inside it.

The practical distinction that comes up in interviews is that a View tree alternates between the two, a ViewGroup at the root, holding Views and further ViewGroups as children, all the way down until you hit leaf nodes that are plain Views with nothing inside them.

How do you create a custom view? Describe the process.

Tier: CommonDifficulty: Medium

You subclass View, or a ViewGroup if you're composing other views, and override the methods that control measuring, drawing, and input.

  • Extend View and add a constructor that reads any custom XML attributes you define in res/values/attrs.xml, using a TypedArray obtained through context.obtainStyledAttributes().
  • Override onMeasure() to work out how big the view wants to be given the constraints its parent hands it, and call setMeasuredDimension() with the result. Skipping this and just trusting the default can leave your view sized wrong inside certain parents.
  • Override onDraw(canvas: Canvas) to actually paint the view's content using Canvas and Paint calls, this is where any custom rendering, not standard child views, happens.
  • Override onSizeChanged() if you need to react when the view's dimensions change, and override touch handling through onTouchEvent() if the view needs to respond to gestures.
class RingView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : View(context, attrs) {

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        style = Paint.Style.STROKE
        strokeWidth = 8f
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        val radius = (minOf(width, height) / 2f) - paint.strokeWidth
        canvas.drawCircle(width / 2f, height / 2f, radius, paint)
    }
}

Most of the time you do not need to go this far, composing existing views inside a custom ViewGroup covers the majority of cases people reach for a "custom view" for. A true View subclass with its own onDraw() is for when you need pixel level control that no combination of existing widgets gives you, a chart, a signature pad, a custom progress ring. In Compose the equivalent is a Canvas composable using DrawScope, without any of the measure and layout override boilerplate.

What is the view tree and how can you optimize its depth?

Tier: CommonDifficulty: Medium

The view tree is the object hierarchy a layout inflates into at runtime, a root ViewGroup containing child Views and ViewGroups, nested as deep as your XML says to nest them. Its depth matters because measuring and laying out a screen is a recursive walk over that entire tree, done on the main thread, every frame something in it changes.

  • Prefer ConstraintLayout over stacking multiple LinearLayouts, it can express positioning relative to siblings or the parent in one flat layer that would otherwise take several nested containers.
  • Use <merge> at the root of any layout you plan to <include> elsewhere, so including it does not add an extra, purely structural ViewGroup on top of what you already have.
  • Watch for RelativeLayout specifically inside a list row, it measures its children twice per pass by design, which is expensive when it happens on every row of a RecyclerView.
  • Use ViewStub for views that are only conditionally needed, so they are not part of the tree, and not paying any measure or layout cost, until they're actually inflated.

You can inspect actual depth with Layout Inspector or ViewTreeObserver, which lets you hook into layout and draw events directly. As a rule of thumb, a screen with more than ten or so nested levels is usually flattenable, and doing so pays off directly in fewer recursive measure and layout calls per frame, which is what shows up as smoother scrolling and animation. This entire category of problem is also why Compose's declarative model is a meaningfully different story, there is no XML view tree to measure recursively in the same way, though a deeply nested composable hierarchy can still cost you in recomposition scope.

Read more ViewTreeObserver (opens in a new tab)

Implement the findViewById method.

Tier: CommonDifficulty: HardAsked at: booking-com

findViewById() is a depth first search over the View tree, checking each node's id against the one you're looking for and recursing into children when the node is a ViewGroup.

fun findViewById(root: View, targetId: Int): View? {
    if (root.id == targetId) return root

    if (root is ViewGroup) {
        for (i in 0 until root.childCount) {
            val found = findViewById(root.getChild(i), targetId)
            if (found != null) return found
        }
    }

    return null
}

The real implementation in the framework is more involved than this, plain Views don't expose their id directly the way this sketch assumes, and it walks ViewGroup.getChildAt() rather than a generic children list, but the shape of the algorithm is exactly this, a recursive tree search that stops as soon as it finds a match. Worst case it's O(n) over every view in the hierarchy, which is another reason a deep, heavily nested layout is slower in more places than just measure and layout, findViewById() calls pay for that depth too.

This is also the underlying reason view binding and data binding exist, they generate direct field references at compile time instead of paying for a tree search at runtime, and they catch a missing id as a compile error instead of a null returned at runtime.

Layouts & Optimization

The question under all of these is what a deep view hierarchy costs during measure and layout.

How do you optimize layouts in Android?

Tier: CommonDifficulty: Medium

The biggest win is a flatter view hierarchy, since measure and layout is a recursive pass over the whole tree, fewer nested ViewGroups means less work every single frame.

  • Prefer ConstraintLayout over nested LinearLayouts, it can express positioning that would otherwise take three or four layers of nesting in a single flat layer.
  • Use <merge> at the root of a layout you plan to <include> elsewhere, so inflating it does not add an extra, pointless ViewGroup wrapper on top of what you already have.
  • Avoid RelativeLayout inside lists, it measures children twice per pass by design, which adds up fast when it happens on every row of a RecyclerView.
  • Use ViewStub for views that are only sometimes needed, an error state, an empty state banner, so their layout does not get inflated at all until you actually need it.
  • Check the hierarchy with Layout Inspector, if you count more than ten or so nested levels in a screen that scrolls or animates, that is usually a sign something could be flattened.

The underlying reason all of this matters is that a deep tree multiplies the cost of every measure and layout pass, and those passes run on the main thread. A screen that looks identical with a flat five level hierarchy versus a nested twelve level one can measure and lay out noticeably faster, which shows up directly as smoother scrolling and fewer dropped frames.

How does ConstraintLayout optimization work?

Tier: CommonDifficulty: Medium

ConstraintLayout gets its speed from being able to position every child in a single flat layer, resolved with one pass of a constraint solver, instead of the multiple nested measure passes a stack of LinearLayouts would need to express the same UI.

Under the hood it uses the Cassowary constraint solving algorithm to work out every view's position and size at once, given the constraints you declared, rather than measuring each nested ViewGroup one level at a time the way traditional layouts do. That is the actual optimization, fewer measure and layout passes overall, not that ConstraintLayout itself is magically cheaper per view.

The practical payoff is real UI, a form with a label, a field, and a button positioned relative to each other, that would take three levels of nested LinearLayout and RelativeLayout to express, collapses into one ConstraintLayout with each child constrained directly to its siblings or the parent. Fewer nesting levels means fewer recursive measure and layout calls on the main thread per frame.

Worth knowing for the interview, ConstraintLayout used to include Barrier, Group, and chains specifically to let you avoid nesting for cases that traditionally forced it, a barrier that shifts based on the longer of two labels, for example, is the kind of thing that used to require a nested layout and now does not.

RecyclerView & Lists

More interview time goes here than to the rest of the View system put together.

Explain the role of RecyclerView.Adapter and RecyclerView.ViewHolder.

Tier: EssentialDifficulty: Easy

Adapter and ViewHolder split the two jobs a list needs done, deciding what data goes where, and holding onto the actual view references so the layout does not get inflated over and over.

  • RecyclerView.Adapter owns the data set. It creates view holders in onCreateViewHolder(), one per item type it needs on screen, and fills them with data in onBindViewHolder() whenever a row needs to show a new item.
  • RecyclerView.ViewHolder wraps one row's views and caches the findViewById() lookups for them, so those lookups happen once when the holder is created, not every single time that row gets bound to new data.
class ItemViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    val title: TextView = view.findViewById(R.id.title)
}

class ItemAdapter(private val items: List<Item>) : RecyclerView.Adapter<ItemViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_row, parent, false)
        return ItemViewHolder(view)
    }

    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        holder.title.text = items[position].title
    }

    override fun getItemCount() = items.size
}

The LayoutManager is what actually decides which positions need a view holder at all, but the Adapter and ViewHolder are what make reuse possible, a small number of ViewHolders get created once and then rebound with onBindViewHolder() as the user scrolls, instead of RecyclerView inflating a fresh layout for every row.

How does RecyclerView work internally?

Tier: EssentialDifficulty: Medium

RecyclerView works by reusing a small pool of item views instead of creating a new one for every row in the data set, which is the whole reason it replaced ListView.

Three pieces make this happen.

  • The Adapter knows how to create a view holder and bind data into it.
  • The ViewHolder wraps one item's views and caches the findViewById() lookups so they only happen once per view, not once per bind.
  • The LayoutManager decides where each visible item goes on screen, whether that's a linear list, a grid, or something staggered, and it also decides which items are currently visible and need a view at all.

As you scroll, only enough view holders exist to cover the visible screen plus a small buffer. When a row scrolls off screen, its view holder isn't destroyed. It goes back into a recycle pool, and when a new row is about to scroll into view, the LayoutManager pulls a view holder out of that pool and the adapter calls onBindViewHolder() to stuff the new data into the existing views. So you get one onCreateViewHolder() call per view holder ever created for the visible window, and many onBindViewHolder() calls as it gets reused, which is far cheaper than inflating a new layout on every scroll tick.

That recycling is also why you have to be careful about state inside a row. If a view holder gets reused for a different item, anything you didn't explicitly reset in onBindViewHolder(), like a checkbox's checked state, can bleed over from the previous item unless you set it every single time.

What is DiffUtil and how does it improve RecyclerView performance?

Tier: EssentialDifficulty: Medium

DiffUtil is a utility that compares an old list and a new list, works out the minimal set of insertions, removals, and moves between them, and tells the adapter to animate and rebind only those changes, instead of redrawing the whole RecyclerView.

Before DiffUtil, the common pattern was notifyDataSetChanged(), which throws away all view holder state and rebinds every visible row, whether it actually changed or not. That's wasteful and it kills animations, since RecyclerView has no idea what actually moved.

DiffUtil fixes both problems. You implement a Callback that tells it how to check if two items are the same entity, usually by id, and whether their contents are equal. DiffUtil runs a diffing algorithm over the two lists and produces a list of granular update operations, which you dispatch to the adapter.

class ItemDiffCallback(
    private val old: List<Item>,
    private val new: List<Item>
) : DiffUtil.Callback() {
    override fun getOldListSize() = old.size
    override fun getNewListSize() = new.size
    override fun areItemsTheSame(oldPos: Int, newPos: Int) =
        old[oldPos].id == new[newPos].id
    override fun areContentsTheSame(oldPos: Int, newPos: Int) =
        old[oldPos] == new[newPos]
}

In practice most people reach for ListAdapter, which wraps this whole flow for you, you just call submitList() with the new data and it runs the diff on a background thread and dispatches the updates automatically. The performance win is real, only the rows that actually changed get rebound, and you get correct move and fade animations for free instead of a full list flash.

How does RecyclerView improve performance over ListView?

Tier: CommonDifficulty: Easy

RecyclerView improves on ListView mainly by making view recycling mandatory and structured instead of optional and hand rolled.

  • ListView's view holder pattern, caching findViewById() lookups in a tag object, was a convention you had to opt into yourself, plenty of real code skipped it and paid for repeated lookups on every scroll. RecyclerView bakes the equivalent, its ViewHolder, directly into the API, there is no way to use it without one.
  • RecyclerView separates layout strategy into a pluggable LayoutManager, so a linear list, a grid, or a staggered grid are all just a different LayoutManager on the same RecyclerView, where ListView only ever supported a single vertical list, a grid meant switching to a whole different widget, GridView.
  • RecyclerView supports item animations for inserts, removals, and moves out of the box through ItemAnimator, and pairs naturally with DiffUtil to compute exactly which rows changed, ListView had no equivalent, notifyDataSetChanged() was the only real option and it rebound everything.
  • RecyclerView decouples what to draw from where to draw it more cleanly, the Adapter only worries about data and binding, the LayoutManager only worries about positioning, which made features like nested scrolling and nested RecyclerViews much easier to build correctly than they ever were on top of ListView.

The net effect is the same underlying idea, reuse a small pool of views instead of inflating one per row, but RecyclerView enforces it structurally rather than leaving it as a pattern you might forget to follow.

What are the components of a RecyclerView?

Tier: CommonDifficulty: Easy

Four pieces work together to make a RecyclerView, and each one owns a distinct part of the job.

  • Adapter, owns the data set, creates view holders, and binds data into them through onCreateViewHolder() and onBindViewHolder().
  • ViewHolder, wraps one row's views and caches its findViewById() lookups so they only happen once, not on every bind.
  • LayoutManager, decides where each item sits on screen and which positions are currently visible, LinearLayoutManager for a list, GridLayoutManager for a grid, StaggeredGridLayoutManager for uneven heights.
  • ItemDecoration, draws extra visuals around items without touching the Adapter, dividers between rows or spacing around a grid are the usual example.
  • ItemAnimator, animates the transitions when items are added, removed, or moved, RecyclerView ships a sensible default one but you can supply your own.

RecycledViewPool is the mechanism sitting underneath all of it, it's where view holders that scrolled off screen go, and where the LayoutManager pulls a view holder from when a new item is about to scroll into view, which is the actual recycling that makes RecyclerView cheap compared to inflating a fresh view for every row.

What is a LayoutManager in RecyclerView?

Tier: CommonDifficulty: Easy

A LayoutManager decides where each item in a RecyclerView is positioned on screen, and which positions are currently visible and need a view at all. It is what actually turns a list of data into a linear list, a grid, or a staggered layout, the Adapter has no say in any of that.

recyclerView.layoutManager = LinearLayoutManager(context)
// or
recyclerView.layoutManager = GridLayoutManager(context, spanCount = 2)
// or
recyclerView.layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
  • LinearLayoutManager lays items out in a single row or column, vertical or horizontal.
  • GridLayoutManager arranges items into a fixed number of columns or rows, with an optional SpanSizeLookup if some items should take up more than one span.
  • StaggeredGridLayoutManager handles items of uneven height in a grid, letting shorter columns pull the next item up rather than leaving gaps.

Because the LayoutManager is the piece deciding what is visible, it is also the piece responsible for triggering recycling, as the user scrolls it works out which positions have scrolled off screen and returns those view holders to the pool, then asks for the ones it now needs. Swapping LinearLayoutManager for GridLayoutManager on the same RecyclerView and Adapter changes the whole layout with no changes to how the data is bound.

What is the difference between ListView and RecyclerView?

Tier: CommonDifficulty: Easy

RecyclerView replaced ListView because it makes view recycling mandatory and enforces it through the API, instead of leaving it as an optional pattern developers had to remember to implement themselves.

  • ListView's view holder pattern was a convention, you cached findViewById() lookups in a tag object by choice, plenty of real apps skipped it. RecyclerView's ViewHolder is built into the API, you cannot use it without one.
  • ListView only ever laid items out vertically, a grid meant switching to an entirely different widget, GridView. RecyclerView separates layout into a pluggable LayoutManager, so linear, grid, and staggered grid are all the same widget with a different LayoutManager attached.
  • RecyclerView has built in support for item animations through ItemAnimator, and pairs with DiffUtil to animate exactly what changed. ListView had no real equivalent, notifyDataSetChanged() rebound and redrew everything with no animation.
  • RecyclerView supports ItemDecoration for dividers and spacing without touching the Adapter's data or view logic, ListView mixed that concern into the Adapter itself or a manual divider drawable.

For any new screen, RecyclerView is simply the current answer, ListView is legacy at this point, kept around mainly for old codebases that predate it. In Compose the equivalent is LazyColumn or LazyRow, which give you the same recycling behavior without an Adapter or ViewHolder at all, Compose only creates the composables it actually needs to display.

How do you handle multiple view types in a single RecyclerView?

Tier: CommonDifficulty: Medium

You override getItemViewType() to tell RecyclerView which layout a given position needs, and branch on that same type in onCreateViewHolder() and onBindViewHolder().

private const val TYPE_HEADER = 0
private const val TYPE_ITEM = 1

override fun getItemViewType(position: Int): Int =
    if (items[position] is Header) TYPE_HEADER else TYPE_ITEM

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    val inflater = LayoutInflater.from(parent.context)
    return when (viewType) {
        TYPE_HEADER -> HeaderViewHolder(inflater.inflate(R.layout.item_header, parent, false))
        else -> ItemViewHolder(inflater.inflate(R.layout.item_row, parent, false))
    }
}

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

getItemViewType() runs for every position RecyclerView needs a view for, so it should stay cheap, a simple type check or lookup, not real computation. RecyclerView keeps a separate recycle pool per view type internally, so a header view holder never gets handed back for a row that expects an item layout, each type recycles only within its own kind.

For more than two or three types, a when chain on a sealed class works better than integer constants, it keeps the mapping between data type and view type exhaustive and lets the compiler catch a missing branch.

How do you optimize RecyclerView scrolling performance?

Tier: CommonDifficulty: Medium

Most scrolling jank comes down to doing too much work in onBindViewHolder(), or fighting RecyclerView's recycling instead of working with it.

  • Keep onBindViewHolder() cheap, it runs on the main thread every time a row scrolls into view, so no network calls, no heavy parsing, no expensive formatting inside it.
  • Let an image loading library like Glide or Coil handle image loading, they pool bitmaps and cancel stale requests for a recycled row automatically, which avoids the flicker and memory churn of loading images by hand.
  • Use notifyItemChanged(), notifyItemInserted(), and friends instead of notifyDataSetChanged(), or better, use DiffUtil through ListAdapter, so only the rows that actually changed get rebound.
  • Call setHasFixedSize(true) when the RecyclerView's own size does not change as items are added or removed, it skips a layout pass that would otherwise happen unnecessarily.
  • Keep the row layout flat, avoid nested ViewGroups where a single ConstraintLayout would do, since a deep hierarchy means more measure and layout work per row, multiplied by every row on screen.
  • Tune setItemViewCacheSize() if you're seeing rows rebind constantly during fast scrolling, a larger cache keeps more off screen views around instead of recycling and rebinding them repeatedly.

Together these changes attack the two real costs of scrolling, binding work per row and layout work per row, rather than any single trick doing all of it alone.

Dialogs & Toasts

What is a Dialog in Android?

Tier: CommonDifficulty: Easy

A Dialog is a small floating window that sits on top of the current screen and asks the user to make a decision or enter something before continuing, it does not fill the screen and the Activity underneath stays fully alive and visible while it shows.

AlertDialog is the one you use almost every time, with a title, a message, and up to three buttons.

AlertDialog.Builder(context)
    .setTitle("Delete item")
    .setMessage("This cannot be undone.")
    .setPositiveButton("Delete") { _, _ -> deleteItem() }
    .setNegativeButton("Cancel", null)
    .show()

The recommended way to show one is not calling .show() directly like that, it's wrapping the dialog inside a DialogFragment. A raw Dialog does not survive a configuration change on its own, it just disappears on rotation, a DialogFragment does, because it participates in the FragmentManager's normal save and restore just like any other Fragment.

class ConfirmDeleteDialog : DialogFragment() {
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return AlertDialog.Builder(requireContext())
            .setTitle("Delete item")
            .setPositiveButton("Delete") { _, _ -> /* ... */ }
            .setNegativeButton("Cancel", null)
            .create()
    }
}

ProgressDialog is deprecated, a ProgressBar placed directly in your layout is the current replacement for a loading indicator.

Read more Dialogs (opens in a new tab)

What is a Toast in Android?

Tier: CommonDifficulty: Easy

A Toast is a small, non blocking popup message that shows briefly and dismisses itself, without stealing focus or interrupting whatever the user is doing underneath it.

Toast.makeText(context, "Message sent", Toast.LENGTH_SHORT).show()

LENGTH_SHORT and LENGTH_LONG are the only two duration options, you don't get precise control over timing beyond that. Since API 30, apps targeting Android 12 or higher have their Toasts limited to two lines of text and shown with the app's icon next to the message, so it's meant for a short confirmation, not a place to put anything the user actually needs to read carefully.

A Toast is the wrong tool once the app is in the background, since there is nothing on screen to show it against, and it's also the wrong tool when you need the user to act on it, nothing about a Toast is tappable or actionable. A Snackbar is the better choice when the app is in the foreground and you want an optional action attached, like an undo button, and a system Notification is the right tool once the app is backgrounded and something still needs the user's attention.

Read more Toasts overview (opens in a new tab)

What is the difference between a Dialog and a DialogFragment?

Tier: CommonDifficulty: Medium

A plain Dialog is just a floating window you create and call .show() on directly, it has no lifecycle awareness of its own and does not survive a configuration change, rotate the device while it's showing and it simply disappears. A DialogFragment wraps a Dialog inside a Fragment, so it participates in the FragmentManager's normal state saving and gets recreated automatically after a rotation or process death, showing the same dialog again without you writing any of that recovery logic yourself.

// plain Dialog, gone on rotation
AlertDialog.Builder(context)
    .setMessage("Are you sure?")
    .show()

// DialogFragment, survives rotation
class ConfirmDialog : DialogFragment() {
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return AlertDialog.Builder(requireContext())
            .setMessage("Are you sure?")
            .create()
    }
}
ConfirmDialog().show(supportFragmentManager, "confirm")

DialogFragment also gives you a proper hook into the back button, onCancel() and onDismiss() fire predictably as part of its Fragment lifecycle, and it lets you pass results back cleanly to whichever screen showed it, using a Fragment result listener rather than a raw callback interface you'd have to wire up by hand on a plain Dialog. This is the reason DialogFragment is the documented, recommended way to show a dialog, a plain Dialog is really only reasonable for something extremely transient that has no reason to ever need to survive the Activity being recreated.

Graphics & Media

How do you handle bitmaps in Android given they take too much memory?

Tier: CommonDifficulty: Medium

You decode the image at a smaller size instead of full resolution, and you reuse existing bitmap memory instead of allocating fresh memory for every image.

  • Read the image's dimensions first without loading the pixels, by decoding with inJustDecodeBounds = true. That gives you outWidth and outHeight for free.
  • Work out an inSampleSize, a power of two, that scales the image down to roughly the size you actually need on screen, then decode again with inJustDecodeBounds = false and that sample size set. A 2048 by 1536 image decoded at inSampleSize = 4 becomes 512 by 384, which is a fraction of the memory of the full size version.
  • Reuse bitmap memory through inBitmap where possible, so decoding a new image can write into an already allocated bitmap's memory instead of the system allocating and then garbage collecting a new block every time, which is what actually causes the janky pauses in image heavy screens.
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeResource(resources, R.id.photo, options)

options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
options.inJustDecodeBounds = false
val bitmap = BitmapFactory.decodeResource(resources, R.id.photo, options)

In practice almost nobody hand rolls this. You reach for Glide, Coil, or Fresco, which already do sampling, bitmap pooling, and caching correctly, and just tell them what size you need the image decoded at. Writing this by hand is worth understanding for the interview, but not worth maintaining in a real app.

Read more Load large bitmaps efficiently (opens in a new tab)

Less common, worth knowing

These come up less often. Skim them once you are comfortable with everything above.

Views & ViewGroups

How does layout inflation work, from XML tags to view references in memory?

Tier: Less commonDifficulty: Hard

LayoutInflater parses the XML file into a tree of View objects using reflection, mapping each tag name to a Java class and constructing an instance of it with the attributes read off that tag.

  • The XML is parsed with a pull parser, walking the file tag by tag rather than loading the whole document into a DOM tree at once, which keeps inflation reasonably fast even for larger layouts.
  • For each tag, LayoutInflater resolves a class, a bare tag like TextView maps to android.widget.TextView, a fully qualified tag like com.example.CustomView is used as is. It then constructs that class through reflection, calling a constructor that takes a Context and an AttributeSet.
  • The attributes on that tag, android:layout_width, android:text, and so on, get read from the AttributeSet and applied to the new View instance, either directly or through the View's own constructor logic.
  • Each View gets attached to its parent ViewGroup as it is created, building up the actual object tree in memory that mirrors the nesting in the XML file.

This is also exactly why findViewById() works the way it does afterward, once inflation finishes you have real View objects sitting in memory with an id you can look up, not the XML anymore. It is also why inflation is not free, reflection based construction for every tag is measurably slower than deeply nested XML, another reason a flatter view hierarchy inflates faster in addition to measuring and laying out faster.

Layouts & Optimization

RelativeLayout vs LinearLayout: which and why?

Tier: Less commonDifficulty: Easy

Neither, honestly, reach for ConstraintLayout instead. Both LinearLayout and RelativeLayout are legacy at this point, kept around for simple cases and old codebases rather than being the recommended choice for new UI.

If you're forced to pick between the two specifically, LinearLayout is the better default. It measures each child once, in a single direction, so it is cheap and predictable for a simple stack of views. RelativeLayout positions children relative to each other or the parent, which is more flexible, but it has to measure children twice per pass to resolve those relative positions correctly, that double measure cost adds up fast, especially inside a RecyclerView row where it happens on every bind.

ConstraintLayout replaced the reason people reached for RelativeLayout in the first place, relative positioning without deep nesting, but resolves it in a single pass through its constraint solver instead of RelativeLayout's double measure, and it can flatten what would have taken nested LinearLayouts into one layer besides. In Compose there is no equivalent question at all, Row and Column cover the LinearLayout case directly, and ConstraintLayout has a Compose version for anything more relative than that.

RecyclerView & Lists

How do you update a specific item in a RecyclerView?

Tier: Less commonDifficulty: Easy

You update the backing list at that position and call notifyItemChanged(position), not notifyDataSetChanged(), so RecyclerView only rebinds the one row instead of every visible row.

fun updateItem(position: Int, newItem: Item) {
    items[position] = newItem
    notifyItemChanged(position)
}

If the change also needs a payload, for example you only changed one field and want to skip a full rebind, notifyItemChanged(position, payload) passes that payload into onBindViewHolder()'s overload that receives a payloads list, letting you update just that piece of the row's view instead of rerunning the whole bind.

override fun onBindViewHolder(holder: ItemViewHolder, position: Int, payloads: MutableList<Any>) {
    if (payloads.isNotEmpty()) {
        holder.updateLikeCount(payloads[0] as Int)
        return
    }
    onBindViewHolder(holder, position)
}

If you're managing the list through ListAdapter and DiffUtil instead of touching positions directly, you just submit a new list with submitList(), and DiffUtil works out which single item changed and dispatches the equivalent granular update for you.

What is SnapHelper?

Tier: Less commonDifficulty: Easy

SnapHelper snaps a RecyclerView's scrolling so it settles on a specific child view instead of stopping wherever momentum happens to run out, the same feel as swiping between cards in an app store or a carousel of images.

val snapHelper = LinearSnapHelper()
snapHelper.attachToRecyclerView(recyclerView)

LinearSnapHelper is the built in one people reach for most, it snaps whichever child is closest to the center of the RecyclerView, one fling lands you on the nearest item. PagerSnapHelper behaves closer to a ViewPager, snapping one full item at a time and only ever letting you land on exactly one child per fling, which is the one to use for a page style carousel rather than a smoothly scrolling snap list.

If neither built in behavior fits, you can extend SnapHelper directly and override calculateDistanceToFinalSnap(), which tells it how far to scroll to reach the snap position, and findSnapView(), which tells it which child should be the snap target, a common example being a custom variant that snaps to the start edge of the RecyclerView instead of the center.

What is the purpose of RecyclerView.setHasFixedSize(true)?

Tier: Less commonDifficulty: Easy

setHasFixedSize(true) tells RecyclerView that changes to its adapter's content will not change the RecyclerView's own size, letting it skip a layout pass it would otherwise run defensively on every content change.

recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.setHasFixedSize(true)
recyclerView.adapter = itemAdapter

Without it, every time the adapter's data changes RecyclerView has to assume that change might have altered how big the RecyclerView itself needs to be, for example if its layout_height is wrap_content and adding a row could grow it, so it requests a fresh layout pass from its own parent to check. When the RecyclerView's size genuinely does not depend on its content, its height is a fixed value or match_parent, that check is wasted work happening on every single data update.

This only concerns the size of the RecyclerView itself, not its individual rows, and setting it when your RecyclerView's size actually does depend on its content, wrap_content sized to the number of items, will produce a RecyclerView that does not resize correctly. It's a small optimization on its own, but a real one on screens with frequent list updates, since it removes an entire unnecessary layout pass from the update path.

How do you optimize a nested RecyclerView?

Tier: Less commonDifficulty: Medium

The main fix is sharing a single RecycledViewPool across all the inner RecyclerViews instead of letting each one keep its own.

By default, every RecyclerView keeps a private pool of recycled view holders. In a vertical feed of horizontal RecyclerViews, a common pattern for a home screen with several shelves, each inner RecyclerView has its own pool even though they're often showing the exact same item layout, so view holders never get reused across shelves, they get thrown away and recreated as you scroll down to the next one.

val sharedPool = RecyclerView.RecycledViewPool()

class ShelfViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    val innerRecyclerView: RecyclerView = view.findViewById(R.id.inner_list)

    fun bind() {
        innerRecyclerView.setRecycledViewPool(sharedPool)
    }
}

Beyond the shared pool, the same general RecyclerView advice applies with more force here since you're paying the cost multiple times, keep onBindViewHolder() cheap, set setHasFixedSize(true) on the inner lists if their size does not change, and let an image loading library handle bitmap reuse rather than decoding images yourself. If the inner lists share the exact same LayoutManager configuration, you can also give them a shared RecyclerView.Pool sized larger than the default, since the default pool only holds a handful of view holders per type, which is often too small once several shelves are competing for it.

Text & Styling

What are the best practices for using text in Android?

Tier: Less commonDifficulty: Easy

The main habits to get right are working in scale independent units, pulling strings out into resources, and not building rich text by hand string concatenation.

  • Size text in sp, not dp, sp respects the user's system font size setting, dp does not, so text sized in dp ignores an accessibility setting a real user may depend on.
  • Put every user facing string in strings.xml instead of hardcoding it in a layout or in code, this is what makes localization possible at all, and it also lets you reuse the same string instead of drifting into slightly different copies of it.
  • Use plural resources, <plurals>, for anything with a count attached, "1 item" versus "2 items", string concatenation with a raw number gets grammar wrong in languages with more complex plural rules than English.
  • Reach for a Spannable when you need mixed styling inside one string, part bold, part a different color, rather than splitting it into multiple TextViews to fake it.
  • Let text reflow instead of fixing a view's height around it, hardcoded heights break the moment a string is longer in another language, which is close to guaranteed once you localize.

The thread running through all of this is that text is one of the least predictable inputs into a layout, its length varies by language, its size varies by user setting, so the practices that hold up are the ones that treat text length and size as something you cannot fix at design time.

What is a SpannableString?

Tier: Less commonDifficulty: Easy

A SpannableString has immutable text but mutable span information, you use it when the text itself never needs to change but the styling applied to it does.

val text = SpannableString("Free shipping on orders over $50")
text.setSpan(
    StyleSpan(Typeface.BOLD),
    0, 4,
    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
textView.text = text

Spans are ranges over the text carrying styling or behavior, color, bold, underline, a clickable link, and you can layer several of them over the same string. The choice between SpannableString and SpannableStringBuilder comes down to whether the text itself is fixed, SpannableString wraps a String you already have and only lets you touch the spans, SpannableStringBuilder is what you reach for when the text content also needs to be built up or edited, since it supports both.

What is a Spannable?

Tier: Less commonDifficulty: Medium

A Spannable is a text interface that lets you attach styling, or other metadata, to a specific range of characters inside a string, rather than styling the whole TextView uniformly. Spannable itself is mutable, both the text and the spans applied to it can change after creation, which is what separates it from SpannableString.

val text = SpannableStringBuilder("Save 20% today")
text.setSpan(
    ForegroundColorSpan(Color.RED),
    5, 8,
    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
textView.text = text

Each span is applied over a start and end index with a flag controlling how the span behaves when text is inserted right at its boundary, SPAN_EXCLUSIVE_EXCLUSIVE being the common default that does not extend the span if you type right at either edge. You can stack multiple spans over the same or overlapping ranges, color, bold, a clickable ClickableSpan, an image span, and they all render together.

The distinction worth remembering for an interview is Spannable versus SpannableString, SpannableStringBuilder is the concrete class you actually reach for when both the text and its spans need to change, SpannableString is for when the text itself is fixed and only the styling needs to be mutable.

Graphics & Media

Tell me about the Bitmap pool.

Tier: Less commonDifficulty: Medium

A bitmap pool is a reusable collection of bitmap memory, kept around and handed out again instead of being freed and reallocated every time an image needs decoding.

Loading images is naturally bursty, a scrolling image feed can decode and discard dozens of bitmaps a second, and each of those is a sizable chunk of memory. Allocating and then garbage collecting that memory constantly is what causes visible stutter, not the decoding itself. A bitmap pool breaks that cycle, when a bitmap is no longer needed it goes into the pool instead of being discarded, and the next decode checks the pool first.

The mechanism is BitmapFactory.Options.inBitmap. If the pool has a bitmap whose memory is large enough to hold the new image, that existing memory gets reused directly rather than a fresh allocation happening.

val options = BitmapFactory.Options().apply {
    inMutable = true
    inBitmap = pool.getReusableBitmap(reqWidth, reqHeight)
}
val bitmap = BitmapFactory.decodeResource(resources, R.id.photo, options)

On API 19 and above the reused bitmap only needs to be big enough in byte count, not an exact dimension match, older versions require the dimensions to match exactly or a new bitmap gets allocated instead. Glide and Fresco both build this pattern in, which is the practical reason you almost never write pooling logic by hand, you just use one of those libraries and get the memory win for free.

Read more Manage bitmap memory (opens in a new tab)

What is a Canvas in Android?

Tier: Less commonDifficulty: Medium

A Canvas is the drawing surface a View paints onto, it holds the actual pixel buffer and exposes methods to draw shapes, text, and bitmaps onto it, paired with a Paint object that controls color, stroke width, and style for whatever you draw.

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    val paint = Paint().apply { color = Color.RED }
    canvas.drawCircle(width / 2f, height / 2f, 40f, paint)
    canvas.drawText("Score: 10", 20f, 40f, paint)
}

You get a Canvas handed to you inside onDraw() when you're building a custom View, and every call on it, drawCircle(), drawLine(), drawText(), drawBitmap(), paints directly onto that View's buffer. It also supports transformations, canvas.translate(), canvas.rotate(), canvas.clipRect(), which let you draw relative to a moved or clipped coordinate space without recalculating every coordinate by hand.

A Canvas is what you reach for when built in Views cannot express what you need, a custom chart, a signature pad, a hand drawn progress indicator. In Compose, the equivalent is a Canvas composable exposing a DrawScope, which wraps the same underlying drawing primitives without the onDraw() override and measure and layout boilerplate a View subclass needs.

What is a SurfaceView?

Tier: Less commonDifficulty: Medium

A SurfaceView is a View subclass that hands you a dedicated drawing surface backed by its own buffer, separate from the rest of the window, so you can draw to it from a background thread instead of the main thread.

A regular View always draws through the normal View hierarchy on the main thread, which is fine for typical UI but becomes a bottleneck for continuous, high frequency rendering, video playback, a camera preview, a game loop. SurfaceView gives you a Surface you can hand off to a rendering thread, or to a codec like MediaCodec or Camera2, which writes frames directly without competing with the main thread's layout and draw passes.

class GameSurfaceView(context: Context) : SurfaceView(context), SurfaceHolder.Callback {
    init { holder.addCallback(this) }

    override fun surfaceCreated(holder: SurfaceHolder) {
        thread { renderLoop(holder) }
    }

    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { }
    override fun surfaceDestroyed(holder: SurfaceHolder) { }
}

The tradeoff is that you own the threading and lifecycle of that surface yourself, through SurfaceHolder.Callback, and it composites as a separate layer, which means it cannot be animated, scaled, or have transparency the way a normal View can without extra work. TextureView was Android's answer to that limitation, trading SurfaceView's performance ceiling for normal View behavior like alpha and transform animations, and Compose today mostly reaches for AndroidView wrapping one of these when it needs this kind of surface at all.

Read more SurfaceView (opens in a new tab)