I am developing an Android app with Kotlin, Jetpack Compose - for UI, and Retrofit - for making requests to a REST API server made by me. I am a beginner at Kotlin Coroutines, Compose and Retrofit and I am facing the following issue:
- immediately after the HomeActivity starts, the assignSyncer() function in the HomeViewModel, which contains a coroutine that retrieves a Syncer object from the server via Retrofit, is called two times instead of once.
It does not produce any observable difference for the mobile user, but the server receives the request two times, and it is not ideal to burden the server and the network. I put some prints in the code and, indeed, the Retrofit call is executed two times - the code flows this way:
- enters HomeActivity (CP_1),
- enters assignSyncer() (CP_2),
- launches coroutine (CP_3),
- getInstance() is called (CP_4),
- and the instance is created (CP_5) and the REST API call is made.
But immediately after, again,
- CP_2,
- CP_3,
- and CP_4 are gone through.
The Syncer object is received correctly and it is integrated into the composable objects, even though this process happens twice.
Therefore, is it something I am doing wrong?
Here's some relevant code:
The HomeActivity which uses Compose:
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) {
/* ... */
}
The 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
}
}
}
}
The Retrofit call that receives a Syncer and the Retrofit instance:
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!!
}
}
}
The AndroidManifest file:
<!-- ... -->
<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>
<!-- ... -->
Thank you!
CodePudding user response:
Your call to homeViewModel.assignSyncer()
is a part of composition. So whenever there is a recomposition this function gets called. For such side effects, you should use proper effect handlers. Like here you want to call this function only once so you can use LaunchedEffect
(refer linked doc for detailed information about these functions).
setContent {
AppyTheme {
HomeFrame(syncer = homeViewModel.receivedSyncer)
LaunchedEffect(Unit) {
homeViewModel.assignSyncer()
}
}
}
But there is still one problem with this code that it will call assignSyncer
after every configuration change. Probably the best place to call this function is the init
block of HomeViewModel.
CodePudding user response:
In short: Never ever do this.
Compose functions must be side-effect free. Calling them multiple times should not cause problems. You should not make API calls just from drawing a composable. Check out the documentation for this: Side-effects in Compose
Composables can be recompositioned (redrawn). This causes your app to call assignSyncer()
multiple times.