首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >Kotlin onClick事件和体系结构最佳实践

Kotlin onClick事件和体系结构最佳实践
EN

Stack Overflow用户
提问于 2020-10-11 11:53:27
回答 2查看 924关注 0票数 2

我刚刚开始了我的第一个Kotlin应用程序,我的目标是学习该语言的最佳实践(当然,我最终会得到一个有用的应用程序:)。我遇到了一个困扰我一段时间的问题: onClick事件的流程应该是什么?

首先,我使用海峡前进的方式在我的片段中设置onClick,比如:binding.image_profile_picture.setOnClickListener { onChangePhoto() }

我检查了一些kotlin训练代码,并相应地修改了我的代码。我理解的一件事是,建议处理ViewModel中的事件,而不是片段(codelab#3)中的事件,因此我的onClicks现在被设置在布局xml中,比如:android:onClick="@{() -> profileViewModel.onChangePhoto()}"

问题是,我所有的事件实际上都需要一个上下文,因为它们开始于某种对话(比如图像选择器)。我发现这篇文章建议使用事件包装器来解决这个问题。我阅读了关于它的实现的Github页面的讨论,并决定给它一个尝试(我也不确定我是否喜欢这个不必要的视图模型片段乒乓)。我实现了氨基照相术 OneTimeEvent,现在我的ViewModel如下所示:

代码语言:javascript
运行
复制
// 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是这样的:

代码语言:javascript
运行
复制
        ...

        // 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为例,它看起来如下:

代码语言:javascript
运行
复制
    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类中处理它们,我不希望我的片段直接与它们交互。但这正变得越来越有趣,因为我最终会把这些功能存储为我的片段的成员--而这感觉是不对的。必须有一个更整洁的解决方案。请帮我找到它:)

谢谢!奥默

EN

回答 2

Stack Overflow用户

回答已采纳

发布于 2020-10-11 15:42:46

另外,我也不确定我是否喜欢这个不必要的视图模型片段乒乓

这是每个人都必须面对的正常反应。好处是视图与ViewModel脱钩,这使得视图只是一个处理用户输入和向用户显示结果的哑类,而ViewModel是处理逻辑的智能类。这使得逻辑易于单元测试,因为您不需要整个应用程序运行(片段需要这样做)。视图非常麻烦,所以将逻辑保留在另一个地方是很好的,因为它没有被大量的视图代码包围,所以更容易阅读。

我的事件需要一个背景..。

这些事件只是向视图发出的信号,以执行特定的操作。您将不会在ViewModel中处理上下文,相反,一旦收到这样做的信号,视图就可以对其上下文执行它所需的操作。

事件可以包含数据,但不能包含Android特定的数据。例如,如果希望通过事件传递可绘制(或任何其他资源),则只传递其资源ID,视图可以使用其上下文将其解析为可绘制的。

例如ViewModel发出一个ChangePhotoEvent

用户单击的整个流程,并导致对话框显示,将如下所示。

视图处理单击,并将其告知ViewModel:

android:onClick="@{() -> profileViewModel.onChangePhoto()}"

ViewModel现在决定了应该发生什么,它可以向存储库询问数据,检查某些条件等等。在这种情况下,它只想向用户显示一个照片选择器,所以它将一个事件发送到视图,并提供足够的信息让它知道需要它做什么:

OneTimeEvent的实现方式比它更糟糕。让我为你简化一下:

代码语言:javascript
运行
复制
public interface OneShotEvent

abstract class BaseViewModel : ViewModel() {
    private val _events = MutableLiveData<OneShotEvent>()
    val events: LiveData<OneShotEvent> = _events

    fun postEvent(event: OneShotEvent) {
        _events.postValue(event)
    }
}
代码语言:javascript
运行
复制
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))
    }
}
代码语言:javascript
运行
复制
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,它为这类东西提供了基类和帮助。我使用它已经有一段时间了,您可以看到一个完整的项目,我正在使用它,这里

票数 -1
EN

Stack Overflow用户

发布于 2020-10-11 15:33:50

首先,使用私有支持字段可能非常烦人。我建议使用interface来代替:

代码语言:javascript
运行
复制
interface MyViewModel {
    val event: LiveData<OneTimeEvent<EventType<Nothing>>>
}

class MyViewModelImpl: MyViewModel {
    override val event = MutableLiveData<OneTimeEvent<EventType<Nothing>>>()
}

其次,如果您只需要一个上下文,那么您可以将所有逻辑移到ViewModel中,并使用如下所示:

代码语言:javascript
运行
复制
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更改。

身份验证确实应该在存储库级别处理,以便可以从多个地方执行身份验证,更重要的是,不要将身份验证提供者与应用程序逻辑的其余部分相结合。如果您将来决定更改身份验证提供程序,那么它应该对您的应用程序产生尽可能小的影响。

票数 -1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/64303699

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档