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:
		@@ -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")
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										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.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 _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*/
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    private val _pokemonDetails = MutableStateFlow<List<PokemonDetails>?>(null)
 | 
			
		||||
    val pokemonDetails: StateFlow<List<PokemonDetails>?> = _pokemonDetails.asStateFlow()
 | 
			
		||||
 | 
			
		||||
    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", ""));
 | 
			
		||||
        viewModelScope.launch {
 | 
			
		||||
            try {
 | 
			
		||||
                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) {
 | 
			
		||||
                /* 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
 | 
			
		||||
 | 
			
		||||
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 {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user