Reputation: 21
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