传统 View UI
RecyclerView 与 Material。
传统 View UI
这一课你将能做到
- 用 RecyclerView 实现高性能列表,理解 ViewHolder 回收机制
- 使用 Material Design 组件(Card、Dialog、BottomSheet)构建企业级界面
- 对比传统 View 体系与 Compose 的优劣,知道何时选择哪种方案
为什么要学
虽然 Compose 是未来,但存量 Android 代码 80% 以上仍是传统 View 体系。RecyclerView 是列表场景的终极方案,Material 组件是 Google 的设计规范落地。掌握它们,才能维护老项目,也才能理解 Compose 底层在解决什么问题。
人话:传统 View 就像 jQuery 时代的 DOM 操作——命令式、手动同步状态;Compose 像 React——声明式、自动 diff。但 jQuery 项目不会一夜消失,RecyclerView 的回收思想甚至影响了 Compose 的 LazyColumn 设计。
面试中,「RecyclerView 优化」和「自定义 View 流程」是资深 Android 工程师的分水岭。
先建立图像
RecyclerView 回收机制:
┌─────────────────────────┐
│ 屏幕可见区域 │
│ ┌─────┐ ┌─────┐ │
│ │Item1│ │Item2│ │ ← 可见的 ViewHolder
│ └─────┘ └─────┘ │
│ ┌─────┐ ┌─────┐ │
│ │Item3│ │Item4│ │
│ └─────┘ └─────┘ │
└─────────────────────────┘
↓ 滚动
┌─────────────────────────┐
│ 回收池 (RecycledViewPool)│
│ ┌─────┐ ┌─────┐ │
│ │Item1│ │Item2│ │ ← 滑出屏幕,等待复用
│ └─────┘ └─────┘ │
└─────────────────────────┘
↓ 新数据进入
┌─────────────────────────┐
│ 复用旧 ViewHolder │
│ 只绑定新数据,不创建新 View │
└─────────────────────────┘
跟着做
步骤 1:RecyclerView 基础列表
先定义数据模型:
data class Track(
val slug: String,
val title: String,
val lessonCount: Int,
val isCompleted: Boolean = false
)
创建 Adapter:
class TrackAdapter(
private val tracks: List<Track>,
private val onItemClick: (Track) -> Unit
) : RecyclerView.Adapter<TrackAdapter.TrackViewHolder>() {
class TrackViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val titleText: TextView = itemView.findViewById(R.id.tvTitle)
val countText: TextView = itemView.findViewById(R.id.tvCount)
val statusIcon: ImageView = itemView.findViewById(R.id.ivStatus)
val card: MaterialCardView = itemView.findViewById(R.id.cardView)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_track, parent, false)
return TrackViewHolder(view)
}
override fun onBindViewHolder(holder: TrackViewHolder, position: Int) {
val track = tracks[position]
holder.titleText.text = track.title
holder.countText.text = "${track.lessonCount} 节课"
holder.statusIcon.setImageResource(
if (track.isCompleted) R.drawable.ic_check_circle
else R.drawable.ic_circle_outline
)
holder.card.setOnClickListener { onItemClick(track) }
}
override fun getItemCount() = tracks.size
}
class TrackListActivity : AppCompatActivity() {
private val tracks = listOf(
Track("android-essentials", "Android 基础", 12, true),
Track("kotlin-essentials", "Kotlin 入门", 10),
Track("compose-advanced", "Compose 进阶", 8)
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_track_list)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = TrackAdapter(tracks) { track ->
Toast.makeText(this, "点击: ${track.title}", Toast.LENGTH_SHORT).show()
}
// 添加分割线
recyclerView.addItemDecoration(
DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
)
}
}
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/cardView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="12dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="center_vertical">
<ImageView
android:id="@+id/ivStatus"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_circle_outline"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="16dp"
android:orientation="vertical">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textStyle="bold"/>
<TextView
android:id="@+id/tvCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="@android:color/darker_gray"/>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
步骤 2:DiffUtil 高效更新
数据变化时,不要直接 notifyDataSetChanged(),用 DiffUtil 计算差异:
class TrackDiffCallback(
private val oldList: List<Track>,
private val newList: List<Track>
) : DiffUtil.Callback() {
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean {
return oldList[oldPos].slug == newList[newPos].slug
}
override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean {
return oldList[oldPos] == newList[newPos]
}
}
// 在 Adapter 中使用
fun updateData(newTracks: List<Track>) {
val diffResult = DiffUtil.calculateDiff(TrackDiffCallback(tracks, newTracks))
(tracks as MutableList).clear()
tracks.addAll(newTracks)
diffResult.dispatchUpdatesTo(this)
}
步骤 3:Material Dialog 与 BottomSheet
// 确认对话框
MaterialAlertDialogBuilder(this)
.setTitle("删除课程")
.setMessage("确定要删除「Android 基础」吗?此操作不可撤销。")
.setPositiveButton("删除") { _, _ ->
// 执行删除
}
.setNegativeButton("取消", null)
.show()
// BottomSheet
class TrackDetailBottomSheet : BottomSheetDialogFragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.bottom_sheet_track, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.findViewById<Button>(R.id.btnStart).setOnClickListener {
dismiss()
}
}
}
// 显示
TrackDetailBottomSheet().show(supportFragmentManager, "track_detail")
步骤 4:DrawerLayout 侧边导航
class MainActivity : AppCompatActivity() {
private lateinit var drawerLayout: DrawerLayout
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
drawerLayout = findViewById(R.id.drawerLayout)
val navView = findViewById<NavigationView>(R.id.navView)
// 汉堡菜单按钮
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val toggle = ActionBarDrawerToggle(
this, drawerLayout, toolbar,
R.string.open, R.string.close
)
drawerLayout.addDrawerListener(toggle)
toggle.syncState()
navView.setNavigationItemSelectedListener { menuItem ->
when (menuItem.itemId) {
R.id.nav_home -> showToast("首页")
R.id.nav_settings -> showToast("设置")
R.id.nav_about -> showToast("关于")
}
drawerLayout.closeDrawer(GravityCompat.START)
true
}
}
override fun onBackPressed() {
if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
drawerLayout.closeDrawer(GravityCompat.START)
} else {
super.onBackPressed()
}
}
}
步骤 5:自定义 View 基础
class ProgressRingView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val rectF = RectF()
private var progress = 0f
init {
paint.style = Paint.Style.STROKE
paint.strokeWidth = 12f
paint.strokeCap = Paint.Cap.ROUND
}
fun setProgress(value: Float) {
progress = value.coerceIn(0f, 100f)
invalidate() // 触发重绘
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val size = width.coerceAtMost(height).toFloat()
val padding = paint.strokeWidth / 2
rectF.set(
padding, padding,
size - padding, size - padding
)
// 背景圆环
paint.color = Color.LTGRAY
canvas.drawArc(rectF, 0f, 360f, false, paint)
// 进度弧
paint.color = Color.BLUE
val sweepAngle = progress * 3.6f
canvas.drawArc(rectF, -90f, sweepAngle, false, paint)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val size = MeasureSpec.getSize(widthMeasureSpec).coerceAtMost(
MeasureSpec.getSize(heightMeasureSpec)
)
setMeasuredDimension(size, size)
}
}
讲清楚要点
-
RecyclerView 的核心是回收:onCreateViewHolder 只在需要新 View 时调用,onBindViewHolder 在复用时重新绑定数据。这就是「对象池」模式,避免频繁创建/销毁 View 导致的内存抖动。
-
ViewHolder 不是可选的:早期 ListView 不用 ViewHolder 会导致 findViewById 每帧执行,性能极差。RecyclerView 强制使用 ViewHolder 模式。
-
Material Design 是规范,不是库:MaterialCardView、TextInputLayout、BottomNavigationView 都是 Material 组件库的实现。遵循规范能提升用户体验的一致性。
-
自定义 View 三步骤:onMeasure(测量尺寸)、onLayout(确定位置,ViewGroup 需要)、onDraw(绘制内容)。invalidate() 触发重绘,requestLayout() 触发重新布局。
-
命令式 vs 声明式:传统 View 是命令式——找到 View,设置属性。Compose 是声明式——描述状态,框架自动更新。RecyclerView 的 Adapter 模式其实已经很接近声明式了。
你可能会懵的地方
-
RecyclerView 和 ListView 到底差在哪?
ListView 只有一级缓存(ConvertView),RecyclerView 有四级(Scrap、Cache、RecycledViewPool、ViewCacheExtension)。RecyclerView 强制 ViewHolder、支持动画、布局管理器解耦。
-
为什么 notifyDataSetChanged() 性能差?
它告诉 RecyclerView「所有数据都变了」,导致所有 ViewHolder 重新绑定,没有动画。DiffUtil 精确计算差异,只更新变化的项。
-
Fragment 和自定义 View 怎么选?
Fragment 适合有独立生命周期、需要返回栈的「页面级」模块。自定义 View 适合「组件级」的 UI 元素,如进度环、图表。Compose 时代两者都减少使用。
-
XML 布局里的 tools: 命名空间有什么用?
tools:text="示例文本" 只在 Android Studio 预览时显示,运行时不会渲染。用于设计时占位。
-
ConstraintLayout 和 LinearLayout 性能差很多吗?
嵌套 LinearLayout 会导致多次测量,性能差。ConstraintLayout 扁平化布局,一次测量。但简单场景 LinearLayout 更易读。
错法对照
| 错法 | 问题 | 正解 |
|---|
notifyDataSetChanged() 更新列表 | 无动画,性能差 | DiffUtil 或 ListAdapter |
在 onBindViewHolder 里创建对象 | 每帧内存分配,GC 频繁 | 对象复用或提前创建 |
| 在 Activity 里直接操作 Adapter 数据 | 数据源与 UI 不同步 | 统一在 ViewModel 管理数据 |
自定义 View 忘记 invalidate() | 属性改变后界面不更新 | 修改属性后调用 invalidate() |
在 onDraw 里创建 Paint 对象 | 每帧内存分配 | 在 init 或构造函数中创建 |
卡点急救
-
RecyclerView 滚动卡顿:检查 onBindViewHolder 是否做了耗时操作(如解析日期、创建对象)。用 TraceView 或 Profiler 定位。
-
MaterialCardView 圆角不显示:确保 app:cardCornerRadius 生效,且没有父布局裁剪了子 View。检查 android:clipToPadding 和 android:clipChildren。
-
自定义 View 在预览中不显示:@JvmOverloads 注解不能少,否则 Android Studio 无法实例化。检查构造函数参数是否匹配。
面试常问
-
RecyclerView 的缓存机制?
四级缓存:Scrap(屏幕内)、Cache(刚滑出,默认 2 个)、ViewCacheExtension(自定义)、RecycledViewPool(共享池)。优先级从高到低,取到 ViewHolder 后重新绑定数据。
-
如何优化 RecyclerView 性能?
- DiffUtil 替代全量刷新;2. 避免
onBindViewHolder 耗时操作;3. 使用 setHasFixedSize(true) 跳过重新测量;4. 图片加载用 Glide/Coil 并取消不可见项;5. 减少布局嵌套。
-
自定义 View 的绘制流程?
measure() → layout() → draw()。onMeasure 确定宽高,onLayout 确定子 View 位置(ViewGroup),onDraw 执行绘制。invalidate 触发重绘,requestLayout 触发重新测量布局。
自测三问
- RecyclerView 的
onCreateViewHolder 和 onBindViewHolder 分别在什么时机调用?
- 为什么 MaterialAlertDialogBuilder 需要传入 Activity 而不是 Application Context?
- 自定义 View 中,
invalidate() 和 requestLayout() 的区别?
小结一句话
传统 View 体系是 Android 的「旧大陆」——命令式、手动同步、性能需要精细调优;掌握 RecyclerView 和 Material 组件,才能维护存量代码,也才能理解 Compose 的革新之处。
延伸