单事件实时数据多次发出空值将给出正确的结果
Single event live data emits null values multiple times will giving the correct result
我在我的应用程序中有一个简单的登录,它在服务器端验证用户名和密码,并基于它显示祝酒词消息。
LoginFragment
private fun onClickLogin(view: View) {
view.loginButton.setOnClickListener {
val emailAddress = view.emailTextInputEditText.text.toString()
val password = view.passwordTextInputEditText.text.toString()
viewmodel.generalLogin(emailAddress, password).observe(viewLifecycleOwner, Observer {
if(it != null){
if (it.status) {
Toast.makeText(
context,
"Hi, " + it.data?.displayName,
Toast.LENGTH_SHORT
).show()
val sharedPref = PreferenceManager
.getDefaultSharedPreferences(context)
val editor = sharedPref.edit()
editor.putString(getString(R.string.user_id), it.data?.email).apply()
editor.putString(getString(R.string.user_name), it.data?.email).apply()
activity?.finish()
} else {
Toast.makeText(
context,
"Error, " + it.message,
Toast.LENGTH_SHORT
).show()
}
}
})
}
}
LoginViewModel
fun generalLogin(email: String, password: String): LiveData<Resource<UserSession>> {
val encryptedPassword = MCryptHelper.bytesToHex(MCryptHelper().encrypt(password))
return Transformations.switchMap(loginRepository.generalLogin(email, encryptedPassword)) {
it.getContentIfNotHandled().let{ resource ->
val userSessionLiveData = MutableLiveData<Resource<UserSession>>()
userSessionLiveData.value = resource
return@switchMap userSessionLiveData
}
}
}
LoginRepository
fun generalLogin(email: String, encryptedPassword: String):MutableLiveData<SingleLiveEvent<Resource<UserSession>>>{
val login = Global.network.login(email, encryptedPassword)
login.enqueue(object : Callback<LoginResponse> {
override fun onResponse(call: Call<LoginResponse>, response: Response<LoginResponse>) {
if(response.body()?.status == 1){
val resource = Resource<UserSession>(true,"Success")
response.body().let {
if(it?.session != null){
resource.data = UserSession(it.session.userId!!,it.session.fullName!!)
}
}
loginMutableData.value = SingleLiveEvent(resource)
}else{
val resource = Resource<UserSession>(false,response.body()?.msg ?: "Login failed. Try again")
loginMutableData.value = SingleLiveEvent(resource)
}
}
override fun onFailure(call: Call<LoginResponse>, t: Throwable) {
loginMutableData.value = SingleLiveEvent(Resource(false, t.localizedMessage))
}
})
return loginMutableData
}
SingleLiveEvent
class SingleLiveEvent<out T>(private val content: T){
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
这个例子对用户来说效果很好。但我注意到的是 LoginViewModel
generalLogin
函数,它多次向片段发出空值,同时在其中给出写入值。这个应用程序只能在没有崩溃的情况下运行,因为我对 LoginFragment
进行了句柄空检查。当您尝试越来越多地使用不正确的登录凭据时,它似乎会增加 null 发出的次数。
有没有更好的方法来解决这个问题?如果 getContentIfNotHandled()
的结果为空,如果有一种方法可以处理此问题,那么根本不发出任何东西,以便在 Fragment 中观察不到任何东西,那就太好了。
提出你的建议。谢谢。
每一层都有一些不同程度的错误。让我们一一回顾。
[1] 点击侦听器内的片段附加观察器
每次用户点击按钮时,'LoginFragment' 都会创建一个新的观察者,而之前的观察者仍然存在并在运行。这就是为什么每次尝试登录时,发射次数都会增加 1 的原因。此外,此设计容易受到屏幕旋转和配置更改的影响。例如,假设用户在登录请求期间旋转屏幕。该登录请求的结果丢失了,因为没有任何东西在观察该请求,最糟糕的部分是视图将显示用户未登录,而存储库的观点是用户。
为了正确解决这个问题,您需要将观察逻辑和点击事件逻辑分开。还请记住始终在 onCreateView()
或 onActivityCreated()
中进行观察,除非您 pass-in 自定义 LifeCycleOwner
对象或移除观察者。
[2] switchMap 在视图模型中的使用不正确
另一个问题是每次调用 viewModel.generalLogin()
时,都会使用另一个 switchMap
,因此会创建一个全新的 LiveData
。 LiveData
不是您应该动态创建的东西。它们需要在视图模型初始化时创建一次并观察到视图模型被清除。
[3] 存储库
存储库代码大部分是合理的,但我认为可以通过使 generalLogin
而不是 return 和 LiveData
来改进它。只是返回以某种风格返回,或者根本不 return 任何东西。另请注意,当前存储库有 loginMutableData
。虽然这没关系,但这是您需要手动跟踪的另一个变量。如果可能,您通常希望保持存储库无状态。
所以完整的解决方案,存储库到片段:
存储库
// Create this new function
fun getLoginStatus(): LiveData<SingleLiveEvent<Resource<UserSession>>> {
return loginMutableData
}
// Notice this function does not return anything.
fun generalLogin(email: String, encryptedPassword: String) {
val login = Global.network.login(email, encryptedPassword)
login.enqueue(object : Callback<LoginResponse> {
override fun onResponse(call: Call<LoginResponse>, response: Response<LoginResponse>) {
...
}
override fun onFailure(call: Call<LoginResponse>, t: Throwable) {
...
}
})
}
ViewModel
val userSessionLiveData: LiveData<SingleLiveEvent<Resource<UserSession>>>
// val userSessionLiveData: LiveData<Resource<UserSession>>
init {
userSessionLiveData = loginRepository.getLoginStatus()
// Use below if some mapping needs to be done
// userSessionLiveData = Transformations.map(loginRepository.getLoginStatus()) {
// return it?.contentIfNotHandled?
// }
}
fun generalLogin(email: String, password: String) {
val encryptedPassword = MCryptHelper.bytesToHex(MCryptHelper().encrypt(password))
loginRepository.generalLogin(email, encryptedPassword)
}
片段
void onCreateView(...): View {
...
viewModel.userSEssionLiveData.observe(viewLifecycleOwner, Observer {
val resource = it?.contentIfNotHandled?
if (resource == null) return
val session = resource.data?
if (resource.status) {
Toast.makeText(
context,
"Hi, " + session.displayName,
Toast.LENGTH_SHORT
).show()
val sharedPref = PreferenceManager
.getDefaultSharedPreferences(context)
val editor = sharedPref.edit()
editor.putString(getString(R.string.user_id), session.email).apply()
editor.putString(getString(R.string.user_name), session.email).apply()
activity?.finish()
} else {
Toast.makeText(
context,
"Error, " + resource.message,
Toast.LENGTH_SHORT
).show()
}
}
...
}
private fun onClickLogin(view: View) {
view.loginButton.setOnClickListener {
val emailAddress = view.emailTextInputEditText.text.toString()
val password = view.passwordTextInputEditText.text.toString()
viewmodel.generalLogin(emailAddress, password)
}
}
我在我的应用程序中有一个简单的登录,它在服务器端验证用户名和密码,并基于它显示祝酒词消息。
LoginFragment
private fun onClickLogin(view: View) {
view.loginButton.setOnClickListener {
val emailAddress = view.emailTextInputEditText.text.toString()
val password = view.passwordTextInputEditText.text.toString()
viewmodel.generalLogin(emailAddress, password).observe(viewLifecycleOwner, Observer {
if(it != null){
if (it.status) {
Toast.makeText(
context,
"Hi, " + it.data?.displayName,
Toast.LENGTH_SHORT
).show()
val sharedPref = PreferenceManager
.getDefaultSharedPreferences(context)
val editor = sharedPref.edit()
editor.putString(getString(R.string.user_id), it.data?.email).apply()
editor.putString(getString(R.string.user_name), it.data?.email).apply()
activity?.finish()
} else {
Toast.makeText(
context,
"Error, " + it.message,
Toast.LENGTH_SHORT
).show()
}
}
})
}
}
LoginViewModel
fun generalLogin(email: String, password: String): LiveData<Resource<UserSession>> {
val encryptedPassword = MCryptHelper.bytesToHex(MCryptHelper().encrypt(password))
return Transformations.switchMap(loginRepository.generalLogin(email, encryptedPassword)) {
it.getContentIfNotHandled().let{ resource ->
val userSessionLiveData = MutableLiveData<Resource<UserSession>>()
userSessionLiveData.value = resource
return@switchMap userSessionLiveData
}
}
}
LoginRepository
fun generalLogin(email: String, encryptedPassword: String):MutableLiveData<SingleLiveEvent<Resource<UserSession>>>{
val login = Global.network.login(email, encryptedPassword)
login.enqueue(object : Callback<LoginResponse> {
override fun onResponse(call: Call<LoginResponse>, response: Response<LoginResponse>) {
if(response.body()?.status == 1){
val resource = Resource<UserSession>(true,"Success")
response.body().let {
if(it?.session != null){
resource.data = UserSession(it.session.userId!!,it.session.fullName!!)
}
}
loginMutableData.value = SingleLiveEvent(resource)
}else{
val resource = Resource<UserSession>(false,response.body()?.msg ?: "Login failed. Try again")
loginMutableData.value = SingleLiveEvent(resource)
}
}
override fun onFailure(call: Call<LoginResponse>, t: Throwable) {
loginMutableData.value = SingleLiveEvent(Resource(false, t.localizedMessage))
}
})
return loginMutableData
}
SingleLiveEvent
class SingleLiveEvent<out T>(private val content: T){
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
这个例子对用户来说效果很好。但我注意到的是 LoginViewModel
generalLogin
函数,它多次向片段发出空值,同时在其中给出写入值。这个应用程序只能在没有崩溃的情况下运行,因为我对 LoginFragment
进行了句柄空检查。当您尝试越来越多地使用不正确的登录凭据时,它似乎会增加 null 发出的次数。
有没有更好的方法来解决这个问题?如果 getContentIfNotHandled()
的结果为空,如果有一种方法可以处理此问题,那么根本不发出任何东西,以便在 Fragment 中观察不到任何东西,那就太好了。
提出你的建议。谢谢。
每一层都有一些不同程度的错误。让我们一一回顾。
[1] 点击侦听器内的片段附加观察器
每次用户点击按钮时,'LoginFragment' 都会创建一个新的观察者,而之前的观察者仍然存在并在运行。这就是为什么每次尝试登录时,发射次数都会增加 1 的原因。此外,此设计容易受到屏幕旋转和配置更改的影响。例如,假设用户在登录请求期间旋转屏幕。该登录请求的结果丢失了,因为没有任何东西在观察该请求,最糟糕的部分是视图将显示用户未登录,而存储库的观点是用户。
为了正确解决这个问题,您需要将观察逻辑和点击事件逻辑分开。还请记住始终在 onCreateView()
或 onActivityCreated()
中进行观察,除非您 pass-in 自定义 LifeCycleOwner
对象或移除观察者。
[2] switchMap 在视图模型中的使用不正确
另一个问题是每次调用 viewModel.generalLogin()
时,都会使用另一个 switchMap
,因此会创建一个全新的 LiveData
。 LiveData
不是您应该动态创建的东西。它们需要在视图模型初始化时创建一次并观察到视图模型被清除。
[3] 存储库
存储库代码大部分是合理的,但我认为可以通过使 generalLogin
而不是 return 和 LiveData
来改进它。只是返回以某种风格返回,或者根本不 return 任何东西。另请注意,当前存储库有 loginMutableData
。虽然这没关系,但这是您需要手动跟踪的另一个变量。如果可能,您通常希望保持存储库无状态。
所以完整的解决方案,存储库到片段:
存储库
// Create this new function
fun getLoginStatus(): LiveData<SingleLiveEvent<Resource<UserSession>>> {
return loginMutableData
}
// Notice this function does not return anything.
fun generalLogin(email: String, encryptedPassword: String) {
val login = Global.network.login(email, encryptedPassword)
login.enqueue(object : Callback<LoginResponse> {
override fun onResponse(call: Call<LoginResponse>, response: Response<LoginResponse>) {
...
}
override fun onFailure(call: Call<LoginResponse>, t: Throwable) {
...
}
})
}
ViewModel
val userSessionLiveData: LiveData<SingleLiveEvent<Resource<UserSession>>>
// val userSessionLiveData: LiveData<Resource<UserSession>>
init {
userSessionLiveData = loginRepository.getLoginStatus()
// Use below if some mapping needs to be done
// userSessionLiveData = Transformations.map(loginRepository.getLoginStatus()) {
// return it?.contentIfNotHandled?
// }
}
fun generalLogin(email: String, password: String) {
val encryptedPassword = MCryptHelper.bytesToHex(MCryptHelper().encrypt(password))
loginRepository.generalLogin(email, encryptedPassword)
}
片段
void onCreateView(...): View {
...
viewModel.userSEssionLiveData.observe(viewLifecycleOwner, Observer {
val resource = it?.contentIfNotHandled?
if (resource == null) return
val session = resource.data?
if (resource.status) {
Toast.makeText(
context,
"Hi, " + session.displayName,
Toast.LENGTH_SHORT
).show()
val sharedPref = PreferenceManager
.getDefaultSharedPreferences(context)
val editor = sharedPref.edit()
editor.putString(getString(R.string.user_id), session.email).apply()
editor.putString(getString(R.string.user_name), session.email).apply()
activity?.finish()
} else {
Toast.makeText(
context,
"Error, " + resource.message,
Toast.LENGTH_SHORT
).show()
}
}
...
}
private fun onClickLogin(view: View) {
view.loginButton.setOnClickListener {
val emailAddress = view.emailTextInputEditText.text.toString()
val password = view.passwordTextInputEditText.text.toString()
viewmodel.generalLogin(emailAddress, password)
}
}