在视图模型范围内创建点击监听器(或其他监听器)可以吗?

Is it okay to create clicklisteners (or other listeners) inside a viewmodelscope?

我有一个包含 googleMap 的片段,我在其中创建了一堆标记(也是可点击的)。它们添加了来自房间实时数据查询的不同信息(颜色、形状等)。此外,我还有一些 MaterialButton 按钮(样式为按钮),我可以在其中切换标记可见状态。目前,这些标记的“设置”需要一些时间(200 毫秒到 2 秒,取决于标记的数量)。为了摆脱这种等待,我打算使用 viewmodelscope。由于其中定义了这些按钮的一些点击监听器(它们应该对标记执行一些操作),当 viewmodelscope 协程部分结束时它们是否仍然存在,如果它们还存在,它们是否仍然存在于正确的协程上下文中,当 fragment and/or viewmodel 结束时,我是否需要对监听器做一些内务处理?

I.E:

class MapsFragment:Fragment(){

   private lateinit var mapsViewModel : MapsViewModel
   private lateinit var googleMap : GoogleMap

//...
   override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        mapsViewModel = ViewModelProvider(requireActivity()).get(MapsViewModel::class.java)

        _binding = FragmentMapsBinding.inflate(inflater, container, false)
        val root:View = binding.root
//...
      return root
   }//onCreateView   

//...

   override fun onViewCreated(view: View, savedInstanceState:Bundle?){
      super.onViewCreated(view, savedInstanceState)
//...

      mapFragment?.getMapAsync(_googleMap->
         _googleMap?.let{safeGoogleMap->
            googleMap = safeGoogleMap
         }?:let{
            Log.e(TAG,"googleMap is null!!")
            return@getMapAsync
         }   
//...
      
         mapsViewModel.apply{

            liveDataMapsListFromFiltered?.observe(
               viewLifecycleOwner
            ){mapDetailList->
                
               viewModelScope.launch{ 

                
                  binding.apply{

                     //...
                     siteMarkers.map{
                       siteMarker.remove() //removes existing markes from map on update
                     }
                     siteMarkers.clear() //empty the siteMarker array on update 
                     //...
                   
                     mapDetailList?.map{
                        it.apply{
                           //...
                           coordinateSiteLongitude?.let { lng->
                              coordinateSiteLatitude?.let { lat->
                                 siteMarkerLatLng = LatLng(lat,lng)
                                 siteLatLngBoundsBuilder?.include(siteMarkerLatLng)
                              }
                           }
                           //...
                           siteMarkerLatLng?.let { safeSiteMarkerLatLng ->
                              val siteMarkerOptions =
                               MarkerOptions()
                                    .position(safeSiteMarkerLatLng)
                                    .anchor(0.5f, 0.5f)
                                    .visible(siteMarkerState)
                                    .flat(true)
                                  .title(setTicketNumber(ticketNumber?.toDouble()))
                                    .snippet(appointmentName)//TODO: Consider build siteId instead
                                    .icon(siteIcon[iconType])
                              siteMarkers.add(
                                  googleMap.addMarker(siteMarkerOptions) //Here are the markers added
                              )
                           }//siteMarkerLatLng?.let
                         

                        }//it.apply
                   
                     }//mapDetailList?.map

                     onSiteCheckedChangeListener?.let{
                        fragmentMapsMapTagSelector
                           ?.apTagSelectorMaterialButtonSite
                           ?.removeOnCheckedChangeListener(it) //clearing listener on button before update
                     }
                   
                     onSiteCheckedChangeListener = MaterialButton.OnCheckedChangeListener { siteButton, isChecked ->
                            
                        siteMarkers.map {
                            it.isVisible = isChecked
                        }
                     }.also {
                        fragmentMapsMapTagSelector
                          ?.mapTagSelectorMaterialButtonSite
                          ?.addOnCheckedChangeListener(it)
                     }

                     //Will this onCheckedChangeListener still survive when this viewmodelscope runs to the end ?
                   

                  }//binding.apply

               }//viewModelScope.launch

            }//liveDataMapsListFromFiltered.observe
         
         }//mapsviewModel.apply   


      }//getMapAsync
   
   }//onViewCreated

}//MapsFragment

我认为您误解了 CoroutineScope 是什么。它确定它运行的协程的生命周期,但不决定在 运行 这些协程的过程中创建的对象的生命周期。

viewModelScope 是一个 CoroutineScope,当关联的 ViewModel 被拆除时,它会自动取消它是 运行 的任何协程。协程不知道你在用它做什么。取消协程只是停止它 运行 完成,就像提前从函数返回一样。在您的代码中,您设置了您的侦听器并且除了在它们设置的视图中之外没有存储对它们的引用,因此它们的生命与各自视图的生命相关联。

如果您要在片段中使用协程为 UI 设置一些东西,您将使用片段的 lifecycleScope,而不是 ViewModel 的 viewModelScope。就像您正在获取要在 UI 中显示的内容一样,您会希望在 Fragment 被销毁时取消协程,而不是可能比 Fragment 寿命更长的 ViewModel。

您在示例代码中使用协程看起来毫无意义,因为我没有看到任何阻塞或异步挂起函数被调用。您提到设置站点标记大约需要 200 毫秒。 Google 地图我不是很熟悉,因为这几年没用过,不知道哪个部分比较耗时。通常,UI 元素不允许您在后台线程上与它们交互,因此您可能会倒霉。但是也许允许在后台线程上完成耗时的部分。您必须阅读文档。为此使用协程不会减少时间,但可以防止 UI 来自 stuttering/freezing.

如果您要使用协程进行一些长时间的计算,则需要切换调度程序来执行阻塞工作并与返回到主调度程序的 UI 元素进行交互。简单地把一些东西放在协程中并不能减少它花费的时间,但它提供了一种方便的方法来在另一个线程上做一些事情,然后在结果准备好后在主线程上继续。例如:

lifecycleScope.launchWhenStarted { // lifecycle coroutines launch on main thread by default
    val result = withContext(Dispatchers.Default) { // switch to dispatcher for background work
        doTimeConsumingCalculation()
    }
    // back on main thread:
    applyResultsToMyViews(result) 
}

通过使用 launchWhenStarted 而不是 launch,Fragment 的 lifecycleScope 将在 Fragment 未附加时暂停协程,这将防止潜在的崩溃尝试更新 UI 当没有 Activity.

时使用 requireContext()requireActivity()