API calls in Kotlin Jetpack Compose Android Studio

I’m developing an app in Kotlin with Jetpack Compose in Android Studio, using the MVVM pattern.
The home page of the app takes a long time to load because it contains several API calls, I would like to be able to make the code more fluid by optimizing the calls.
Could someone help me by inserting a correct loading indicator and/or modifying the code regarding the API calls (more precisely those to oggetti and utenti).
Thanks

I’ll show you the files related to the issue:

HomeScreen.kt

@Composable
fun HomeScreen(
    navController: NavHostController,
    viewModel: HomeViewModel = viewModel()
) {
    // inizializzazione del viewModel
    val context = LocalContext.current
    LaunchedEffect(Unit) {
        viewModel.initialize(context)
    }

    /*
    cleanup del viewModel quando il composable viene smontato
    permette di interrompere gli aggiornamenti di posizione e le chiamate di rete
    */
    DisposableEffect(Unit) {
        onDispose {
            viewModel.stopLocationUpdates()
            viewModel.clear()
        }
    }

    // listener per i cambiamenti di destinazione
    DisposableEffect(navController) {
        val listener = NavController.OnDestinationChangedListener { _, destination, _ ->
            if (destination.route != "home") {
                viewModel.stopLocationUpdates()
            } else {
                viewModel.startLocationUpdates()
            }
        }
        navController.addOnDestinationChangedListener(listener)
        onDispose {
            navController.removeOnDestinationChangedListener(listener)
        }
    }

    val oggetti = viewModel.oggettiVirtuali.collectAsState().value
    val oggettiJson = Gson().toJson(oggetti)

    // raggio d'azione
    val raggioAzione by viewModel.raggioAzione.collectAsState()
    Log.d("HomeScreen", "raggioAzione: $raggioAzione")

    // punti vita e punti esperienza dell'utente
    val puntiVita by viewModel.puntiVita.collectAsState()
    val puntiEsperienza by viewModel.puntiEsperienza.collectAsState()

    val isLoading by viewModel.isLoading.collectAsState()

    Box(
        modifier = Modifier
            .fillMaxSize()
        // .padding(top = 45.dp, bottom = 15.dp),
    ) {

        if (isLoading) {
            CircularProgressIndicator(
                modifier = Modifier
                    .align(Alignment.Center)
                    .size(50.dp)
            )
        } else {
            MapScreen(navController, viewModel, raggioAzione)

            IconButton(
                onClick = {
                    navController.navigate("listaOggetti/$oggettiJson/$raggioAzione")
                    true
                },
                modifier = Modifier
                    .align(Alignment.TopCenter)
                    .padding(top = 45.dp)
                    .size(100.dp)
            ) {
                Icon(
                    painter = painterResource(R.drawable.oggettivicini),
                    contentDescription = "Lista Oggetti",
                    tint = Color.Unspecified,
                    modifier = Modifier.size(80.dp)
                )
            }

            IconButton(
                onClick = { navController.navigate(Routes.Classifica) },
                modifier = Modifier
                    .align(Alignment.BottomStart)
                    .padding(bottom = 140.dp, start = 16.dp)
                    .size(100.dp)
            ) {
                Icon(
                    painter = painterResource(R.drawable.classifica),
                    contentDescription = "Classifica",
                    tint = Color.Unspecified,
                    modifier = Modifier.size(80.dp)
                )
            }

            IconButton(
                onClick = { navController.navigate(Routes.Profilo) },
                modifier = Modifier
                    .align(Alignment.BottomEnd)
                    .padding(bottom = 180.dp, end = 16.dp)
                    .size(100.dp)
            ) {
                Icon(
                    painter = painterResource(R.drawable.user),
                    contentDescription = "Profilo",
                    tint = Color.Unspecified,
                    modifier = Modifier.size(60.dp)
                )
            }

            Column(
                modifier = Modifier
                    .align(Alignment.BottomEnd)
                    .padding(bottom = 100.dp, end = 16.dp)
            ) {
                Row(
                    verticalAlignment = Alignment.CenterVertically,
                    modifier = Modifier.padding(top = 8.dp, bottom = 4.dp)
                ) {
                    Icon(
                        painter = painterResource(R.drawable.life),
                        contentDescription = "Punti vita",
                        tint = Color.Unspecified,
                        modifier = Modifier.size(40.dp)
                    )
                    Text(
                        text = puntiVita.toString(),
                        fontSize = 18.sp,
                        fontWeight = androidx.compose.ui.text.font.FontWeight.Bold,
                        color = Color.Black,
                        modifier = Modifier.padding(start = 8.dp)
                    )
                }

                Row(
                    verticalAlignment = Alignment.CenterVertically
                ) {
                    Icon(
                        painter = painterResource(R.drawable.experience),
                        contentDescription = "Punti esperienza",
                        tint = Color.Unspecified,
                        modifier = Modifier.size(40.dp)
                    )
                    Text(
                        text = puntiEsperienza.toString(),
                        fontSize = 18.sp,
                        fontWeight = androidx.compose.ui.text.font.FontWeight.Bold,
                        color = Color.Black,
                        modifier = Modifier.padding(start = 8.dp)
                    )
                }
            }
        }

    }

}

@SuppressLint("UnrememberedMutableState")
@Composable
fun MapScreen(
    navController: NavHostController,
    viewModel: HomeViewModel,
    raggioAzione: Int
) {

    // posizione utente
    val posizione by viewModel.posizioneUtente.collectAsState()
    val cameraPositionState = rememberCameraPositionState()

    // oggetti virtuali
    val oggettiVirtuali by viewModel.oggettiVirtuali.collectAsState()

    // utenti vicini
    val utenti by viewModel.utentiVicini.collectAsState()

    LaunchedEffect(posizione, oggettiVirtuali, utenti) {
        if (posizione != null) {
            cameraPositionState.move(CameraUpdateFactory.newLatLngZoom(posizione!!, 20f))
        }
    }

    GoogleMap(
        modifier = Modifier.fillMaxSize(),
        cameraPositionState = cameraPositionState,
        properties = MapProperties(isMyLocationEnabled = true)
    ) {
        posizione?.let {
            Circle(
                center = it,
                radius = raggioAzione.toDouble(),
                fillColor = Color(0x80CCCCCC),
                strokeWidth = 1.0F,
                strokeColor = Color.Red
            )
        }

        oggettiVirtuali?.forEach { oggetto ->
            var position = LatLng(oggetto.lat, oggetto.lon)

            if (oggetto.type == "monster") {
                val icon = resizeMapIcons(LocalContext.current, R.drawable.monster, 120, 120)
                Marker(
                    state = rememberMarkerState(position = position),
                    icon = icon,
                    onClick = {
                        navController.navigate("InfoOggetto/${oggetto.id}")
                        true
                    },
                )
            } else if (oggetto.type == "candy") {
                val icon = resizeMapIcons(LocalContext.current, R.drawable.candy, 120, 120)
                Marker(
                    state = rememberMarkerState(position = position),
                    icon = icon,
                    onClick = {
                        navController.navigate("InfoOggetto/${oggetto.id}")
                        true
                    },
                )
            } else if (oggetto.type == "weapon" || oggetto.type == "amulet" || oggetto.type == "armor") {
                val icon = resizeMapIcons(LocalContext.current, R.drawable.artefatti, 120, 120)
                Marker(
                    state = rememberMarkerState(position = position),
                    icon = icon,
                    onClick = {
                        navController.navigate("InfoOggetto/${oggetto.id}")
                        true
                    },
                )
            }

        }

        utenti?.forEach { utente ->
            var position = LatLng(utente.lat, utente.lon)
            val icon = resizeMapIcons(LocalContext.current, R.drawable.users, 140, 140)
            Marker(
                state = rememberMarkerState(position = position),
                icon = icon,
                onClick = {
                    navController.navigate("profiloGiocatori/${utente.uid}")
                    true
                },
            )
        }
    }
}

private fun resizeMapIcons(
    context: Context,
    iconName: Int,
    width: Int,
    height: Int
): BitmapDescriptor {
    val imageBitmap = BitmapFactory.decodeResource(context.resources, iconName)
    val resizedBitmap = Bitmap.createScaledBitmap(imageBitmap, width, height, false)
    return BitmapDescriptorFactory.fromBitmap(resizedBitmap)
}

/*
@Preview
@Composable
fun HomeScreenPreview() {
    Mostri_da_tascaTheme {
        HomeScreen(
            navController = rememberNavController(),
            viewModel = HomeViewModel()
        )
    }
}
*/

HomeViewModel.kt

class HomeViewModel : ViewModel() {

    // POSIZIONE UTENTE
    private val _posizioneUtente = MutableStateFlow<LatLng?>(null)
    val posizioneUtente: StateFlow<LatLng?> = _posizioneUtente.asStateFlow()
    private lateinit var locationProvider: Posizione

    // OGGETTI VIRTUALI
    private val _oggettiVirtuali = MutableStateFlow<List<GetObjects>>(emptyList())
    val oggettiVirtuali: StateFlow<List<GetObjects>> = _oggettiVirtuali.asStateFlow()

    // UTENTI VICINI
    private val _utentiVicini = MutableStateFlow<List<GetUsers>>(emptyList())
    val utentiVicini: StateFlow<List<GetUsers>> = _utentiVicini.asStateFlow()

    // RAGGIO D'AZIONE
    private val _raggioAzione = MutableStateFlow<Int>(100)
    val raggioAzione: StateFlow<Int> = _raggioAzione.asStateFlow()

    // PUNTI VITA ED ESPERIENZA
    private val _puntiVita = MutableStateFlow<Int>(100)
    val puntiVita: StateFlow<Int> = _puntiVita.asStateFlow()
    private val _puntiEsperienza = MutableStateFlow<Int>(0)
    val puntiEsperienza: StateFlow<Int> = _puntiEsperienza.asStateFlow()

    // LOADING
    private val _isLoading = MutableStateFlow(false)
    val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()

    private var updateJob: Job? = null

    /*
    Quando la componente viene montata:
        - inizializzazione del LocationProvider e avvio degli aggiornamenti di posizione
        - verifica e caricamento delle credenziali
    */
    fun initialize(context: Context) {
        locationProvider = Posizione(context.applicationContext)
        locationProvider.setLocationUpdateListener { location ->
            updateJob?.cancel()  // cancella eventuali job precedenti

            updateJob = viewModelScope.launch {
                _isLoading.value = true // inizio caricamento
                _posizioneUtente.value = LatLng(location.latitude, location.longitude)

                val sid = Credenziali(context).getCredenziali().first.toString()
                val lat = location.latitude
                val lon = location.longitude

                launch {
                    oggetti(
                        context,
                        sid,
                        lat,
                        lon
                    )
                }

                launch {
                    utenti(
                        sid,
                        lat,
                        lon
                    )
                }

                _isLoading.value = false // fine caricamento

                // imposto il raggio d'azione (bottoneAttivazione restituisce una coppia con il flag [true o false] e il raggio [Int])
                if (_oggettiVirtuali.value.isNotEmpty()) {
                    var (_, raggio) = Posizione(context).bottoneAttivazione(
                        _oggettiVirtuali.value.first().lat,
                        _oggettiVirtuali.value.first().lon
                    )
                    _raggioAzione.value = raggio.toInt()
                    Log.d("HomeViewModel", "Raggio d'azione aggiornato: ${_raggioAzione.value}")
                }

                // aggiorno vita ed esperienza dell'utente
                aggiornaVitaEdEsperienza(
                    sid, // Credenziali(context).getCredenziali().first.toString(),
                    Credenziali(context).getCredenziali().second!!.toInt(),
                )
            }
        }
        startLocationUpdates()

        // registrazione dell'utente
        val credenziali = Credenziali(context)
        credenziali.verificaECaricaCredenziali(context)
    }

    // chiamata getObjects e aggiornamento della variabile di stato
    private suspend fun oggetti(context: Context, sid: String, lat: Double, lon: Double) {
        withContext(Dispatchers.IO) {
            try {
                val oggettiRicevuti = Api.api.getObjects(sid, lat, lon)
                _oggettiVirtuali.value = oggettiRicevuti

                // aggiorno gli oggetti nello storage
                val storage = Storage(context)
                storage.aggiornaOggettiDB(oggettiRicevuti, Api.api)

                Log.d(
                    "HomeViewModel",
                    "getObjects - Oggetti virtuali ricevuti: $oggettiRicevuti"
                )
            } catch (e: Exception) {
                Log.d(
                    "HomeViewModel",
                    "getObjects - Errore durante il caricamento degli oggetti: $e"
                )
            }

        }
    }

    // chiamata getUsers e aggiornamento della variabile di stato
    private suspend fun utenti(sid: String, lat: Double, lon: Double) {
        withContext(Dispatchers.IO) {
            try {
                val utentiRicevuti = Api.api.getUsers(sid, lat, lon)
                _utentiVicini.value = utentiRicevuti

                Log.d(
                    "HomeViewModel",
                    "getUsers - Utenti vicini ricevuti: $utentiRicevuti"
                )
            } catch (e: Exception) {
                Log.d(
                    "HomeViewModel",
                    "getUsers - Errore durante il caricamento degli utenti: $e"
                )
            }
        }
    }

    private suspend fun aggiornaVitaEdEsperienza(sid: String, uid: Int) {
        val utente = Api.api.getUsersID(uid, sid)

        _puntiVita.value = utente.life
        _puntiEsperienza.value = utente.experience
    }

    fun startLocationUpdates() {
        if (::locationProvider.isInitialized && locationProvider.checkPermissions()) {
            locationProvider.startLocationUpdates()
        } else { // permessi non concessi
            Log.d(
                "HomeViewModel",
                "startLocationUpdates - Location provider non inizializzato o permessi non concessi"
            )
        }
    }

    fun stopLocationUpdates() {
        Log.d("HomeViewModel", "Interruzione aggiornamenti della posizione")
        try {
            if (::locationProvider.isInitialized) {
                locationProvider.stopLocationUpdates()
            }
        } catch (e: Exception) {
            Log.d(
                "HomeViewModel",
                "Errore durante l'interruzione degli aggiornamenti della posizione: ${e.message}"
            )
        }
    }

    // funzione di pulizia per interrompere le operazioni in corso
    fun clear() {
        stopLocationUpdates()
        updateJob?.cancel()
    }

    override fun onCleared() {
        super.onCleared()
        clear()
    }

}

In case it can be useful I’ll also show you the Posizione.kt file:

class Posizione(private val context: Context) {

    // FusedLocationProviderClient è utilizzato per ottenere aggiornamenti sulla posizione
    var fusedLocationProviderClient: FusedLocationProviderClient =
        LocationServices.getFusedLocationProviderClient(context)

    // callback che riceve gli aggiornamenti della posizione
    private lateinit var locationCallback: LocationCallback

    // configurazione delle richieste di posizione
    private var locationRequest: LocationRequest = LocationRequest.create().apply {
        interval = 10000 // aggiornamento posizione ogni 10 secondi
        fastestInterval = 5000 // intervallo più veloce tra gli aggiornamenti di posizione
        priority = LocationRequest.PRIORITY_HIGH_ACCURACY // alta precisione
    }

    // gestisce gli aggiornamenti di posizione
    private var locationUpdateListener: ((Location) -> Unit)? = null

    init {
        setupLocationCallback() // inizializza il callback della posizione
    }

    // configura il callback che gestisce gli aggiornamenti della posizione
    private fun setupLocationCallback() {
        locationCallback = object : LocationCallback() {
            override fun onLocationResult(locationResult: LocationResult) {
                super.onLocationResult(locationResult)
                for (location in locationResult.locations) {
                    Log.d(
                        "POSIZIONE",
                        "Posizione attuale: lat ${location.latitude}, lon ${location.longitude}"
                    )
                    locationUpdateListener?.invoke(location)
                }
            }
        }
        Log.d("POSIZIONE", "Callback di posizione configurato")
    }

    fun setLocationUpdateListener(update: (Location) -> Unit) {
        locationUpdateListener = update
    }

    // controlla se i permessi di posizione sono stati concessi
    fun checkPermissions(): Boolean {
        val fineLocationPermission =
            ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)
        val coarseLocationPermission =
            ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION)
        val permissionsGranted =
            fineLocationPermission == PackageManager.PERMISSION_GRANTED && coarseLocationPermission == PackageManager.PERMISSION_GRANTED

        Log.d("POSIZIONE", "Permessi di localizzazione concessi: $permissionsGranted")
        return permissionsGranted
    }

    // Avvia gli aggiornamenti della posizione se i permessi sono stati concessi
    fun startLocationUpdates() {
        if (!checkPermissions()) {
            Log.d("POSIZIONE", "Permessi di localizzazione non concessi, richiesta in corso...")
            return
        }
        startLocationUpdatesWithPermission()
    }

    @SuppressLint("MissingPermission")
    fun startLocationUpdatesWithPermission() {
        Log.d("POSIZIONE", "Inizio aggiornamenti della posizione con permessi concessi")
        try {
            fusedLocationProviderClient.requestLocationUpdates(
                locationRequest,
                locationCallback,
                Looper.getMainLooper()
            )
        } catch (e: Exception) {
            Log.e("POSIZIONE", "Errore durante la richiesta degli aggiornamenti della posizione", e)
        }
    }

    // Interrompe gli aggiornamenti della posizione
    fun stopLocationUpdates() {
        Log.d("POSIZIONE", "Interruzione aggiornamenti della posizione")
        try {
            fusedLocationProviderClient.removeLocationUpdates(locationCallback)
            Log.d("POSIZIONE", "Callback rimosso per gli aggiornamenti della posizione")
        } catch (e: Exception) {
            Log.e("POSIZIONE", "Errore durante la rimozione degli aggiornamenti della posizione", e)
        }
    }

    // controlla la visualizzazione o meno del pulsante di attivazione dell'oggetto (in InfoOggettoScreen) e restituisce il raggio di attivazione (utilizzato nella HomeScreen ma recuperato da HomeViewModel)
    @SuppressLint("MissingPermission")
    suspend fun bottoneAttivazione(
        oggettoLat: Double,
        oggettoLon: Double
    ): Pair<Boolean, Double> {

        var raggio: Double = 100.0

        // posizione attuale del dispositivo (è una chiamata asincrona)
        val posizioneUtente = withContext(Dispatchers.IO) {
            fusedLocationProviderClient.lastLocation.await()
        }
        if (posizioneUtente != null) {
            Log.d(
                "Bottone Attivazione",
                "Posizione utente: lat ${posizioneUtente.latitude}, lon ${posizioneUtente.longitude}"
            )
        } else {
            Log.d("Bottone Attivazione", "Posizione utente nulla")
        }

        // distanza tra utente e oggetto
        val calcoloDistanza = FloatArray(1)
        if (posizioneUtente != null) {
            Location.distanceBetween(
                posizioneUtente.latitude,
                posizioneUtente.longitude,
                oggettoLat,
                oggettoLon,
                calcoloDistanza
            )
        }
        val distanza = calcoloDistanza[0]
        Log.d("Bottone Attivazione", "Distanza tra oggetto e utente: $distanza")

        val sid =
            Credenziali(context).getCredenziali().first.toString() // recupero il sid dal database
        val uid =
            Credenziali(context).getCredenziali().second?.toInt() // recupero l'uid dal database
        val utente = Api.api.getUsersID(uid!!, sid) // recupero le informazioni dell'utente

        if (utente.amulet != null) {
            Log.d("Bottone Attivazione", "Amuleto equipaggiato")

            val amuleto =
                Api.api.getObjectsID(utente.amulet, sid) // recupero le informazioni dell'amuleto
            Log.d("Bottone Attivazione", "Info amuleto: $amuleto")

            if (amuleto.level != null) {
                raggio *= (1 + (amuleto.level / 100.0)) // aumento il raggio di attivazione dell'amuleto (secondo la proporzione)
            }
        } else { // amuleto non equipaggiato (il raggio rimane 100)
            Log.d("Bottone Attivazione", "Amuleto non equipaggiato")
        }

        Log.d("Bottone Attivazione", "Raggio di attivazione: $raggio")
        Log.d("Bottone Attivazione", "distanza <= raggio: ${distanza <= raggio}")

        return Pair(distanza <= raggio, raggio)

    }

}

I tried to insert the loading indicator but it doesn’t behave as it should, it finishes before the items on the screen are shown

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật