我刚刚开始了我的第一个Kotlin应用程序,我的目标是学习该语言的最佳实践(当然,我最终会得到一个有用的应用程序:)。我遇到了一个困扰我一段时间的问题: onClick事件的流程应该是什么?
首先,我使用海峡前进的方式在我的片段中设置onClick,比如:binding.image_profile_picture.setOnClickListener { onChangePhoto() }
。
我检查了一些kotlin训练代码,并相应地修改了我的代码。我理解的一件事是,建议处理ViewModel中的事件,而不是片段(codelab#3)中的事件,因此我的onClicks现在被设置在布局xml中,比如:android:onClick="@{() -> profileViewModel.onChangePhoto()}"
。
问题是,我所有的事件实际上都需要一个上下文,因为它们开始于某种对话(比如图像选择器)。我发现这篇文章建议使用事件包装器来解决这个问题。我阅读了关于它的实现的Github页面的讨论,并决定给它一个尝试(我也不确定我是否喜欢这个不必要的视图模型片段乒乓)。我实现了氨基照相术 OneTimeEvent
,现在我的ViewModel如下所示:
// One time event for the fragment to listen to
private val _event = MutableLiveData<OneTimeEvent<EventType<Nothing>>>()
val event: LiveData<OneTimeEvent<EventType<Nothing>>> = _event
// Types of supported events
sealed class EventType<in T>(val func: (T) -> Task<Void>?) {
class ShowMenuEvent(func: (Context) -> Task<Void>?, val view: View) : EventType<Context>(func)
class ChangePhotoEvent(func: (Uri) -> Task<Void>?) : EventType<Uri>(func)
class EditNameEvent(func: (String) -> Task<Void>?) : EventType<String>(func)
...
}
fun onShowMenu(view: View) {
_event.value = OneTimeEvent(EventType.ShowMenuEvent(Authentication::signOut, view))
}
fun onChangePhoto() {
_event.value = OneTimeEvent(EventType.ChangePhotoEvent(Authentication::updatePhotoUrl))
}
fun onEditName() {
_event.value = OneTimeEvent(EventType.EditNameEvent(Authentication::updateDisplayName))
}
...
我的片段的onCreateView
是这样的:
...
// Observe if an event was thrown
viewModel.event.observe(
viewLifecycleOwner, {
it.consume { event ->
when (event) {
is ProfileViewModel.EventType.ShowMenuEvent ->
showMenu(event.func, event.view)
is ProfileViewModel.EventType.EditEmailEvent ->
showEditEmailDialog(event.func)
is ProfileViewModel.EventType.ChangePhotoEvent ->
showImagePicker(event.func)
...
}
}
}
)
return binding.root
如果我们坚持以showImagePicker
为例,它看起来如下:
private fun showImagePicker(func: (Uri) -> Task<Void>?) {
onPickedFunc = func
val intent =
Intent(
Intent.ACTION_PICK,
android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI
)
startActivityForResult(intent, RC_PICK_IMAGE)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == RC_PICK_IMAGE && resultCode == Activity.RESULT_OK) {
data?.data?.let { onPickedFunc(it)?.withProgressBar(progress_bar) }
}
}
我之所以这样传递Authentication::updatePhotoUrl
函数,而不是仅仅从片段中调用它,是因为我想坚持使用MVVM准则。对FirebaseAuth API的所有调用对我来说都像是一个“库级”,所以我在Authentication
类中处理它们,我不希望我的片段直接与它们交互。但这正变得越来越有趣,因为我最终会把这些功能存储为我的片段的成员--而这感觉是不对的。必须有一个更整洁的解决方案。请帮我找到它:)
谢谢!奥默
发布于 2020-10-11 15:42:46
另外,我也不确定我是否喜欢这个不必要的视图模型片段乒乓
这是每个人都必须面对的正常反应。好处是视图与ViewModel脱钩,这使得视图只是一个处理用户输入和向用户显示结果的哑类,而ViewModel是处理逻辑的智能类。这使得逻辑易于单元测试,因为您不需要整个应用程序运行(片段需要这样做)。视图非常麻烦,所以将逻辑保留在另一个地方是很好的,因为它没有被大量的视图代码包围,所以更容易阅读。
我的事件需要一个背景..。
这些事件只是向视图发出的信号,以执行特定的操作。您将不会在ViewModel中处理上下文,相反,一旦收到这样做的信号,视图就可以对其上下文执行它所需的操作。
事件可以包含数据,但不能包含Android特定的数据。例如,如果希望通过事件传递可绘制(或任何其他资源),则只传递其资源ID,视图可以使用其上下文将其解析为可绘制的。
例如ViewModel发出一个ChangePhotoEvent
,
用户单击的整个流程,并导致对话框显示,将如下所示。
视图处理单击,并将其告知ViewModel:
android:onClick="@{() -> profileViewModel.onChangePhoto()}"
ViewModel现在决定了应该发生什么,它可以向存储库询问数据,检查某些条件等等。在这种情况下,它只想向用户显示一个照片选择器,所以它将一个事件发送到视图,并提供足够的信息让它知道需要它做什么:
OneTimeEvent
的实现方式比它更糟糕。让我为你简化一下:
public interface OneShotEvent
abstract class BaseViewModel : ViewModel() {
private val _events = MutableLiveData<OneShotEvent>()
val events: LiveData<OneShotEvent> = _events
fun postEvent(event: OneShotEvent) {
_events.postValue(event)
}
}
class MyViewModel: BaseViewModel() {
data class ChangePhotoEvent(val updatePhotoUrl: String) : OneShotEvent
// for events without parameters, define them as objects
// object ParameterlessEvent : OneShotEvent
fun onChangePhoto() {
postEvent(ChangePhotoEvent(Authentication::updatePhotoUrl))
}
}
class MyFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.events.observe(this) { event ->
when (event) {
is ChangePhotoEvent -> {
// display the dialog, here you can use Context to do so
showImagePicker(event.updatePhotoUrl)
}
// omit 'is' if event is an object
// ParameterlessEvent -> {}
}
}
}
}
我不明白你和Authentication::updatePhotoUrl
到底在做什么。在onActivityResult
中,您应该再次调用ViewModel,得到的结果是:viewModel.onPhotoChanged(data?.data)
,ViewModel应该调用Authentication.updatePhotoUrl()
。所有的逻辑都发生在ViewModel中,视图只是用户事件的中继.
如果您需要从API检索任何数据,ViewModel必须在后台线程上这样做,最好使用Coroutines。然后,您可以将数据作为事件的参数传递。
您可以查看一个框架,比如RainbowCake,它为这类东西提供了基类和帮助。我使用它已经有一段时间了,您可以看到一个完整的项目,我正在使用它,这里。
发布于 2020-10-11 15:33:50
首先,使用私有支持字段可能非常烦人。我建议使用interface
来代替:
interface MyViewModel {
val event: LiveData<OneTimeEvent<EventType<Nothing>>>
}
class MyViewModelImpl: MyViewModel {
override val event = MutableLiveData<OneTimeEvent<EventType<Nothing>>>()
}
其次,如果您只需要一个上下文,那么您可以将所有逻辑移到ViewModel
中,并使用如下所示:
interface FragmentEvent {
fun invoke(fragment: Fragment)
}
class ShowPickerEvent: FragmentEvent {
override fun invoke(fragment: Fragment) {
val intent =
Intent(
Intent.ACTION_PICK,
MediaStore.Images.Media.INTERNAL_CONTENT_URI
)
fragment.startActivityForResult(intent, RC_PICK_IMAGE)
}
}
一般来说,我认为您对MVVM的方法是正确的,所有的逻辑都应该由ViewModel
来处理,而View
应该将用户的请求传递给ViewModel
,并显示这些操作(或数据中的一些其他更改)可能发生的UI更改。
身份验证确实应该在存储库级别处理,以便可以从多个地方执行身份验证,更重要的是,不要将身份验证提供者与应用程序逻辑的其余部分相结合。如果您将来决定更改身份验证提供程序,那么它应该对您的应用程序产生尽可能小的影响。
https://stackoverflow.com/questions/64303699
复制相似问题