I have created a compose project that follows the standard architecture sttucture of a project. It has a data class, interface, api class, repository. The repository is connected to a viewmodel and this in turn is fetched in a composable. My goal is very simple, I have added some locations in a postgres database, very simple with id, name, lng, lat.
I have used postman and verified that it does fetch the information correctly from an api. But I have run into a problem, it does not display a list of the locations in a composable. Could someone explain to me why that is ?
This is what I get for return:
I have messed around with viewmodel and screen but without success in figuring out the error.
I created a private val:
// Location data
private val _selectedLocations = MutableLiveData<LocationData?>(null)
val selectedLocations: LiveData<LocationData?> = _selectedLocations
I created a new screen that calls Locations:
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun LocationScreens(
locationsHiltViewModel: LocationsHiltViewModel,
) {
var loadingAnim by remember { mutableStateOf(false) }
val locationData = selectedLocations.value
locationData?.let { existingLocation ->
locationsHiltViewModel.locations.collectAsState(initial = NetworkResult.Loading).value.let { response ->
when(response) {
is NetworkResult.Loading -> {
loadingAnim = true
}
is NetworkResult.Success -> {
loadingAnim = false
Locations(
locationData = LocationData()
)
}
is NetworkResult.Error -> {
loadingAnim = false
Toast.makeText(
LocalContext.current,
response.message,
Toast.LENGTH_LONG
).show()
}
}
}
LoadingAnim(loadingAnim)
} ?: run {
NoLocationsMatch()
}
}
LocationsHiltViewModel: Fetches all the data from Repo!
@HiltViewModel
class LocationsHiltViewModel @Inject constructor(
private val repo: Repo
) : ViewModel() {
private val _locations = MutableStateFlow<NetworkResult<List<LocationData>>>(NetworkResult.Loading)
val locations : StateFlow<NetworkResult<List<LocationData>>> get() = _locations
init {
fetchLocations()
}
fun fetchLocations() {
viewModelScope.launch {
try {
_locations.value = NetworkResult.Loading
val response = repo.getLocationData()
if (response.isSuccessful) {
response.body()?.let { locations ->
_locations.value = NetworkResult.Success(locations)
} ?: run {
_locations.value = NetworkResult.Success(listOf())
Log.d("Locations 1", "Response body is null")
}
} else {
_locations.value = NetworkResult.Error(response.message())
Log.d("Locations 2", "Error: ${response.code()} ${response.message()}")
}
} catch (e: Exception) {
_locations.value = NetworkResult.Error(e.message.toString())
Log.d("Location 3", "Exception: ${e.message.toString()}")
}
}
}
}
LocationScreen:
@Composable
fun Locations(
locationData: LocationData?
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(Color.Black)
.padding(top = 30.dp)
) {
Column(
modifier = Modifier
.padding(10.dp)
.fillMaxWidth()
.background(Color.Black)
) {
LazyVerticalGrid(
columns = GridCells.Fixed(2), // 2 columns in the grid
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(8.dp)
) {
locationData?.let { location ->
items(3) {
LocationItems(
id = location.id,
locationName = location.locationname,
lng= location.lng,
lat= location.lat
)
}
}
}
}
}
}
}
@Composable
private fun LocationItems(
id: Int?,
locationName: String?,
lng: Double?,
lat: Double?,
) {
Box(
modifier = Modifier
.padding(10.dp)
.fillMaxWidth()
.background(Color.Black, shape = RoundedCornerShape(12.dp))
.padding(10.dp),
contentAlignment = Alignment.CenterStart
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.Start
) {
id?.let { Text(text = "ID: $it") }
locationName?.let { Text(text = "Location Name: $it") }
lng?.let { Text(text = "lng: $it") }
lat?.let { Text(text = "lat: $it") }
}
}
}
Assuming No locations Found comes from the NoLocationsMatch
composable the issue becomes clear when you look at the condition when it is displayed: locationData
must be null. Now, that starts out as null, and since it is never changed, nothing else will ever be displayed than NoLocationsMatch()
.
If you don't want that then remove it. I don't see why you even need selectedLocations
, and even if there is a reason it shouldn't be a LiveData, it should be a MutableState (when it is located in a composable) or a MutableStateFlow (when it is located in the view model). LiveData isn't needed anymore when using Compose.
Now you wouldn't see No locations Found anymore, instead you would see a black screen. That is because, well, you painted everything black. I don't know what the reason is for that, but just for the purpose of actually seeing something, just switch the color in LocationItems from Color.Black
to Color.White
.
But even though you can see something now, that isn't the locations retrieved from the view model because you simply throw them away the moment you retrieve them. Instead of passing them to your Locations
composable, you create a new, empty LocationData()
object and pass that instead. What you really want is to pass the content of response
. Which might pose as the next problem since that contains a List<LocationData>
where the Locations
composable - despite its plural name - only accepts a single LocationData object.
Well, you can go from there. The initial problem that you couldn't see the locations from the view model should be solved now.