You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android中ViewModel调用接口方法时Context为空引发空指针异常求助

问题分析与解决方案

问题

在Activity中实现ToastMessageListener接口并通过Hilt注入ViewModel后,ViewModel调用接口的showToast方法时出现Context为空的NullPointerException,但在Activity的生命周期方法(如onCreate)中直接显示Toast却正常。

核心原因

  1. 错误的作用域绑定:原Hilt模块将SplashActivity绑定到SingletonComponent,导致Activity被作为单例持有,脱离了正常的Android生命周期。当Activity销毁后,该单例引用的Context会变为null;即使Activity未销毁,单例的Activity实例也可能不是当前活跃的实例。
  2. 异步回调的生命周期问题:Firebase认证的addOnCompleteListener是异步执行的,当回调触发时,原Activity可能已经处于销毁或非活跃状态,此时调用持有的Listener会导致Context失效。

解决方案

方案1:修正Hilt绑定的作用域

将Listener的绑定改为与Activity生命周期绑定,避免单例持有Activity:

  1. 修改Hilt模块:
@Module
@InstallIn(ActivityComponent::class) // 改为ActivityComponent,与Activity生命周期绑定
abstract class ToastListenerModule {
    @Binds
    @ActivityScoped // 添加Activity作用域,确保Listener与Activity同生命周期
    abstract fun bindToastMessageListener(activity: SplashActivity): ToastMessageListener
}
  1. 修正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,彻底避免生命周期冲突:

  1. 修改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()
    }
}
  1. 修改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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 00:53:16