嘿,朋友!我是Agnes。今天咱们不聊那些枯燥的教科书理论,我想带你真正走进Android开发的“战场”。
你还记得你第一次写出Toast.makeText(this, "Hello World", Toast.LENGTH_SHORT).show()时的心情吗?那时候屏幕亮起的瞬间,你觉得整个世界都是你的。但很快,现实给了你一记重锤——App闪退了,Logcat里红一片,全是NullPointerException。接着,列表滑动卡顿得像PPT,内存涨得让手机发烫,Activity一切换,数据全丢了……
别慌,这正是我们今天要聊的内容。从最原始的HelloWorld出发,一路穿越MVVM架构的迷宫,亲手挖开那些坑,最后站在Jetpack的的肩膀上俯瞰整个开发全景。我会用真实的代码和案例,像老程序员带新人一样,把这五年踩过的坑都摊开给你看。
第一章:HelloWorld的“隐藏陷阱”——你以为你懂Android吗?
1.1 一个被忽视的“最小可运行单元”
很多初学者写的HelloWorld是这样的:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 很多教程到这里就结束了
val button = findViewById<Button>(R.id.btn_click)
button.setOnClickListener {
Toast.makeText(this, "Clicked!", Toast.LENGTH_SHORT).show()
}
}
}
看起来没问题,对吧?但如果你问:“如果用户在点击按钮时,App被系统回收内存并重新创建,会发生什么?” 很多人会愣住。
1.2 真实案例:那个“消失”的按钮
场景还原:小明写了一个倒计时功能,代码简洁:
class TimerActivity : AppCompatActivity() {
private lateinit var timer: Timer
private lateinit var textView: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_timer)
textView = findViewById(R.id.tv_timer)
timer = Timer()
timer.scheduleAtFixedRate(object : TimerTask() {
override fun run() {
runOnUiThread {
textView.text = "${--count} seconds left"
}
}
}, 0, 1000)
}
override fun onDestroy() {
super.onDestroy()
timer.cancel() // 看起来我们取消了timer
}
}
问题出在哪?
当系统因内存压力回收这个Activity时(比如用户按Home键,屏幕变暗,系统回收后台App),timer的run方法可能正在执行。此时textView可能已经被销毁(因为Activity生命周期已经走到onDestroy),但你依然在尝试访问它!
更糟的是,TimerTask持有TimerActivity的隐式引用。即使你调用了timer.cancel(),如果run方法已经入队,它仍然会执行一次,然后访问一个已经为空的textView引用。
结果:NullPointerException崩溃,或者更隐蔽的——IllegalStateException: Can not perform this action after onSaveInstanceState。
1.3 专家建议:从第一天起,养成“生命周期意识”
不要写“会跑的”代码,要写“活得明白”的代码。每个对象都要知道:
- 它什么时候出生(
onCreate) - 它什么时候该休息(
onPause/onStop) - 它什么时候彻底消失(
onDestroy)
改进方案:
class TimerActivity : AppCompatActivity() {
private var timer: Timer? = null
private var count = 60
private var isActivityAlive = true // 一个标志位
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_timer)
startTimer()
}
private fun startTimer() {
timer = Timer()
timer?.scheduleAtFixedRate(object : TimerTask() {
override fun run() {
if (!isActivityAlive) return // 如果Activity已死,停止任务
runOnUiThread {
if (isActivityAlive) { // 双重检查
textView.text = "${--count} seconds left"
}
}
}
}, 0, 1000)
}
override fun onPause() {
super.onPause()
isActivityAlive = false // 暂停时标记为不可用
timer?.cancel()
}
override fun onResume() {
super.onResume()
isActivityAlive = true // 恢复时重新启用
startTimer()
}
override fun onDestroy() {
super.onDestroy()
timer?.cancel()
timer = null // 释放引用,防止内存泄漏
}
}
你看,这就是“从HelloWorld开始就要建立的严谨思维”。
第二章:数据绑定的误区——为什么你的App越来越慢?
2.1 传统写法 vs 数据绑定
很多开发者刚接触数据绑定(Data Binding),会兴奋地把它用在所有地方。结果发现,每输入一个字符,整个列表都在刷新!
错误示范:
<!-- activity_main.xml -->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="viewModel"
type="com.example.app.MainViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={viewModel.searchQuery}" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{viewModel.displayName}" />
<!-- 一个复杂的RecyclerView -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adapter="@{viewModel.adapter}" />
</LinearLayout>
</layout>
class MainViewModel : ViewModel() {
val searchQuery = MutableLiveData<String>()
val displayName = MutableLiveData<String>()
val adapter = LivePagingDataAdapter<String, ???>(diffCallback)
init {
// 每次searchQuery变化,都触发displayName更新
searchQuery.observeForever { query ->
displayName.value = "Hello, $query!"
}
}
}
问题:adapter是一个LiveData吗?不是!但你把它绑到了RecyclerView上。更糟的是,searchQuery变化时,displayName变化,这会触发整个View树的重绘,包括RecyclerView!即使内容没变,它也会重新请求布局。
2.2 数据绑定的正确姿势:局部更新
数据绑定的核心优势是细粒度的UI更新,而不是全量刷新。
关键原则:
- 只绑定需要响应用户交互的UI:比如输入框、按钮状态。
- 复杂列表不要直接用数据绑定:用ViewModel + LiveData/StateFlow + Adapter分离处理。
- 使用
@Bindable和ObservableField代替LiveData做简单绑定:它们不触发整个View树的重组。
改进方案:
class MainViewModel : ViewModel() {
// 使用ObservableField,性能比LiveData好,因为它不触发全量重组
val searchQuery = ObservableField<String>()
val displayName = ObservableField<String>()
// 列表数据单独管理,不直接绑定到View
private val _items = MutableLiveData<List<String>>()
val items: LiveData<List<String>> = _items
fun onSearchQueryChanged(newQuery: String) {
searchQuery.set(newQuery)
// 只更新需要变化的值
displayName.set("Searching: $newQuery")
// 触发列表数据更新,而不是UI重组
filterItems(newQuery)
}
private fun filterItems(query: String) {
// 模拟数据过滤
val filtered = allItems.filter { it.contains(query, ignoreCase = true) }
_items.postValue(filtered)
}
}
<!-- 只绑定输入框和简单的文本显示 -->
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onTextChanged="@{(text, start, before, count) -> viewModel.onSearchQueryChanged(text.toString())}" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{viewModel.displayName}" />
<!-- RecyclerView使用独立的Adapter,不通过数据绑定 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adapter="@{viewModel.adapter}" />
注意:这里我们用了onTextChanged回调,而不是@={}双向绑定。因为双向绑定在长列表场景下性能开销巨大。
第三章:RecyclerView优化——从“卡顿”到“丝滑”
3.1 你为什么会遇到卡顿?
RecyclerView卡顿的三大元凶:
- ViewHolder绑定数据过多:每次
onBindViewHolder都做大量计算或IO操作。 - 布局嵌套过深:复杂的XML层级导致测量和绘制耗时。
- 频繁刷新列表:调用
notifyDataSetChanged()而不是局部刷新。
3.2 真实案例:图片加载导致的OOM和卡顿
问题代码:
class SimpleAdapter : RecyclerView.Adapter<SimpleAdapter.ViewHolder>() {
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val imageView: ImageView = view.findViewById(R.id.image_view)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_simple, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
// 错误!在主线程加载网络图片
val bitmap = loadBitmapFromUrl(item.imageUrl)
holder.imageView.setImageBitmap(bitmap)
}
}
后果:
- 主线程被阻塞,UI卡顿。
- 快速滑动时,大量图片请求堆积,内存暴涨,触发OOM。
- 图片加载速度慢,用户体验极差。
3.3 优化方案:异步加载 + 缓存 + 占位符
步骤1:使用Glide/Picasso等图片库
class OptimizedAdapter : RecyclerView.Adapter<OptimizedAdapter.ViewHolder>() {
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val imageView: ImageView = view.findViewById(R.id.image_view)
val progressBar: ProgressBar = view.findViewById(R.id.progress_bar)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
// Glide自动处理异步加载、缓存、取消请求
Glide.with(holder.itemView.context)
.load(item.imageUrl)
.placeholder(R.drawable.placeholder) // 占位图,避免空白闪烁
.error(R.drawable.error_image) // 错误时显示
.into(holder.imageView)
// 如果需要显示加载进度,可以监听
Glide.with(holder.itemView.context)
.load(item.imageUrl)
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
holder.progressBar.visibility = View.GONE
return false
}
override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
holder.progressBar.visibility = View.GONE
return false
}
})
.into(holder.imageView)
}
}
步骤2:使用DiffUtil进行精确更新
class DiffCallback : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String) = oldItem == newItem
override fun areContentsTheSame(oldItem: String, newItem: String) = oldItem == newItem
}
class OptimizedAdapter : ListAdapter<String, OptimizedAdapter.ViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_simple, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
holder.textView.text = item
// 图片加载逻辑同上
}
}
// 在ViewModel中
val items = MutableLiveData<List<String>>()
fun updateItems(newItems: List<String>) {
// 使用ListAdapter的submitList,它会自动计算差异
// 不需要手动调用notifyDataSetChanged
items.postValue(newItems)
}
步骤3:布局优化
<!-- 避免嵌套过深,使用ConstraintLayout -->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/image_view"
android:layout_width="0dp"
android:layout_height="200dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/text_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="@id/image_view"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
3.4 高级技巧:预取(Prefetch)和分页
对于长列表,使用Paging 3库:
class MyViewModel : ViewModel() {
val pagedList = Pager(
config = PagingConfig(pageSize = 20),
remoteMediator = MyRemoteMediator(),
pagingSourceFactory = { MyPagingSource() }
).flow.cachedIn(viewModelScope)
}
这会自动处理数据分页、缓存和网络请求,你只需要关注UI展示。
第四章:MVVM架构——不是“模式”,是“纪律”
4.1 为什么你需要MVVM?
想象一下:你的Activity里写了500行代码,包含业务逻辑、UI更新、数据获取、网络请求…… 维护它就像在屎山上看风景。
MVVM的核心价值是分离关注点:
- Model:数据模型(POJO/DTO)
- ViewModel:业务逻辑和UI状态
- View:UI布局(XML)和控制器(Activity/Fragment)
4.2 一个完整的MVVM实战:用户列表App
步骤1:定义Model
data class User(
val id: Int,
val name: String,
val email: String,
val avatarUrl: String
)
步骤2:创建Repository(数据层)
class UserRepository(private val api: UserApi) {
suspend fun getUsers(): Result<List<User>> {
return try {
val response = api.getUsers()
Result.success(response.data)
} catch (e: Exception) {
Result.failure(e)
}
}
}
步骤3:编写ViewModel(核心)
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
// UI状态
private val _uiState = MutableStateFlow<UserUiState>(UserUiState.Loading)
val uiState: StateFlow<UserUiState> = _uiState
// 事件
private val _events = Channel<UserEvent>()
val events: Channel<UserEvent> = _events
init {
loadUsers()
}
fun loadUsers() {
viewModelScope.launch {
_uiState.value = UserUiState.Loading
repository.getUsers().fold(
onSuccess = { users ->
_uiState.value = UserUiState.Success(users)
},
onFailure = { error ->
_uiState.value = UserUiState.Error(error.message ?: "Unknown error")
}
)
}
}
fun onRetryClick() {
loadUsers()
}
fun onUserClick(user: User) {
viewModelScope.launch {
_events.send(UserEvent.NavigateToDetail(user))
}
}
}
// UI状态数据类
sealed class UserUiState {
object Loading : UserUiState()
data class Success(val users: List<User>) : UserUiState()
data class Error(val message: String) : UserUiState()
}
// 事件
sealed class UserEvent {
data class NavigateToDetail(val user: User) : UserEvent()
}
步骤4:View层(Activity)
”`kotlin class UserActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModels()
private lateinit var binding: ActivityUserBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityUserBinding.inflate(layoutInflater)
setContentView(binding.root)
setupToolbar()
observeViewModel()
}
private fun setupToolbar() {
binding.toolbar.setTitle("Users")
setSupportActionBar(binding.toolbar)
}
private
