嘿,朋友!欢迎加入Android开发的大坑……哦不,是广阔天地!我是你的老学长,看着无数萌新从“Hello World”兴奋得跳脚,到被NullPointerException按在地上摩擦,再到能独立写出漂亮架构的代码战士。今天这篇指南,我不跟你拽那些晦涩的学术名词,我们就聊聊最接地气、也最让你头疼的几个坎儿:Activity的生命周期和ViewModel的正确姿势。
别担心,这篇文章没有冗长的引言,也没有最后那篇“综上所述”的废话。我们就像坐在咖啡馆里,边喝拿铁边看代码,把那些坑一个个填平。
一、 那些年被屏幕旋转“背刺”的日子:重新理解Activity生命周期
很多初学者刚接触Android时,觉得Activity就是个页面,打开就是onCreate,退出就是onDestroy。简单!直到有一天,你旋了一下屏幕(或者打开了系统设置面板),你发现数据没了!或者更糟糕,页面崩溃了!
1.1 生命周期的真相:它不是一条直线,而是一张网
首先,我们要纠正一个观念:Activity的生命周期并不是你按下返回键才结束那么简单。
想象一下Activity是一个人在生活中的状态:
- 诞生 (
onCreate):婴儿出生,准备一切(初始化视图、绑定数据)。 - 起床 (
onStart):睁开眼,知道周围存在了。 - 出门见人 (
onResume):开始和外界交互,拿到焦点,可以点按钮、看视频。 - 被电话打断 (
onPause):来了个弹窗或者另一个半透明的Activity,你的界面还看得见,但不能操作了。 - 躲进 closet (
onStop):完全被挡住,看不见了,但人还活着。 - 去世 (
onDestroy):彻底结束,内存释放。 - 复活 (
onRestart):如果从onStop状态重新回来,会先调用onRestart,然后再onStart->onResume。
关键点来了: 旋转屏幕会发生什么? 在旧的Android版本(以及很多默认配置下),旋转屏幕 = 销毁Activity = 重建Activity。
为什么?因为屏幕方向变了,系统认为“配置”变了,它为了确保使用正确的资源(比如横屏的布局文件),会选择杀掉当前的Activity,然后重新创建一个新的。
1.2 避坑实战:如何保存“瞬逝”的数据?
如果你还在onCreate里写 private String userData = "secret";,旋转屏幕后,userData就变回null或者初始值了。这就是经典的“数据丢失坑”。
错误示范(初学者常见):
public class MainActivity extends AppCompatActivity {
private String userScore = "0"; // 旋转屏幕后,这个变量重置了!
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 假设这里有个 TextView 显示分数
TextView scoreView = findViewById(R.id.tv_score);
scoreView.setText(userScore); // 旋转后,这里还是 "0",而不是用户刚才刷到的 "100"
}
// 用户点击按钮增加分数
public void addScore(View view) {
userScore = String.valueOf(Integer.parseInt(userScore) + 1);
((TextView) findViewById(R.id.tv_score)).setText(userScore);
}
}
正确姿势1: onSaveInstanceState(临时救急)
Bundle savedInstanceState 是系统递给你的“记事本”。
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("USER_SCORE", userScore); // 在销毁前,把数据塞进去
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ...
if (savedInstanceState != null) {
userScore = savedInstanceState.getString("USER_SCORE"); // 重建时取出来
}
}
注意:Bundle 适合存小量、简单、临时的数据(如字符串、基本类型)。千万别往里面塞大对象或复杂集合,容易OOM(内存溢出)甚至报错。
正确姿势2: 使用 ViewModel(这才是真正的解决方案)
这就是我们要讲的重头戏!onSaveInstanceState 是个“补丁”,而 ViewModel 是“架构”。
二、 ViewModel:别让Activity成为数据的“保管员”
为什么需要ViewModel? 因为Activity太“脆弱”了。它会被系统杀掉,会被旋转,会被配置变更搞死。但数据不应该这么脆弱。
ViewModel的核心思想: 把数据逻辑从Activity/Fragment中剥离出来,交给ViewModel管理。ViewModel的生命周期比Activity长,它会在屏幕旋转等配置变更时存活下来,数据还在!
2.1 如何引入ViewModel?
别慌,不需要下载什么神秘库。Google官方已经封装好了,在Android Studio里非常顺手。
添加依赖(如果你用的是较新的AGP版本,通常默认包含): 在
build.gradle (Module: app)中确认:dependencies { // Lifecycle implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2" // 推荐用KTX版本,更简洁 implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.2" // 如果使用 Kotlin,这俩是神器 implementation "androidx.activity:activity-ktx:1.7.2" implementation "androidx.fragment:fragment-ktx:1.6.2" }注:版本号请以你项目实际使用的为准,2.6.x 是目前较稳定的版本。
创建你的第一个ViewModel:
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel class ScoreViewModel : ViewModel() { // 用 MutableLiveData 包装数据,这样UI可以“观察”数据变化 val score = MutableLiveData<Int>().apply { value = 0 // 初始值 } fun addScore() { // 业务逻辑在这里,不在Activity里 score.value = (score.value ?: 0) + 1 } fun resetScore() { score.value = 0 } }看懂了吗?
ViewModel就是一个普通的Kotlin类,继承自ViewModel。它里面放着数据(LiveData)和操作数据的函数(addScore)。它不知道Activity的存在,不知道布局的存在,非常干净!
2.2 在Activity中连接ViewModel
现在,回到你的Activity,看看多么优雅:
class MainActivity : AppCompatActivity() {
// 这一行是关键!获取ViewModel,传入这个Activity的ViewModelStore
private val viewModel: ScoreViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val tvScore = findViewById<TextView>(R.id.tv_score)
val btnAdd = findViewById<Button>(R.id.btn_add)
// 观察数据变化。当 score 变化时,自动更新UI
viewModel.score.observe(this) { score ->
tvScore.text = score.toString()
}
btnAdd.setOnClickListener {
viewModel.addScore() // 只调用方法,不用管数据存哪
}
}
}
神奇的地方来了:
当你旋转屏幕,MainActivity 被销毁再重建。但是!by viewModels() 这个委托属性,会帮你在重建的Activity中找到同一个 ViewModel 实例(基于同一个 ViewModelStore)。
结果:tvScore 的文字不会闪烁,不会重置为0,因为它直接从活着的ViewModel里取数据。
2.3 避坑指南:ViewModel的三大陷阱
虽然ViewModel很好用,但新手很容易踩这几个坑:
陷阱1:在ViewModel里持有Context
// 错误!不要这样做!
class BadViewModel(private val context: Context) : ViewModel() {
fun showToast() {
Toast.makeText(context, "Oops", Toast.LENGTH_SHORT).show()
}
}
为什么不行? ViewModel的生命周期比Activity长。如果你把Activity的Context传进去,即使Activity被销毁了,ViewModel还拿着它的引用,内存泄漏(Memory Leak) 就发生了!系统回收不了这个Activity,内存一直涨,最后OOM。
正确做法: ViewModel里只做纯数据和逻辑操作。如果需要弹窗、Toast,回调给Activity处理,或者使用AndroidViewModel(它持有Application Context,相对安全,但尽量少用)。
陷阱2:每次点击都重新创建ViewModel
// 错误!这样和普通的类没区别,旋转屏幕还是丢数据
private val viewModel = ScoreViewModel()
为什么不行? by viewModels() 是Kotlin的属性委托,它利用了Activity的ViewModelProvider。如果你直接 new 一个,它就是一个普通对象,生命周期随变量作用域结束。
正确做法: 永远用 by viewModels() 或者 ViewModelProvider(this).get(ScoreViewModel::class.java)。
陷阱3:混淆 LiveData 和 StateFlow
现在Jetpack Compose流行,StateFlow 越来越常用。但在传统的View系统中,LiveData 是ViewModel的最佳拍档。
- LiveData:生命周期感知,Activity销毁了,它自动取消订阅,不会闪退。
- StateFlow:速度更快,适合Kotlin协程,但需要手动管理生命周期(或用
repeatOnLifecycle)。
建议: 初学者先用 LiveData,因为它直观且安全。熟悉后,再过渡到 StateFlow。
三、 进阶:ViewModel + Navigation + SharedViewModel
你以为ViewModel只能存个分数?太天真了!在实际开发中,页面间传递数据是个大痛点。
比如:用户在一个列表页,点击一个Item,跳转到详情页。传统做法是用 Intent.putExtra() 传参。但如果要传一个复杂的对象,或者两个页面要共享状态(比如购物车,详情页改了数量,列表页要实时更新),Intent就搞不定了。
这时候,共享ViewModel 登场!
3.1 场景:购物车小能手
假设你有一个 ShopActivity,里面有两个Fragment:
ProductListFragment:展示商品。CartFragment:展示已加购物车的商品。
当用户在列表页点击“加入购物车”,购物车页面的数量要立即变化。
3.2 代码实现
Step 1: 创建共享ViewModel
class CartViewModel : ViewModel() {
private val _cartItems = MutableLiveData<List<String>>(emptyList())
val cartItems: LiveData<List<String>> = _cartItems
fun addToCart(item: String) {
val current = _cartItems.value ?: emptyList()
_cartItems.value = current + item // 简单演示,实际应该是对象
}
}
Step 2: 在Activity中获取同一个ViewModel实例
关键点:必须在Activity中获取,然后传递给Fragment! 如果Fragment自己调 by viewModels(),它们会得到不同的ViewModel实例(因为它们的ViewModelStore不同)。
class ShopActivity : AppCompatActivity() {
// Activity持有唯一的ViewModel实例
private val cartViewModel: CartViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_shop)
// 将ViewModel传递给Fragment
val listFragment = ProductListFragment.newInstance(cartViewModel)
val cartFragment = CartFragment.newInstance(cartViewModel)
// 这里简化了FragmentTransaction,实际请用Navigation Component
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, listFragment)
.addToBackStack(null)
.commit()
}
}
Step 3: Fragment使用ViewModel
class ProductListFragment : Fragment() {
// 通过构造函数传入Activity提供的ViewModel
private val viewModel: CartViewModel by activityViewModels()
// 注意!这里用的是 activityViewModels(),不是 viewModels()!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btnAddToCart.setOnClickListener {
viewModel.addToCart("Apple")
}
}
}
class CartFragment : Fragment() {
private val viewModel: CartViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 观察购物车变化,实时更新UI
viewModel.cartItems.observe(viewLifecycleOwner) { items ->
tvCartCount.text = "共 ${items.size} 件商品"
}
}
}
重点解释 activityViewModels():
这个委托会从Activity的ViewModelStore中获取ViewModel,而不是Fragment自己的。这样,同一个Activity下的所有Fragment都能访问到同一个 CartViewModel,实现数据共享!
避坑:
- 不要用
by viewModels()在Fragment里,那样每个Fragment都有自己的副本。 - 不要用
requireActivity().intent传复杂对象,容易序列化失败。
四、 给初学者的“避坑”终极清单
读完上面的例子,你可能已经有点晕了。别急,我把最关键的几点浓缩成一张清单,建议你截图保存:
别在Activity里存状态!
- 凡是旋转屏幕后需要保留的数据,要么进
Bundle,要么进ViewModel。首选ViewModel。
- 凡是旋转屏幕后需要保留的数据,要么进
ViewModel不能持有Context(除非是Application Context)
- 想发Toast?把Context传进去是错的。把数据回调给Activity,让Activity去展示。
- 非要存Context?用
AndroidViewModel并传入Application。
理解
this和viewLifecycleOwner的区别- 在Fragment的
onCreateView里观察LiveData,用viewLifecycleOwner。 - 在
onCreate里观察,用this。 - 错误:在
onDestroyView之后还观察LiveData,会Crash!因为View已经没了,但LiveData还在回调。
- 在Fragment的
不要在ViewModel里做网络请求的直接UI响应
- ViewModel只负责数据。网络请求可以放在ViewModel里(用
viewModelScope),但结果要通过LiveData/StateFlow暴露出去,让UI层决定怎么处理。
- ViewModel只负责数据。网络请求可以放在ViewModel里(用
善用
by viewModels()和by activityViewModels()- Activity里用
by viewModels()。 - Fragment想独占数据用
by viewModels()。 - Fragment想和Activity共享数据用
by activityViewModels()。
- Activity里用
五、 写在最后:像工程师一样思考
朋友,Android开发不只是写代码,更是一种架构思维的训练。
Activity生命周期告诉我们:系统是不可信的,不要依赖它的善意。 ViewModel告诉我们:数据和展示要分离,责任要清晰。
当你下次再遇到旋转屏幕数据丢失的问题,或者两个页面传值传得头晕目眩时,记得回来看看这篇文章。别急着加 try-catch,先想想:我的数据放在哪里了?我的ViewModel存活了吗?
编程这条路,坑很多,但填平每一个坑,都是成长的印记。加油,未来的Android大牛!如果还有疑问,随时回来翻翻这篇文章,或者在评论区留言,我们一起交流。
(注:本文代码示例基于Kotlin,这是Google推荐的Android开发语言。如果你还在用Java,逻辑是一样的,只是语法稍显啰嗦,建议尽快过渡到Kotlin!)
