Edoardo
Edoardo

Reputation: 21

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

Upvotes: 1

Views: 52

Answers (0)

Related Questions