diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 360f9ab..796bb03 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") } \ No newline at end of file diff --git a/app/src/main/java/com/ti/mobpo/model/PokeModels.kt b/app/src/main/java/com/ti/mobpo/model/PokeModels.kt new file mode 100644 index 0000000..81c8799 --- /dev/null +++ b/app/src/main/java/com/ti/mobpo/model/PokeModels.kt @@ -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 +) + +data class PokemonDetails( + val id: Int, + val name: String, + val types: List, + @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 +) \ No newline at end of file diff --git a/app/src/main/java/com/ti/mobpo/network/PokeApiService.kt b/app/src/main/java/com/ti/mobpo/network/PokeApiService.kt new file mode 100644 index 0000000..e5ecb3a --- /dev/null +++ b/app/src/main/java/com/ti/mobpo/network/PokeApiService.kt @@ -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) + } +} diff --git a/app/src/main/java/com/ti/mobpo/ui/PokeSearchViewModel.kt b/app/src/main/java/com/ti/mobpo/ui/PokeSearchViewModel.kt index 3129be2..a36b747 100644 --- a/app/src/main/java/com/ti/mobpo/ui/PokeSearchViewModel.kt +++ b/app/src/main/java/com/ti/mobpo/ui/PokeSearchViewModel.kt @@ -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 -) - -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?>(null) + val pokemonDetails: StateFlow?> = _pokemonDetails.asStateFlow() - private val _initialPokemonList = MutableStateFlow?>(null) - private val _filteredPokemonList = MutableStateFlow?>(null) - - val pokemonList: StateFlow?> = _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() + 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() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/ti/mobpo/ui/screens/PokeSearch.kt b/app/src/main/java/com/ti/mobpo/ui/screens/PokeSearch.kt index f7e3a02..cffe645 100644 --- a/app/src/main/java/com/ti/mobpo/ui/screens/PokeSearch.kt +++ b/app/src/main/java/com/ti/mobpo/ui/screens/PokeSearch.kt @@ -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 { + } } \ No newline at end of file