Kotlin 协程在 Compose 函数中调用了两次而不是一次
Kotlin coroutine called two times instead of once in Compose function
我正在使用 Kotlin 开发一个 Android 应用程序,Jetpack Compose - UI,以及 Retrofit - 用于向我创建的 REST API 服务器发出请求。我是 Kotlin Coroutines、Compose 和 Retrofit 的初学者,我面临以下问题:
- 在 HomeActivity 启动后,HomeViewModel 中的 assignSyncer() 函数立即被调用 两次,而不是一次[=50],其中包含一个通过 Retrofit 从服务器检索同步对象的协程=].
对于移动端用户来说并没有产生任何可观察到的差异,但是服务器接收了两次请求,对服务器和网络造成负担并不理想。我在代码中打印了一些内容,实际上,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!!
}
}
}
Android清单文件:
<!-- ... -->
<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>
<!-- ... -->
谢谢!
您对 homeViewModel.assignSyncer()
的调用是合成的一部分。所以只要有重组,这个函数就会被调用。对于这样的 side effects,您应该使用适当的效果处理程序。像这里一样,您只想调用此函数一次,这样您就可以使用 LaunchedEffect
(有关这些函数的详细信息,请参阅链接文档)。
setContent {
AppyTheme {
HomeFrame(syncer = homeViewModel.receivedSyncer)
LaunchedEffect(Unit) {
homeViewModel.assignSyncer()
}
}
}
但是这段代码还有一个问题,它会在每次配置更改后调用 assignSyncer
。调用此函数的最佳位置可能是 HomeViewModel 的 init
块。
简而言之:永远不要这样做。
Compose 函数必须没有副作用。多次调用它们应该不会引起问题。您不应仅通过绘制可组合项进行 API 调用。查看文档:Side-effects in Compose
可组合项可以重新组合(重新绘制)。这会导致您的应用多次调用 assignSyncer()
。
我正在使用 Kotlin 开发一个 Android 应用程序,Jetpack Compose - UI,以及 Retrofit - 用于向我创建的 REST API 服务器发出请求。我是 Kotlin Coroutines、Compose 和 Retrofit 的初学者,我面临以下问题:
- 在 HomeActivity 启动后,HomeViewModel 中的 assignSyncer() 函数立即被调用 两次,而不是一次[=50],其中包含一个通过 Retrofit 从服务器检索同步对象的协程=].
对于移动端用户来说并没有产生任何可观察到的差异,但是服务器接收了两次请求,对服务器和网络造成负担并不理想。我在代码中打印了一些内容,实际上,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!!
}
}
}
Android清单文件:
<!-- ... -->
<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>
<!-- ... -->
谢谢!
您对 homeViewModel.assignSyncer()
的调用是合成的一部分。所以只要有重组,这个函数就会被调用。对于这样的 side effects,您应该使用适当的效果处理程序。像这里一样,您只想调用此函数一次,这样您就可以使用 LaunchedEffect
(有关这些函数的详细信息,请参阅链接文档)。
setContent {
AppyTheme {
HomeFrame(syncer = homeViewModel.receivedSyncer)
LaunchedEffect(Unit) {
homeViewModel.assignSyncer()
}
}
}
但是这段代码还有一个问题,它会在每次配置更改后调用 assignSyncer
。调用此函数的最佳位置可能是 HomeViewModel 的 init
块。
简而言之:永远不要这样做。
Compose 函数必须没有副作用。多次调用它们应该不会引起问题。您不应仅通过绘制可组合项进行 API 调用。查看文档:Side-effects in Compose
可组合项可以重新组合(重新绘制)。这会导致您的应用多次调用 assignSyncer()
。