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:
commit
f6a8ba6133
@ -66,6 +66,7 @@ dependencies {
|
|||||||
debugImplementation(libs.androidx.ui.test.manifest)
|
debugImplementation(libs.androidx.ui.test.manifest)
|
||||||
implementation("androidx.navigation:navigation-compose:$nav_version")
|
implementation("androidx.navigation:navigation-compose:$nav_version")
|
||||||
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
|
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
|
||||||
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
implementation("com.squareup.retrofit2:retrofit:2.11.0")
|
||||||
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
|
implementation("com.squareup.retrofit2:converter-gson:2.11.0")
|
||||||
|
implementation("io.coil-kt:coil-compose:2.6.0")
|
||||||
}
|
}
|
39
app/src/main/java/com/ti/mobpo/model/PokeModels.kt
Normal file
39
app/src/main/java/com/ti/mobpo/model/PokeModels.kt
Normal 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
|
||||||
|
)
|
29
app/src/main/java/com/ti/mobpo/network/PokeApiService.kt
Normal file
29
app/src/main/java/com/ti/mobpo/network/PokeApiService.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -2,68 +2,41 @@ package com.ti.mobpo.ui
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import retrofit2.Retrofit
|
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
|
||||||
import retrofit2.http.GET
|
|
||||||
import java.io.IOException
|
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() {
|
class PokeSearchViewModel : ViewModel() {
|
||||||
private val retrofit = Retrofit.Builder()
|
private val service = PokeApi.retrofitService;
|
||||||
.baseUrl(BASE_URL)
|
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
|
||||||
.build()
|
|
||||||
|
|
||||||
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() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
val response = service.getPokemonSpecies()
|
|
||||||
_initialPokemonList.value = response.results
|
|
||||||
_filteredPokemonList.value = listOf(PokemonSpecies("Please enter a search term", ""));
|
|
||||||
} catch (e: IOException) {
|
|
||||||
/*TODO*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun search(query: String) {
|
fun search(query: String) {
|
||||||
val initialPokemonList = _initialPokemonList.value
|
viewModelScope.launch {
|
||||||
if (query.isNotBlank() && initialPokemonList != null) {
|
try {
|
||||||
val filteredList = initialPokemonList.filter { it.name.contains(query, ignoreCase = true) }
|
val response = service.getPokemon()
|
||||||
_filteredPokemonList.value = filteredList.take(SHOW_LIMIT);
|
val filteredList = response.results.filter { it.name.contains(query, ignoreCase = true) }
|
||||||
} else {
|
val detailsList = mutableListOf<PokemonDetails>()
|
||||||
_filteredPokemonList.value = listOf(PokemonSpecies("Please enter a search term", ""));
|
for (pokemonSpecies in filteredList) {
|
||||||
|
val details = service.getPokemonDetails(extractPokemonId(pokemonSpecies.url))
|
||||||
|
detailsList.add(details)
|
||||||
|
}
|
||||||
|
_pokemonDetails.value = detailsList
|
||||||
|
} catch (e: IOException) {
|
||||||
|
/* Handle error */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun extractPokemonId(url: String): Int {
|
||||||
|
val parts = url.split("/")
|
||||||
|
return parts[parts.size - 2].toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,24 +1,41 @@
|
|||||||
package com.ti.mobpo.ui.screens
|
package com.ti.mobpo.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import com.ti.mobpo.ui.PokeSearchViewModel
|
import com.ti.mobpo.ui.PokeSearchViewModel
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
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.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.ti.mobpo.R
|
import com.ti.mobpo.R
|
||||||
import com.ti.mobpo.ui.SearchBar
|
import com.ti.mobpo.ui.SearchBar
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
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
|
@Composable
|
||||||
fun PokeSearchScreen(pokeSearchViewModel: PokeSearchViewModel) {
|
fun PokeSearchScreen(pokeSearchViewModel: PokeSearchViewModel) {
|
||||||
@ -27,7 +44,7 @@ fun PokeSearchScreen(pokeSearchViewModel: PokeSearchViewModel) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PokeSearch(pokeSearchViewModel: PokeSearchViewModel) {
|
fun PokeSearch(pokeSearchViewModel: PokeSearchViewModel) {
|
||||||
val searchResults by pokeSearchViewModel.pokemonList.collectAsState()
|
val searchResults by pokeSearchViewModel.pokemonDetails.collectAsState()
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
@ -44,8 +61,13 @@ fun PokeSearch(pokeSearchViewModel: PokeSearchViewModel) {
|
|||||||
|
|
||||||
searchResults?.let { results ->
|
searchResults?.let { results ->
|
||||||
if (results.isNotEmpty()) {
|
if (results.isNotEmpty()) {
|
||||||
results.forEach { pokemon ->
|
LazyVerticalGrid(
|
||||||
Text(text = pokemon.name)
|
columns = GridCells.Adaptive(150.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
|
) {
|
||||||
|
items(items = results, key = { pokemon -> pokemon.id }) { pokemon ->
|
||||||
|
PokemonCard(pokemon)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Text("No results found")
|
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)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun PokeSearchApp(){
|
fun PokeSearchApp(){
|
||||||
val pokeSearchViewModel: PokeSearchViewModel = viewModel()
|
val pokeSearchViewModel: PokeSearchViewModel = viewModel()
|
||||||
PokeSearchScreen(pokeSearchViewModel)
|
PokeSearchScreen(pokeSearchViewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun PhotosGridScreenPreview() {
|
||||||
|
MobileSecurityTheme {
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user