简体   繁体   English

Kotlin onClick 事件和架构最佳实践

[英]Kotlin onClick events and architecture best practices

I have just started my first Kotlin app, and I'm aiming to learn the best practices for that language along the way (and end up with a working app of course :) ).我刚刚开始我的第一个 Kotlin 应用程序,我的目标是在此过程中学习该语言的最佳实践(当然最终会得到一个可用的应用程序 :))。 I've come across a problem I find myself struggling with for some time: What should be the flow of an onClick event?我遇到了一个问题,我发现自己苦苦挣扎了一段时间:onClick 事件的流程应该是什么?

At first, I used the strait forward way of setting the onClick in my fragment like so: binding.image_profile_picture.setOnClickListener { onChangePhoto() } .起初,我使用了在我的片段中设置 onClick 的海峡转发方式,如下所示: binding.image_profile_picture.setOnClickListener { onChangePhoto() }

I went over some of the kotlin training codelabs and changed my code accordingly.我浏览了一些 kotlin 培训代码实验室并相应地更改了我的代码。 One thing I understood is that it is recommended to handle events in the ViewModel and not in the fragment ( codelab#3 ), so now my onClicks are set in the layout xml like so: android:onClick="@{() -> profileViewModel.onChangePhoto()}" .我理解的一件事是,建议在 ViewModel 中而不是在片段中处理事件( codelab#3 ),所以现在我的 onClicks 在布局 xml 中设置如下: android:onClick="@{() -> profileViewModel.onChangePhoto()}"

The problem with that is all of my events actually need a context as they start with some kind of dialog (like the image picker).问题是我的所有事件实际上都需要一个上下文,因为它们以某种对话框(如图像选择器)开始。 I found this article recommending to solve this problem using an event wrapper.我发现这篇文章建议使用事件包装器来解决这个问题。 I read the discussion on it's implementation's Github page and decided to give it a shot (also I'm not sure I like this unnecessary ViewModel-Fragment ping-pong).我阅读了关于它的实现的Github 页面的讨论并决定试一试(我也不确定我是否喜欢这个不必要的 ViewModel-Fragment 乒乓)。 I implemented aminography's OneTimeEvent and now my ViewModel looks like this:我实现了Anomography 的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))
    }

    ...

and my Fragment's onCreateView looks like this:我的 Fragment 的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

And if we stick to showImagePicker as an example, it looks like this:如果我们坚持以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) }
        }
    }

The reason I am passing the Authentication::updatePhotoUrl function like that instead of just calling it from the Fragment is that I want to stick to the MVVM guidelines .我像这样传递Authentication::updatePhotoUrl函数而不是仅仅从 Fragment 调用它的原因是我想坚持MVVM 指南 All the calls to FirebaseAuth API feel like a "Repository-level" to me so I handle them in my Authentication class which I don't want my Fragments to interact with directly.对 FirebaseAuth API 的所有调用对我来说都像是“存储库级别”,因此我在我的Authentication类中处理它们,我不希望 Fragments 直接与之交互。 But this is becoming funny, as I am ending up storing those functions as members of my Fragment - and that just doesn't feel right.但这变得有趣了,因为我最终将这些函数存储为 Fragment 的成员 - 这感觉不对。 There has to be a neater solution than that.必须有比这更简洁的解决方案。 Please help me find it :)请帮我找到它:)

Thanks!谢谢! Omer欧玛

First, using private backing fields can be very annoying.首先,使用私有支持字段可能非常烦人。 I would recommend using an interface instead:我建议改用interface

interface MyViewModel {
    val event: LiveData<OneTimeEvent<EventType<Nothing>>>
}

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

Second, if all you need is a context then you can move all the logic to the ViewModel and use something like this:其次,如果您只需要一个上下文,那么您可以将所有逻辑移动到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)
    }
}

In general, I think your approach to MVVM is correct and all the logic should be handled by the ViewModel while the View should pass the requests from the user to the ViewModel and display UI changes that may occur as a result of these actions (or some other changes in data).总的来说,我认为您对 MVVM 的方法是正确的,所有逻辑都应该由ViewModel处理,而View应该将来自用户的请求传递给ViewModel并显示可能由于这些操作(或某些操作)而发生的 UI 更改数据的其他变化)。

The authentication should indeed be handled at the repository level so that authentication can be performed from several places and more importantly not to couple the authentication provider with the rest of your app logic.身份验证确实应该在存储库级别处理,以便可以从多个位置执行身份验证,更重要的是不要将身份验证提供程序与应用程序逻辑的其余部分耦合。 If you'll decide to change the authentication provider in the future, it should have as little impact as possible on your application.如果您决定将来更改身份验证提供程序,它应该对您的应用程序的影响尽可能小。

also I'm not sure I like this unnecessary ViewModel-Fragment ping-pong我也不确定我喜欢这个不必要的 ViewModel-Fragment 乒乓

That's a normal reaction that everyone has to it.这是每个人对它的正常反应。 The benefits are decoupling of View from ViewModel, which makes the View be just a dumb class that handles user input and display results to the user, while the ViewModel is the smart class that handles the logic.好处是将View与ViewModel解耦,使得View只是一个处理用户输入并向用户显示结果的哑类,而ViewModel则是处理逻辑的智能类。 This makes the logic easy to unit-test, as you don't need the entire application to be running (which a Fragment would require).这使得逻辑易于单元测试,因为您不需要运行整个应用程序(片段将需要)。 Views are quite boiler-place heavy, so it's great to keep the logic in another place, where its easier to read as it isn't surrounded by a ton of view code.视图非常繁重,因此将逻辑保存在另一个地方很好,因为它没有被大量视图代码包围,因此更易于阅读。

My events need a context [...]我的活动需要一个上下文 [...]

The events are just signals to the View to perform a specific action.这些事件只是给 View 执行特定操作的信号。 You will not be dealing with Context in the ViewModel, instead, the View can do what it needs with its Context, once it receives the signal to do so.您不会在 ViewModel 中处理 Context,相反,一旦 View 收到信号,它就可以用它的 Context 做它需要的事情。

Events can contain data, but not Android-specific data.事件可以包含数据,但不能包含特定于 Android 的数据。 If you want to for example pass a Drawable (or any other resource) through an event, you would pass only its resource ID, which the View can resolve into a Drawable using its Context.例如,如果您想通过事件传递 Drawable(或任何其他资源),您只需传递其资源 ID,View 可以使用其上下文将其解析为 Drawable。

Eg the ViewModel emits a ChangePhotoEvent ,例如 ViewModel 发出一个ChangePhotoEvent

The whole flow of a user clicking, and it resulting in a Dialog being shown, would look like this.用户点击的整个流程,并导致显示一个对话框,看起来像这样。

The View handles the click, and tells the ViewModel about it: View 处理点击,并告诉 ViewModel 关于它:

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

The ViewModel now determines what should happen, it can ask Repositories for data, check some conditions, etc. In this case, it just wants to display a photo picker to the user, so it sends an event to the View, with just enough information for it to know what is being asked of it: ViewModel 现在决定应该发生什么,它可以向 Repositories 询问数据,检查一些条件等。在这种情况下,它只是想向用户显示一个照片选择器,所以它向 View 发送一个事件,并提供足够的信息让它知道被问到什么:

The way you had OneTimeEvent implemented is worse than it has to be.你实现OneTimeEvent的方式比它必须的更糟糕。 Let me simplify it for you:让我为你简化一下:

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 -> {}
            }
        }
    }
}

I didn't understand what exactly you were doing with Authentication::updatePhotoUrl .我不明白你到底在做什么Authentication::updatePhotoUrl In onActivityResult , you should call the ViewModel again, with the result you received: viewModel.onPhotoChanged(data?.data) , and the ViewModel should call Authentication.updatePhotoUrl() .onActivityResult ,您应该再次调用 ViewModel,得到的结果是: viewModel.onPhotoChanged(data?.data) ,而 ViewModel 应该调用Authentication.updatePhotoUrl() All logic happens in the ViewModel, the View is just a relay of user-events.所有逻辑都发生在 ViewModel 中,View 只是用户事件的中继。

If you need to retrieve any data from an API, the ViewModel has to do that, on a background thread, preferably using Coroutines.如果您需要从 API 检索任何数据,则 ViewModel 必须在后台线程上执行此操作,最好使用协程。 Then you can pass the data as a param of the Event.然后您可以将数据作为事件的参数传递。


You could check out a framework, like RainbowCake , which provides base classes and helpers for this kind of stuff.您可以查看一个框架,例如RainbowCake ,它为此类内容提供基类和帮助程序。 I've been using it for a while, you can see a full project where I'm using it here .我已经使用它一段时间了,您可以在此处查看我正在使用它的完整项目。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM