Kotlin 协程在 Compose 函数中调用了两次而不是一次

Posted

技术标签:

【中文标题】Kotlin 协程在 Compose 函数中调用了两次而不是一次【英文标题】:Kotlin coroutine called two times instead of once in Compose function 【发布时间】:2022-01-01 09:16:59 【问题描述】:

我正在使用 Kotlin、Jetpack Compose(用于 UI 和 Retrofit)开发一个 android 应用程序,用于向我创建的 REST API 服务器发出请求。我是 Kotlin Coroutines、Compose 和 Retrofit 的初学者,我面临以下问题:

在 HomeActivity 启动后,HomeViewModel 中的 assignSyncer() 函数立即被调用两次而不是一次,该函数包含一个通过 Retrofit 从服务器检索 Syncer 对象的协程。。李>

对于移动用户不会产生任何可观察到的差异,但是服务器接收到两次请求,对服务器和网络造成负担并不理想。我在代码中打印了一些内容,实际上,Retrofit 调用被执行了两次——代码是这样流动的:

    进入 HomeActivity (CP_1), 进入assignSyncer() (CP_2), 启动协程 (CP_3), getInstance() 被调用 (CP_4), 并创建实例 (CP_5) 并进行 REST API 调用。

但紧接着,又一次,

    CP_2, CP_3, 和 CP_4 都通过了。

正确接收 Syncer 对象并将其集成到可组合对象中,即使此过程发生两次。

所以,是不是我做错了什么?

以下是一些相关代码:


使用 Compose 的 HomeActivity:

class HomeActivity() : ComponentActivity() 

    private val homeViewModel by viewModels<HomeViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)
        println("CP_1")
        setContent 
            AppyTheme 
                HomeFrame(syncer = homeViewModel.receivedSyncer)
                homeViewModel.assignSyncer()
            
        
    
    
    @Composable fun HomeFrame(syncer: Syncer) 
    /* ... */


HomeViewModel:

class HomeViewModel : ViewModel() 

    var receivedSyncer: Syncer by mutableStateOf(Syncer()) // Syncer() - initialises an empty Syncer object with default values for its fields

    var connectionError: Boolean by mutableStateOf(false)

    fun assignSyncer() 
        println("CP_2")
        viewModelScope.launch 
            try 
                println("CP_3")
                val api = APIService.getInstance()
                receivedSyncer = api.requestSyncer(0L, 0L)
             catch (e: Exception) 
                println("CP_EXC - $e.message")
                connectionError = true
            
        
    


接收 Syncer 和 Retrofit 实例的 Retrofit 调用:

interface APIService 

    @GET("base/sync/generate")
    suspend fun requestSyncer(
        @Query("fir-id") firstId: Long,
        @Query("sec-id") secondId: Long
    ): Syncer

    companion object 

        private const val BASE_URL = "http://192.168.1.4:8080/"

        private var apiService: APIService? = null

        fun getInstance(): APIService 
            println("CP_4")
            if (apiService == null) 
                println("CP_5")
                val gson = GsonBuilder().setLenient().create()
                val okHttpClient = OkHttpClient
                    .Builder()
                    .readTimeout(15, TimeUnit.SECONDS)
                    .connectTimeout(10, TimeUnit.SECONDS)
                    .build()
                apiService = Retrofit
                    .Builder()
                    .baseUrl(BASE_URL)
                    .client(okHttpClient)
                    .addConverterFactory(GsonConverterFactory.create(gson))
                    .build()
                    .create(APIService::class.java)
            
            return apiService!!
        
    


AndroidManifest 文件:

<!-- ... -->
<activity
    android:name=".ui.home.HomeActivity"
    android:exported="true"
    android:theme="@style/Theme.Appy.NoActionBar" >
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
<!-- ... -->

谢谢!

【问题讨论】:

【参考方案1】:

您对homeViewModel.assignSyncer() 的调用是作曲的一部分。因此,只要有重组,就会调用这个函数。对于这样的side effects,您应该使用适当的效果处理程序。就像这里你只想调用这个函数一次,这样你就可以使用LaunchedEffect(有关这些函数的详细信息,请参阅链接文档)。

setContent 
    AppyTheme 
        HomeFrame(syncer = homeViewModel.receivedSyncer)
        LaunchedEffect(Unit) 
            homeViewModel.assignSyncer()
        
    

但是这段代码还有一个问题,就是每次配置更改后都会调用assignSyncer。调用此函数的最佳位置可能是 HomeViewModel 的 init 块。

【讨论】:

非常感谢!添加 LaunchedEffect() 函数解决了这个问题。虽然我还没有写一个 init 块,但是如果重复调用会回来,我会实现它。【参考方案2】:

简而言之:永远不要这样做。

Compose 函数必须没有副作用。多次调用它们应该不会引起问题。您不应该仅仅通过绘制可组合来进行 API 调用。查看相关文档:Side-effects in Compose

可组合物可以重新组合(重绘)。这会导致您的应用多次调用assignSyncer()

【讨论】:

感谢您的回答!我只是模糊地掌握 Compose 中的 UI 元素在任何更改时都会重新渲染,但我认为此功能不会导致不需要的效果。

以上是关于Kotlin 协程在 Compose 函数中调用了两次而不是一次的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin 协程协程的挂起和恢复 ① ( 协程的挂起和恢复概念 | 协程的 suspend 挂起函数 )

Kotlin 协程协程的挂起和恢复 ① ( 协程的挂起和恢复概念 | 协程的 suspend 挂起函数 )

如何在主函数中恢复协程的执行?

kotlin协程

如何使用协程在android中每1秒检查一次GPS可用性

史上最详Android版kotlin协程入门进阶实战