Câu hỏi phỏng vấn Android UI - Views
Phần này tổng hợp các câu hỏi về Android UI Views - hệ thống View truyền thống của Android.
1. View Fundamentals
Q: View và ViewGroup khác nhau thế nào?
Trả lời:
| Aspect | View | ViewGroup |
|---|---|---|
| Definition | Single UI element | Container for Views |
| Can contain children | ❌ | ✅ |
| Examples | TextView, Button, ImageView | LinearLayout, ConstraintLayout |
| Hierarchy | Leaf node | Branch node |
// View - single element
val textView = TextView(context).apply {
text = "Hello, World!"
textSize = 16f
}
// ViewGroup - container
val linearLayout = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
addView(TextView(context).apply { text = "Child 1" })
addView(TextView(context).apply { text = "Child 2" })
}💡 Mẹo:
View.javacó hơn 34,000 dòng code! Tránh tạo View không cần thiết để optimize performance.
📚 Tìm hiểu thêm: AndroidView trong Compose
Q: View Lifecycle gồm những giai đoạn nào?
Trả lời:
| Phase | Method | Purpose |
|---|---|---|
| Creation | onFinishInflate() | After XML inflation |
| Attachment | onAttachedToWindow() | View added to window |
| Measurement | onMeasure() | Determine size |
| Layout | onLayout() | Position children |
| Drawing | onDraw() | Render on canvas |
| Interaction | onTouchEvent() | Handle user input |
| Detachment | onDetachedFromWindow() | Cleanup |
class CustomView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
override fun onAttachedToWindow() {
super.onAttachedToWindow()
// Start animations, register listeners
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// Calculate desired size
val width = calculateWidth(widthMeasureSpec)
val height = calculateHeight(heightMeasureSpec)
setMeasuredDimension(width, height)
}
override fun onDraw(canvas: Canvas) {
// Draw content
canvas.drawRect(...)
canvas.drawText(...)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
// Cleanup resources
}
}2. RecyclerView
Q: RecyclerView hoạt động như thế nào?
Trả lời:
RecyclerView tái sử dụng ViewHolders để hiển thị large datasets hiệu quả.
Components:
| Component | Responsibility |
|---|---|
| Adapter | Create ViewHolders, bind data |
| ViewHolder | Hold view references |
| LayoutManager | Position items |
| ItemDecoration | Dividers, spacing |
| ItemAnimator | Add/remove animations |
class UserAdapter : RecyclerView.Adapter<UserAdapter.ViewHolder>() {
private var items = listOf<User>()
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val binding = ItemUserBinding.bind(view)
fun bind(user: User) {
binding.nameText.text = user.name
binding.avatar.load(user.avatarUrl)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = ItemUserBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return ViewHolder(view.root)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.size
fun submitList(newItems: List<User>) {
items = newItems
notifyDataSetChanged() // Use DiffUtil for better performance
}
}Q: DiffUtil là gì? Tại sao nên dùng?
Trả lời:
DiffUtil tính toán minimal updates cần thiết để biến đổi list cũ thành list mới.
// ❌ Without DiffUtil - redraws everything
adapter.submitList(newList)
adapter.notifyDataSetChanged()
// ✅ With DiffUtil - only updates changed items
class UserDiffCallback : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem == newItem
}
}
class UserAdapter : ListAdapter<User, UserAdapter.ViewHolder>(UserDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
// ...
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position))
}
}
// Usage
adapter.submitList(newList) // DiffUtil handles updates automaticallyBenefits:
- ✅ Smooth animations
- ✅ Better performance
- ✅ Preserves scroll position
Q: RecyclerView optimizations?
Trả lời:
// 1. Use setHasFixedSize for fixed-size items
recyclerView.setHasFixedSize(true)
// 2. Set item view cache size
recyclerView.setItemViewCacheSize(20)
// 3. Use RecycledViewPool for multiple RecyclerViews
val sharedPool = RecyclerView.RecycledViewPool()
recyclerView1.setRecycledViewPool(sharedPool)
recyclerView2.setRecycledViewPool(sharedPool)
// 4. Avoid nested RecyclerViews (use ConcatAdapter)
val concatAdapter = ConcatAdapter(headerAdapter, contentAdapter, footerAdapter)
recyclerView.adapter = concatAdapter
// 5. Use stable IDs
class MyAdapter : RecyclerView.Adapter<ViewHolder>() {
init {
setHasStableIds(true)
}
override fun getItemId(position: Int): Long = items[position].id
}
// 6. Prefetch for nested scrolling
(recyclerView.layoutManager as LinearLayoutManager)
.initialPrefetchItemCount = 43. Custom Views
Q: Làm sao tạo Custom View?
Trả lời:
3 cách tạo Custom View:
1. Compound View (combine existing views)
class UserInfoView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : LinearLayout(context, attrs) {
private val binding: ViewUserInfoBinding
init {
orientation = HORIZONTAL
binding = ViewUserInfoBinding.inflate(
LayoutInflater.from(context), this
)
// Read custom attributes
context.obtainStyledAttributes(attrs, R.styleable.UserInfoView).use {
binding.nameText.text = it.getString(R.styleable.UserInfoView_userName)
}
}
fun setUser(user: User) {
binding.nameText.text = user.name
binding.avatar.load(user.avatarUrl)
}
}2. Custom Drawing (draw on canvas)
class CircleProgressView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
var progress: Float = 0f
set(value) {
field = value.coerceIn(0f, 1f)
invalidate() // Request redraw
}
override fun onDraw(canvas: Canvas) {
val centerX = width / 2f
val centerY = height / 2f
val radius = minOf(centerX, centerY) - paint.strokeWidth
// Draw background circle
paint.color = Color.LTGRAY
canvas.drawCircle(centerX, centerY, radius, paint)
// Draw progress arc
paint.color = Color.BLUE
canvas.drawArc(
centerX - radius, centerY - radius,
centerX + radius, centerY + radius,
-90f, 360f * progress, false, paint
)
}
}3. Extend existing View
class SquareImageView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : AppCompatImageView(context, attrs) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
// Make it square
val size = minOf(measuredWidth, measuredHeight)
setMeasuredDimension(size, size)
}
}4. Touch Handling
Q: Touch event handling trong Android?
Trả lời:
Touch events được dispatch từ parent → children (tunnel down), xử lý từ children → parent (bubble up).
| Method | Purpose | Return |
|---|---|---|
dispatchTouchEvent() | Dispatch to children | true if handled |
onInterceptTouchEvent() | Parent intercepts | true to steal |
onTouchEvent() | Handle touch | true if consumed |
class CustomViewGroup : FrameLayout {
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
// Return true to steal touch from children
return when (ev.action) {
MotionEvent.ACTION_MOVE -> {
val dx = abs(ev.x - startX)
dx > touchSlop // Intercept if horizontal swipe
}
else -> false
}
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
startX = event.x
return true
}
MotionEvent.ACTION_MOVE -> {
// Handle drag
return true
}
MotionEvent.ACTION_UP -> {
// Handle release
return true
}
}
return super.onTouchEvent(event)
}
}5. Layouts
Q: ConstraintLayout vs other layouts?
Trả lời:
| Layout | Nesting | Performance | Flexibility |
|---|---|---|---|
| ConstraintLayout | Flat | ✅ Best | ✅ Highest |
| LinearLayout | Can nest | ⚠️ OK | Medium |
| RelativeLayout | 2-pass | ⚠️ Slower | Medium |
| FrameLayout | Flat | ✅ Fast | Low |
<!-- ConstraintLayout - flat hierarchy -->
<androidx.constraintlayout.widget.ConstraintLayout>
<ImageView
android:id="@+id/avatar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/name"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toTopOf="@id/avatar"/>
<TextView
android:id="@+id/email"
app:layout_constraintStart_toStartOf="@id/name"
app:layout_constraintTop_toBottomOf="@id/name"/>
</androidx.constraintlayout.widget.ConstraintLayout>ConstraintLayout features:
- Chains, barriers, guidelines
- Percentage sizing
- Circular positioning
- MotionLayout animations
Q: View measure specs?
Trả lời:
MeasureSpec là 32-bit int chứa mode (2 bits) và size (30 bits).
| Mode | Description | Example |
|---|---|---|
EXACTLY | Exact size required | 100dp, match_parent |
AT_MOST | Max size limit | wrap_content with limit |
UNSPECIFIED | No constraints | ScrollView children |
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val desiredWidth = 200.dpToPx()
val width = when (widthMode) {
MeasureSpec.EXACTLY -> widthSize
MeasureSpec.AT_MOST -> minOf(desiredWidth, widthSize)
else -> desiredWidth
}
setMeasuredDimension(width, calculatedHeight)
}6. Animations
Q: Animation types trong Android?
Trả lời:
| Type | Use Case | API |
|---|---|---|
| Property Animation | Any property | ObjectAnimator, ValueAnimator |
| View Animation | Simple transforms | XML, Animation class |
| Transition | Layout changes | TransitionManager |
| MotionLayout | Complex gestures | MotionScene |
// ObjectAnimator
ObjectAnimator.ofFloat(view, "translationX", 0f, 100f).apply {
duration = 300
interpolator = AccelerateDecelerateInterpolator()
start()
}
// ViewPropertyAnimator (simpler)
view.animate()
.translationX(100f)
.alpha(0.5f)
.setDuration(300)
.setInterpolator(AccelerateDecelerateInterpolator())
.start()
// Transition
TransitionManager.beginDelayedTransition(container, Fade())
view.visibility = View.GONE // Animates automatically
// MotionLayout
motionLayout.transitionToEnd()
motionLayout.setTransitionDuration(500)📝 View Best Practices
| Practice | Why |
|---|---|
| Keep hierarchy flat | Faster measure/layout |
| Use ConstraintLayout | Single-pass layout |
| Recycle ViewHolders | Memory efficiency |
| Use DiffUtil | Minimal updates |
Avoid requestLayout() in loops | Expensive |
Use invalidate() for redraw | Not full relayout |
Clean up in onDetachedFromWindow() | Prevent leaks |
// ❌ Bad - deep hierarchy
LinearLayout > LinearLayout > LinearLayout > TextView
// ✅ Good - flat with ConstraintLayout
ConstraintLayout > TextView, TextView, TextView📚 Tìm hiểu thêm: AndroidView trong Compose
Last updated on