Android中ViewModel调用接口方法时Context为空引发空指针异常求助
问题分析与解决方案
问题
在Activity中实现ToastMessageListener接口并通过Hilt注入ViewModel后,ViewModel调用接口的showToast方法时出现Context为空的NullPointerException,但在Activity的生命周期方法(如onCreate)中直接显示Toast却正常。
核心原因
- 错误的作用域绑定:原Hilt模块将
SplashActivity绑定到SingletonComponent,导致Activity被作为单例持有,脱离了正常的Android生命周期。当Activity销毁后,该单例引用的Context会变为null;即使Activity未销毁,单例的Activity实例也可能不是当前活跃的实例。 - 异步回调的生命周期问题:Firebase认证的
addOnCompleteListener是异步执行的,当回调触发时,原Activity可能已经处于销毁或非活跃状态,此时调用持有的Listener会导致Context失效。
解决方案
方案1:修正Hilt绑定的作用域
将Listener的绑定改为与Activity生命周期绑定,避免单例持有Activity:
- 修改Hilt模块:
@Module @InstallIn(ActivityComponent::class) // 改为ActivityComponent,与Activity生命周期绑定 abstract class ToastListenerModule { @Binds @ActivityScoped // 添加Activity作用域,确保Listener与Activity同生命周期 abstract fun bindToastMessageListener(activity: SplashActivity): ToastMessageListener }
- 修正SplashActivity的构造方法(Activity不能使用构造注入,需移除
@Inject constructor()):
@AndroidEntryPoint class SplashActivity : AppCompatActivity(), ToastMessageListener { private lateinit var binding: ActivitySplashBinding @Inject lateinit var auth: FirebaseAuth // 注入ViewModel private val viewModel: AuthenticationViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) installSplashScreen() binding = ActivitySplashBinding.inflate(layoutInflater) setContentView(binding.root) } override fun showToast(message: String) { // 先判断Activity是否处于活跃状态,避免Context失效 if (!isFinishing && !isDestroyed) { Toast.makeText(this, message, Toast.LENGTH_SHORT).show() } } }
方案2:使用LiveData发送Toast事件(更推荐的MVVM实践)
ViewModel不直接持有Activity的Listener,而是通过LiveData发送事件,Activity观察事件并显示Toast,彻底避免生命周期冲突:
- 修改AuthenticationViewModel:
@HiltViewModel class AuthenticationViewModel @Inject constructor( private val auth: FirebaseAuth ) : ViewModel() { val emailText = ObservableField<String>("") val passwordText = ObservableField<String>("") val nameText = ObservableField<String>("") // 用于发送Toast事件的LiveData private val _toastEvent = MutableLiveData<String?>() val toastEvent: LiveData<String?> = _toastEvent fun onSubmitClicked(isLogin: Boolean) { if (isLogin) { val email = emailText.get() val password = passwordText.get() if (email.isNullOrEmpty() || password.isNullOrEmpty()) { println("Email or password is empty") return } auth.signInWithEmailAndPassword(emailText.get() ?: "", passwordText.get() ?: "") .addOnCompleteListener() { task -> if (task.isSuccessful) { _toastEvent.postValue("Login Successful") } else { val exception = task.exception _toastEvent.postValue("Login Failed: ${exception?.message}") } } } else { println("SIGN Up BUTTON CLICKED ${emailText.get()} ${passwordText.get()}") } } fun isValidInput(): Boolean { val inputEm = emailText.get() ?: return false val inputPass = passwordText.get() ?: return false return inputEm.isNotEmpty() && inputPass.isNotEmpty() } }
- 修改SplashActivity观察LiveData:
@AndroidEntryPoint class SplashActivity : AppCompatActivity() { private lateinit var binding: ActivitySplashBinding @Inject lateinit var auth: FirebaseAuth private val viewModel: AuthenticationViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) installSplashScreen() binding = ActivitySplashBinding.inflate(layoutInflater) setContentView(binding.root) // 观察Toast事件 viewModel.toastEvent.observe(this) { message -> message?.let { if (!isFinishing && !isDestroyed) { Toast.makeText(this, it, Toast.LENGTH_SHORT).show() } // 重置事件,避免重复显示 _toastEvent.value = null } } } }
注意事项
- 方案2中可移除原
ToastMessageListener接口的相关绑定,因为不再需要该接口。 - 始终在显示Toast前检查Activity的活跃状态,避免因Activity销毁导致的崩溃。
内容的提问来源于stack exchange,提问作者Ashish
相关产品推荐
相关产品推荐

