Merge branch 'display-pokemon-as-images' into 'master'

Display pokemon as images

See merge request ti/2023-2024/s4/mobile-security/students/joren-schipman/pokesearch!1
This commit is contained in:
Schipman Joren 2024-04-29 16:01:20 +00:00
commit f6a8ba6133
5 changed files with 159 additions and 51 deletions

View File

@ -66,6 +66,7 @@ dependencies {
debugImplementation(libs.androidx.ui.test.manifest)
implementation("androidx.navigation:navigation-compose:$nav_version")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.retrofit2:converter-gson:2.11.0")
implementation("io.coil-kt:coil-compose:2.6.0")
}

View File

@ -0,0 +1,39 @@
package com.ti.mobpo.model
import com.google.gson.annotations.SerializedName
data class PokemonSpecies(
@SerializedName("name") val name: String,
@SerializedName("url") val url: String
)
data class PokemonResponse(
@SerializedName("results") val results: List<PokemonSpecies>
)
data class PokemonDetails(
val id: Int,
val name: String,
val types: List<Type>,
@SerializedName("sprites") val sprites: Sprites
)
data class Type(
@SerializedName("type") val typeName: TypeName
)
data class TypeName(
@SerializedName("name") val name: String
)
data class Sprites(
@SerializedName("other") val other: Other
)
data class Other(
@SerializedName("official-artwork") val officialArtwork: OfficialArtwork
)
data class OfficialArtwork(
@SerializedName("front_default") val frontDefault: String
)

View File

@ -0,0 +1,29 @@
package com.ti.mobpo.network
import com.ti.mobpo.model.PokemonDetails
import com.ti.mobpo.model.PokemonResponse
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Path
private const val BASE_URL = "https://pokeapi.co/api/v2/"
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
interface PokeApiService {
@GET("pokemon/?offset=0&limit=2000")
suspend fun getPokemon(): PokemonResponse
@GET("pokemon/{id}")
suspend fun getPokemonDetails(@Path("id") id: Int): PokemonDetails
}
object PokeApi {
val retrofitService : PokeApiService by lazy {
retrofit.create(PokeApiService::class.java)
}
}

View File

@ -2,68 +2,41 @@ package com.ti.mobpo.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.gson.annotations.SerializedName
import com.ti.mobpo.model.PokemonDetails
import com.ti.mobpo.network.PokeApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import java.io.IOException
private const val BASE_URL = "https://pokeapi.co/api/v2/";
private const val SHOW_LIMIT = 20;
data class PokemonSpecies(
@SerializedName("name") val name: String,
@SerializedName("url") val url: String
)
data class PokemonSpeciesResponse(
@SerializedName("results") val results: List<PokemonSpecies>
)
interface PokeApiService {
@GET("pokemon-species/?offset=0&limit=1025")
suspend fun getPokemonSpecies(): PokemonSpeciesResponse
}
class PokeSearchViewModel : ViewModel() {
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
private val service = PokeApi.retrofitService;
private val service = retrofit.create(PokeApiService::class.java)
private val _pokemonDetails = MutableStateFlow<List<PokemonDetails>?>(null)
val pokemonDetails: StateFlow<List<PokemonDetails>?> = _pokemonDetails.asStateFlow()
private val _initialPokemonList = MutableStateFlow<List<PokemonSpecies>?>(null)
private val _filteredPokemonList = MutableStateFlow<List<PokemonSpecies>?>(null)
val pokemonList: StateFlow<List<PokemonSpecies>?> = _filteredPokemonList.asStateFlow()
init {
fetchPokemonSpecies()
}
private fun fetchPokemonSpecies() {
fun search(query: String) {
viewModelScope.launch {
try {
val response = service.getPokemonSpecies()
_initialPokemonList.value = response.results
_filteredPokemonList.value = listOf(PokemonSpecies("Please enter a search term", ""));
val response = service.getPokemon()
val filteredList = response.results.filter { it.name.contains(query, ignoreCase = true) }
val detailsList = mutableListOf<PokemonDetails>()
for (pokemonSpecies in filteredList) {
val details = service.getPokemonDetails(extractPokemonId(pokemonSpecies.url))
detailsList.add(details)
}
_pokemonDetails.value = detailsList
} catch (e: IOException) {
/*TODO*/
/* Handle error */
}
}
}
fun search(query: String) {
val initialPokemonList = _initialPokemonList.value
if (query.isNotBlank() && initialPokemonList != null) {
val filteredList = initialPokemonList.filter { it.name.contains(query, ignoreCase = true) }
_filteredPokemonList.value = filteredList.take(SHOW_LIMIT);
} else {
_filteredPokemonList.value = listOf(PokemonSpecies("Please enter a search term", ""));
}
private fun extractPokemonId(url: String): Int {
val parts = url.split("/")
return parts[parts.size - 2].toInt()
}
}
}

View File

@ -1,24 +1,41 @@
package com.ti.mobpo.ui.screens
import androidx.compose.foundation.Image
import com.ti.mobpo.ui.PokeSearchViewModel
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.ti.mobpo.R
import com.ti.mobpo.ui.SearchBar
import androidx.lifecycle.viewmodel.compose.viewModel
import com.ti.mobpo.model.PokemonDetails
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.ti.mobpo.ui.theme.MobileSecurityTheme
@Composable
fun PokeSearchScreen(pokeSearchViewModel: PokeSearchViewModel) {
@ -27,7 +44,7 @@ fun PokeSearchScreen(pokeSearchViewModel: PokeSearchViewModel) {
@Composable
fun PokeSearch(pokeSearchViewModel: PokeSearchViewModel) {
val searchResults by pokeSearchViewModel.pokemonList.collectAsState()
val searchResults by pokeSearchViewModel.pokemonDetails.collectAsState()
Column(
verticalArrangement = Arrangement.Top,
@ -44,8 +61,13 @@ fun PokeSearch(pokeSearchViewModel: PokeSearchViewModel) {
searchResults?.let { results ->
if (results.isNotEmpty()) {
results.forEach { pokemon ->
Text(text = pokemon.name)
LazyVerticalGrid(
columns = GridCells.Adaptive(150.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
items(items = results, key = { pokemon -> pokemon.id }) { pokemon ->
PokemonCard(pokemon)
}
}
} else {
Text("No results found")
@ -54,9 +76,53 @@ fun PokeSearch(pokeSearchViewModel: PokeSearchViewModel) {
}
}
@Composable
fun PokemonCard(pokemon: PokemonDetails) {
Card(
shape = MaterialTheme.shapes.medium,
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
modifier = Modifier
.padding(4.dp)
.fillMaxWidth()
.aspectRatio(1f)
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Bottom
) {
AsyncImage(
model = ImageRequest.Builder(context = LocalContext.current).data(pokemon.sprites.other.officialArtwork.frontDefault)
.crossfade(true).build(),
contentDescription = pokemon.name,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.weight(1f)
)
Text(
text = pokemon.name.replace("-", " "),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 4.dp) // Adjust padding as needed
.wrapContentSize(Alignment.Center)
)
}
}
}
@Preview(showBackground = true)
@Composable
fun PokeSearchApp(){
val pokeSearchViewModel: PokeSearchViewModel = viewModel()
PokeSearchScreen(pokeSearchViewModel)
}
@Preview(showBackground = true)
@Composable
fun PhotosGridScreenPreview() {
MobileSecurityTheme {
}
}