【问题标题】:Is it okay to create clicklisteners (or other listeners) inside a viewmodelscope?可以在 viewmodelscope 中创建 clicklisteners(或其他 listeners)吗?
【发布时间】:2021-03-13 17:01:50
【问题描述】:

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

IE:

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

【问题讨论】:

  • “viewmodelscope 走到尽头”是什么意思? ViewModel 有一个名为viewModelScope 的属性,它是一个用于运行协程的 CoroutineScope,与您显示的代码无关。您已经在 Activity 的生命周期中检索了 ViewModel,因此 ViewModel 的生命周期将与托管此 Fragment 的 Activity 的生命周期相匹配。
  • 我实际上忘记了 viewmodelscope 行...抱歉,我会尽快重新整理我的 Q。
  • 然后我添加了viewModelScope.launch。通过这样做,我希望在设置标记并准备就绪时避免延迟。隐藏/取消隐藏可以很好地使用每个标记的可见参数(在观察按钮上的选中更改时通过它们进行迭代)。但我担心通过使用这个 viewmodelscope,当 viewmodelscope 结束时“基础”不存在,所以当片段死亡时,片段不会杀死按钮上的 onchangelisteners。
  • ...或者我是否需要一些“聪明的”withContext 钳制,例如 viewModelScope.launch 中的第一个 withContext(Dispatchers.Default){ ... } 并在每次写入 View 后代时使用 withContext(Dispatcher.Main){...} 对其进行调味。顺便说一句:您可以读取带有 Dispatchers.Main 上下文的视图(或后代)(不影响(更改)用户界面吗?)。
  • ... 以及如何最好地停止这些协程。我已将我的视图模型绑定到 Activity 而不是 Fragment。所以我可能对视图模型的寿命超过片段有问题,并且当片段死亡时,对视图 vil 的引用无效,因为视图模型仍然活着(?)。

标签: google-maps kotlin android-room kotlin-coroutines android-viewmodel


【解决方案1】:

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

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

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

您在示例代码中使用协程看起来毫无意义,因为我没有看到任何阻塞或异步挂起函数被调用。您提到设置站点标记需要 200 毫秒。我对谷歌地图不熟悉,因为这几年没用过,所以不知道哪个部分比较耗时。通常,UI 元素不允许您在后台线程上与它们进行交互,因此您可能不走运。但也许允许在后台线程上完成耗时的部分。您必须阅读文档。为此使用协程不会缩短时间,但可以防止 UI 卡顿/冻结。

如果您要使用协程进行一些长时间的计算,您需要切换调度程序来完成阻塞工作并与主调度程序上的 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,片段的lifecycleScope 将在片段未附加时暂停协程,这将防止潜在的崩溃尝试使用requireContext()requireActivity() 更新UI没有活动。

【讨论】:

  • 感谢您回答得这么好,这可以解决问题。我不知道launchWhenStarted,这可能有助于解决一些棘手的问题,但我想知道当使用launchWhenStarted时片段未附加时,您在片段中定义的侦听器是否“停止”......所以如果你点击一个按钮(当然会与听众做出反应)并尝试几乎同时销毁片段(即:将您的设备旋转 180 度),这也会阻止听众吗?
  • 协程与协程创建的对象的生命周期无关。无论它们是在标准函数中还是在任何类型的协程中创建的,都没有区别。当 Fragment 被销毁时,它的视图及其属性引用的所有内容都将被垃圾收集。这包括听众。 Fragment 在主线程上被拆除,您的视图侦听器也在主线程上被调用,因此您不必担心这些事情会同时发生。
  • 好的,非常感谢!!
  • 顺便说一句(如果有兴趣知道,我可能会将其发布在单独的 Q 中)。我实际上用launchWhenStarted 测试了你的方法,并在这里和那里测试一些效果。我首先用withContext(Dispatchers.Main) {...} 进行了包装,在里面我尝试从MaterialButton 中读取isChecked 值,就像val state = withContext(Dispatchers.Default) { myMaterialButton.isChecked } 一样。令我惊讶的是,这并没有崩溃!我认为您需要在主线程中执行各种操作(设置/阅读)。还是仅在设置值/执行操作时才需要这样做?
  • 我不认为从主线程的视图中读取属性会导致崩溃。从另一个线程设置值可能导致崩溃,但有时它会成功。一些 View 方法被设计为在检测到它们被从主线程中调用时立即抛出 RuntimeException。其他人则不会,因此最终可能会出现不一致的状态和同步问题。
猜你喜欢
  • 2023-03-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-06-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多