Merge branch 'addFavourites' into 'master'
Add favourites See merge request ti/2023-2024/s4/mobile-security/students/joren-schipman/pokesearch!2
This commit is contained in:
		@@ -1,6 +1,7 @@
 | 
			
		||||
plugins {
 | 
			
		||||
    alias(libs.plugins.androidApplication)
 | 
			
		||||
    alias(libs.plugins.jetbrainsKotlinAndroid)
 | 
			
		||||
    id("com.google.devtools.ksp") version "1.9.20-1.0.14"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
android {
 | 
			
		||||
@@ -69,4 +70,9 @@ dependencies {
 | 
			
		||||
    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")
 | 
			
		||||
 | 
			
		||||
    implementation("androidx.room:room-runtime:${rootProject.extra["room_version"]}")
 | 
			
		||||
    implementation("androidx.core:core-ktx:1.12.0")
 | 
			
		||||
    ksp("androidx.room:room-compiler:${rootProject.extra["room_version"]}")
 | 
			
		||||
    implementation("androidx.room:room-ktx:${rootProject.extra["room_version"]}")
 | 
			
		||||
}
 | 
			
		||||
@@ -4,6 +4,7 @@
 | 
			
		||||
    <uses-permission android:name="android.permission.INTERNET" />
 | 
			
		||||
 | 
			
		||||
    <application
 | 
			
		||||
        android:name=".PokeSearch"
 | 
			
		||||
        android:allowBackup="true"
 | 
			
		||||
        android:enableOnBackInvokedCallback="true"
 | 
			
		||||
        android:dataExtractionRules="@xml/data_extraction_rules"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										21
									
								
								app/src/main/java/com/ti/mobpo/AppViewModelProvider.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								app/src/main/java/com/ti/mobpo/AppViewModelProvider.kt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
package com.ti.mobpo
 | 
			
		||||
 | 
			
		||||
import androidx.lifecycle.ViewModelProvider
 | 
			
		||||
import androidx.lifecycle.createSavedStateHandle
 | 
			
		||||
import androidx.lifecycle.viewmodel.CreationExtras
 | 
			
		||||
import androidx.lifecycle.viewmodel.initializer
 | 
			
		||||
import androidx.lifecycle.viewmodel.viewModelFactory
 | 
			
		||||
import com.ti.mobpo.ui.pokesearch.PokeSearchViewModel
 | 
			
		||||
 | 
			
		||||
object AppViewModelProvider {
 | 
			
		||||
    val Factory = viewModelFactory {
 | 
			
		||||
        initializer {
 | 
			
		||||
            PokeSearchViewModel(
 | 
			
		||||
                pokesearchApplication().appContainer.favouritesRepository
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun CreationExtras.pokesearchApplication(): PokeSearch =
 | 
			
		||||
    (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as PokeSearch)
 | 
			
		||||
							
								
								
									
										19
									
								
								app/src/main/java/com/ti/mobpo/PokeSearch.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								app/src/main/java/com/ti/mobpo/PokeSearch.kt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
package com.ti.mobpo
 | 
			
		||||
 | 
			
		||||
import android.app.Application
 | 
			
		||||
import com.ti.mobpo.data.AppContainer
 | 
			
		||||
import com.ti.mobpo.data.AppDataContainer
 | 
			
		||||
 | 
			
		||||
class PokeSearch : Application() {
 | 
			
		||||
    lateinit var appContainer: AppContainer
 | 
			
		||||
        private set
 | 
			
		||||
 | 
			
		||||
    override fun onCreate() {
 | 
			
		||||
        super.onCreate()
 | 
			
		||||
        appContainer = createAppContainer()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun createAppContainer(): AppContainer {
 | 
			
		||||
        return AppDataContainer(this)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										16
									
								
								app/src/main/java/com/ti/mobpo/data/AppContainer.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								app/src/main/java/com/ti/mobpo/data/AppContainer.kt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
package com.ti.mobpo.data
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
interface AppContainer {
 | 
			
		||||
    val favouritesRepository: FavouritesRepository
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AppDataContainer(private val context: Context) : AppContainer {
 | 
			
		||||
 | 
			
		||||
    override val favouritesRepository: FavouritesRepository by lazy {
 | 
			
		||||
        OfflineFavouritesRepository(PokemonDatabase.getDatabase(context).FavouriteDao())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								app/src/main/java/com/ti/mobpo/data/Favourite.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								app/src/main/java/com/ti/mobpo/data/Favourite.kt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
package com.ti.mobpo.data
 | 
			
		||||
 | 
			
		||||
import androidx.room.Entity
 | 
			
		||||
import androidx.room.PrimaryKey
 | 
			
		||||
import com.ti.mobpo.model.Sprites
 | 
			
		||||
import com.ti.mobpo.model.Type
 | 
			
		||||
 | 
			
		||||
@Entity(tableName = "favourites")
 | 
			
		||||
data class Favourite(
 | 
			
		||||
    @PrimaryKey val id: Int,
 | 
			
		||||
    val name: String
 | 
			
		||||
)
 | 
			
		||||
							
								
								
									
										23
									
								
								app/src/main/java/com/ti/mobpo/data/FavouriteDao.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								app/src/main/java/com/ti/mobpo/data/FavouriteDao.kt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
package com.ti.mobpo.data
 | 
			
		||||
 | 
			
		||||
import androidx.room.Dao
 | 
			
		||||
import androidx.room.Delete
 | 
			
		||||
import androidx.room.Insert
 | 
			
		||||
import androidx.room.OnConflictStrategy
 | 
			
		||||
import androidx.room.Query
 | 
			
		||||
import kotlinx.coroutines.flow.Flow
 | 
			
		||||
 | 
			
		||||
@Dao
 | 
			
		||||
interface FavouriteDao {
 | 
			
		||||
    @Insert(onConflict = OnConflictStrategy.IGNORE)
 | 
			
		||||
    suspend fun insert(favourite: Favourite)
 | 
			
		||||
 | 
			
		||||
    @Delete
 | 
			
		||||
    suspend fun delete(favourite: Favourite)
 | 
			
		||||
 | 
			
		||||
    @Query("SELECT id FROM favourites")
 | 
			
		||||
    fun getAllFavoriteIds(): Flow<List<Int>>
 | 
			
		||||
 | 
			
		||||
    @Query("SELECT EXISTS(SELECT 1 FROM favourites WHERE id = :id LIMIT 1)")
 | 
			
		||||
    suspend fun isFavourite(id: Int): Boolean
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										29
									
								
								app/src/main/java/com/ti/mobpo/data/FavouritesRepository.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								app/src/main/java/com/ti/mobpo/data/FavouritesRepository.kt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
package com.ti.mobpo.data
 | 
			
		||||
 | 
			
		||||
import kotlinx.coroutines.flow.Flow
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Repository that provides insert, update, delete, and retrieve of [Item] from a given data source.
 | 
			
		||||
 */
 | 
			
		||||
interface FavouritesRepository {
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve all the items from the the given data source.
 | 
			
		||||
     */
 | 
			
		||||
    fun getAllFavoriteIds(): Flow<List<Int>>
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Insert item in the data source
 | 
			
		||||
     */
 | 
			
		||||
    suspend fun insertItem(item: Favourite)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Delete item from the data source
 | 
			
		||||
     */
 | 
			
		||||
    suspend fun deleteItem(item: Favourite)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if the item is a favourite
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
    suspend fun isFavourite(id: Int): Boolean
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,20 @@
 | 
			
		||||
package com.ti.mobpo.data
 | 
			
		||||
 | 
			
		||||
import kotlinx.coroutines.flow.Flow
 | 
			
		||||
 | 
			
		||||
class OfflineFavouritesRepository(private val favouriteDao: FavouriteDao) : FavouritesRepository {
 | 
			
		||||
    override fun getAllFavoriteIds(): Flow<List<Int>> {
 | 
			
		||||
        return favouriteDao.getAllFavoriteIds()
 | 
			
		||||
    }
 | 
			
		||||
    override suspend fun insertItem(item: Favourite) {
 | 
			
		||||
        favouriteDao.insert(item)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun deleteItem(item: Favourite) {
 | 
			
		||||
        favouriteDao.delete(item)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun isFavourite(id: Int): Boolean {
 | 
			
		||||
        return favouriteDao.isFavourite(id)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										23
									
								
								app/src/main/java/com/ti/mobpo/data/PokemonDatabase.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								app/src/main/java/com/ti/mobpo/data/PokemonDatabase.kt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
package com.ti.mobpo.data
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import androidx.room.Database
 | 
			
		||||
import androidx.room.Room
 | 
			
		||||
import androidx.room.RoomDatabase
 | 
			
		||||
 | 
			
		||||
@Database(entities = [Favourite::class], version = 1, exportSchema = false)
 | 
			
		||||
abstract class PokemonDatabase : RoomDatabase() {
 | 
			
		||||
    abstract fun FavouriteDao(): FavouriteDao
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        @Volatile
 | 
			
		||||
        private var Instance: PokemonDatabase? = null
 | 
			
		||||
        fun getDatabase(context: Context): PokemonDatabase {
 | 
			
		||||
            return Instance ?: synchronized(this) {
 | 
			
		||||
                Room.databaseBuilder(context, PokemonDatabase::class.java, "favorites_database")
 | 
			
		||||
                    .build()
 | 
			
		||||
                    .also { Instance = it }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -15,7 +15,8 @@ data class PokemonDetails(
 | 
			
		||||
    val id: Int,
 | 
			
		||||
    val name: String,
 | 
			
		||||
    val types: List<Type>,
 | 
			
		||||
    @SerializedName("sprites") val sprites: Sprites
 | 
			
		||||
    @SerializedName("sprites") val sprites: Sprites,
 | 
			
		||||
    var isFavorite: Boolean = false
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
data class Type(
 | 
			
		||||
 
 | 
			
		||||
@@ -8,10 +8,8 @@ import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.material.icons.Icons
 | 
			
		||||
import androidx.compose.material.icons.filled.Favorite
 | 
			
		||||
import androidx.compose.material.icons.filled.Home
 | 
			
		||||
import androidx.compose.material.icons.filled.Person
 | 
			
		||||
import androidx.compose.material.icons.outlined.FavoriteBorder
 | 
			
		||||
import androidx.compose.material.icons.outlined.Home
 | 
			
		||||
import androidx.compose.material.icons.outlined.Person
 | 
			
		||||
import androidx.compose.material3.Icon
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.material3.NavigationBar
 | 
			
		||||
@@ -22,6 +20,7 @@ import androidx.compose.material3.Text
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.runtime.rememberUpdatedState
 | 
			
		||||
import androidx.compose.runtime.saveable.rememberSaveable
 | 
			
		||||
import androidx.compose.runtime.setValue
 | 
			
		||||
@@ -34,8 +33,6 @@ import androidx.navigation.compose.currentBackStackEntryAsState
 | 
			
		||||
import androidx.navigation.compose.rememberNavController
 | 
			
		||||
import com.ti.mobpo.ui.screens.Favourites
 | 
			
		||||
import com.ti.mobpo.ui.screens.PokeSearchScreen
 | 
			
		||||
import com.ti.mobpo.ui.screens.Profile
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun Navigation() {
 | 
			
		||||
@@ -51,13 +48,7 @@ fun Navigation() {
 | 
			
		||||
            selectedIcon = Icons.Filled.Favorite,
 | 
			
		||||
            unselectedItem = Icons.Outlined.FavoriteBorder,
 | 
			
		||||
            route = Screen.Favourites.route
 | 
			
		||||
        ),
 | 
			
		||||
        BottomNavigationItem(
 | 
			
		||||
            title = "Profile",
 | 
			
		||||
            selectedIcon = Icons.Filled.Person,
 | 
			
		||||
            unselectedItem = Icons.Outlined.Person,
 | 
			
		||||
            route = Screen.Profile.route
 | 
			
		||||
        ),
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    var selectedItemIndex by rememberSaveable {
 | 
			
		||||
@@ -70,8 +61,6 @@ fun Navigation() {
 | 
			
		||||
    val updatedSelectedIndex = items.indexOfFirst { it.route == currentRoute }
 | 
			
		||||
    selectedItemIndex = rememberUpdatedState(updatedSelectedIndex).value
 | 
			
		||||
 | 
			
		||||
    val pokeSearchViewModel: PokeSearchViewModel = viewModel()
 | 
			
		||||
 | 
			
		||||
    Scaffold (
 | 
			
		||||
        bottomBar = {
 | 
			
		||||
            NavigationBar {
 | 
			
		||||
@@ -115,16 +104,12 @@ fun Navigation() {
 | 
			
		||||
                            ExitTransition.None
 | 
			
		||||
                        }) {
 | 
			
		||||
                        composable(route = Screen.PokeSearch.route) {
 | 
			
		||||
                            PokeSearchScreen(pokeSearchViewModel)
 | 
			
		||||
                            PokeSearchScreen()
 | 
			
		||||
                        }
 | 
			
		||||
                        composable(
 | 
			
		||||
                            route = Screen.Favourites.route) {
 | 
			
		||||
                            Favourites()
 | 
			
		||||
                        }
 | 
			
		||||
                        composable(
 | 
			
		||||
                            route = Screen.Profile.route) {
 | 
			
		||||
                            Profile()
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
@@ -137,7 +122,6 @@ fun Navigation() {
 | 
			
		||||
@Composable
 | 
			
		||||
fun NavigationPreview() {
 | 
			
		||||
    Surface {
 | 
			
		||||
        val pokeSearchViewModel: PokeSearchViewModel = viewModel()
 | 
			
		||||
        PokeSearchScreen(pokeSearchViewModel)
 | 
			
		||||
        PokeSearchScreen()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -2,7 +2,5 @@ package com.ti.mobpo.ui
 | 
			
		||||
 | 
			
		||||
sealed class Screen (val route: String) {
 | 
			
		||||
    object PokeSearch : Screen("default_screen")
 | 
			
		||||
    object TopNav : Screen("navigation_bar")
 | 
			
		||||
    object Favourites : Screen("favourites_page")
 | 
			
		||||
    object Profile : Screen("profile_page")
 | 
			
		||||
}
 | 
			
		||||
@@ -1,3 +1,3 @@
 | 
			
		||||
package com.ti.mobpo.ui
 | 
			
		||||
package com.ti.mobpo.ui.pokesearch
 | 
			
		||||
 | 
			
		||||
data class PokeSearchUiState(val searchQuery: String = "")
 | 
			
		||||
@@ -1,7 +1,9 @@
 | 
			
		||||
package com.ti.mobpo.ui
 | 
			
		||||
package com.ti.mobpo.ui.pokesearch
 | 
			
		||||
 | 
			
		||||
import androidx.lifecycle.ViewModel
 | 
			
		||||
import androidx.lifecycle.viewModelScope
 | 
			
		||||
import com.ti.mobpo.data.Favourite
 | 
			
		||||
import com.ti.mobpo.data.FavouritesRepository
 | 
			
		||||
import com.ti.mobpo.model.PokemonDetails
 | 
			
		||||
import com.ti.mobpo.network.PokeApi
 | 
			
		||||
import kotlinx.coroutines.flow.MutableStateFlow
 | 
			
		||||
@@ -12,7 +14,7 @@ import java.io.IOException
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PokeSearchViewModel : ViewModel() {
 | 
			
		||||
class PokeSearchViewModel(private val favouritesRepository: FavouritesRepository) : ViewModel() {
 | 
			
		||||
    private val service = PokeApi.retrofitService;
 | 
			
		||||
 | 
			
		||||
    private val _pokemonDetails = MutableStateFlow<List<PokemonDetails>?>(null)
 | 
			
		||||
@@ -26,7 +28,8 @@ class PokeSearchViewModel : ViewModel() {
 | 
			
		||||
                val detailsList = mutableListOf<PokemonDetails>()
 | 
			
		||||
                for (pokemonSpecies in filteredList) {
 | 
			
		||||
                    val details = service.getPokemonDetails(extractPokemonId(pokemonSpecies.url))
 | 
			
		||||
                    detailsList.add(details)
 | 
			
		||||
                    val isFavorite = favouritesRepository.isFavourite(details.id)
 | 
			
		||||
                    detailsList.add(details.copy(isFavorite = isFavorite))
 | 
			
		||||
                }
 | 
			
		||||
                _pokemonDetails.value = detailsList
 | 
			
		||||
            } catch (e: IOException) {
 | 
			
		||||
@@ -39,4 +42,26 @@ class PokeSearchViewModel : ViewModel() {
 | 
			
		||||
        val parts = url.split("/")
 | 
			
		||||
        return parts[parts.size - 2].toInt()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun toggleFavorite(pokemonId: Int) {
 | 
			
		||||
        _pokemonDetails.value = _pokemonDetails.value?.map { pokemon ->
 | 
			
		||||
            if (pokemon.id == pokemonId) {
 | 
			
		||||
                pokemon.copy(isFavorite = !pokemon.isFavorite)
 | 
			
		||||
            } else {
 | 
			
		||||
                pokemon
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        viewModelScope.launch {
 | 
			
		||||
            if (_pokemonDetails.value != null) {
 | 
			
		||||
                val pokemon = _pokemonDetails.value!!.find { it.id == pokemonId }
 | 
			
		||||
                pokemon?.let {
 | 
			
		||||
                    if (it.isFavorite) {
 | 
			
		||||
                        favouritesRepository.insertItem(Favourite(it.id, it.name))
 | 
			
		||||
                    } else {
 | 
			
		||||
                        favouritesRepository.deleteItem(Favourite(it.id, it.name))
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
package com.ti.mobpo.ui.screens
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.Image
 | 
			
		||||
import com.ti.mobpo.ui.PokeSearchViewModel
 | 
			
		||||
import com.ti.mobpo.ui.pokesearch.PokeSearchViewModel
 | 
			
		||||
import androidx.compose.foundation.layout.Arrangement
 | 
			
		||||
import androidx.compose.foundation.layout.Column
 | 
			
		||||
import androidx.compose.foundation.layout.Row
 | 
			
		||||
import androidx.compose.foundation.layout.Spacer
 | 
			
		||||
import androidx.compose.foundation.layout.aspectRatio
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxSize
 | 
			
		||||
@@ -14,19 +14,30 @@ 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.material.icons.Icons
 | 
			
		||||
import androidx.compose.material.icons.filled.Favorite
 | 
			
		||||
import androidx.compose.material.icons.outlined.FavoriteBorder
 | 
			
		||||
import androidx.compose.material3.Card
 | 
			
		||||
import androidx.compose.material3.CardDefaults
 | 
			
		||||
import androidx.compose.material3.Icon
 | 
			
		||||
import androidx.compose.material3.IconButton
 | 
			
		||||
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.runtime.mutableStateOf
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.runtime.setValue
 | 
			
		||||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.layout.ContentScale
 | 
			
		||||
import androidx.compose.ui.layout.layout
 | 
			
		||||
import androidx.compose.ui.platform.LocalContext
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.text.style.TextOverflow
 | 
			
		||||
import androidx.compose.ui.tooling.preview.Preview
 | 
			
		||||
import androidx.compose.ui.unit.Constraints
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import com.ti.mobpo.R
 | 
			
		||||
import com.ti.mobpo.ui.SearchBar
 | 
			
		||||
@@ -34,13 +45,13 @@ import androidx.lifecycle.viewmodel.compose.viewModel
 | 
			
		||||
import com.ti.mobpo.model.PokemonDetails
 | 
			
		||||
import coil.compose.AsyncImage
 | 
			
		||||
import coil.request.ImageRequest
 | 
			
		||||
import com.ti.mobpo.AppViewModelProvider
 | 
			
		||||
import com.ti.mobpo.capitalizeFirstLetterAfterHyphens
 | 
			
		||||
import com.ti.mobpo.ui.theme.MobileSecurityTheme
 | 
			
		||||
import java.util.Locale
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun PokeSearchScreen(pokeSearchViewModel: PokeSearchViewModel) {
 | 
			
		||||
    PokeSearch(pokeSearchViewModel)
 | 
			
		||||
fun PokeSearchScreen(viewModel: PokeSearchViewModel = viewModel(factory = AppViewModelProvider.Factory)) {
 | 
			
		||||
    PokeSearch(viewModel)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
@@ -68,7 +79,9 @@ fun PokeSearch(pokeSearchViewModel: PokeSearchViewModel) {
 | 
			
		||||
                    horizontalArrangement = Arrangement.spacedBy(8.dp)
 | 
			
		||||
                ) {
 | 
			
		||||
                    items(items = results, key = { pokemon -> pokemon.id }) { pokemon ->
 | 
			
		||||
                        PokemonCard(pokemon)
 | 
			
		||||
                        PokemonCard(pokemon, toggleFavorite = { pokemonId ->
 | 
			
		||||
                            pokeSearchViewModel.toggleFavorite(pokemonId)
 | 
			
		||||
                        })
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
@@ -80,14 +93,15 @@ fun PokeSearch(pokeSearchViewModel: PokeSearchViewModel) {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun PokemonCard(pokemon: PokemonDetails) {
 | 
			
		||||
fun PokemonCard(pokemon: PokemonDetails, toggleFavorite: (Int) -> Unit) {
 | 
			
		||||
    var isFavourite by remember { mutableStateOf(false) }
 | 
			
		||||
    Card(
 | 
			
		||||
        shape = MaterialTheme.shapes.medium,
 | 
			
		||||
        elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
 | 
			
		||||
        modifier = Modifier
 | 
			
		||||
            .padding(4.dp)
 | 
			
		||||
            .fillMaxWidth()
 | 
			
		||||
            .aspectRatio(1f)
 | 
			
		||||
            .aspectRatio(0.75f)
 | 
			
		||||
    ) {
 | 
			
		||||
        Column(
 | 
			
		||||
            modifier = Modifier.fillMaxSize(),
 | 
			
		||||
@@ -102,14 +116,38 @@ fun PokemonCard(pokemon: PokemonDetails) {
 | 
			
		||||
                    .fillMaxWidth()
 | 
			
		||||
                    .weight(1f)
 | 
			
		||||
            )
 | 
			
		||||
            Text(
 | 
			
		||||
                text = capitalizeFirstLetterAfterHyphens(pokemon.name),
 | 
			
		||||
                style = MaterialTheme.typography.bodySmall,
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .fillMaxWidth()
 | 
			
		||||
                    .padding(horizontal = 8.dp, vertical = 4.dp) // Adjust padding as needed
 | 
			
		||||
                    .wrapContentSize(Alignment.Center)
 | 
			
		||||
            )
 | 
			
		||||
            Row(
 | 
			
		||||
                verticalAlignment = Alignment.CenterVertically,
 | 
			
		||||
                modifier = Modifier.fillMaxWidth()
 | 
			
		||||
            ) {
 | 
			
		||||
                Text(
 | 
			
		||||
                    text = capitalizeFirstLetterAfterHyphens(pokemon.name),
 | 
			
		||||
                    modifier = Modifier
 | 
			
		||||
                        .padding(8.dp)
 | 
			
		||||
                        .weight(1f),
 | 
			
		||||
                    maxLines = 2,
 | 
			
		||||
                    overflow = TextOverflow.Ellipsis
 | 
			
		||||
                )
 | 
			
		||||
                IconButton(
 | 
			
		||||
                    onClick = { toggleFavorite(pokemon.id) },
 | 
			
		||||
                    modifier = Modifier.wrapContentSize()
 | 
			
		||||
                        .layout { measurable, constraints ->
 | 
			
		||||
                            if (constraints.maxHeight == Constraints.Infinity) {
 | 
			
		||||
                                layout(0, 0) {}
 | 
			
		||||
                            } else {
 | 
			
		||||
                                val placeable = measurable.measure(constraints)
 | 
			
		||||
                                layout(placeable.width, placeable.height) {
 | 
			
		||||
                                    placeable.place(0, 0)
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                ) {
 | 
			
		||||
                    Icon(
 | 
			
		||||
                        imageVector = if (pokemon.isFavorite) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
 | 
			
		||||
                        contentDescription = "Favourite"
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +0,0 @@
 | 
			
		||||
package com.ti.mobpo.ui.screens
 | 
			
		||||
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun Profile(){
 | 
			
		||||
    Text("Profile Page")
 | 
			
		||||
}
 | 
			
		||||
@@ -2,4 +2,10 @@
 | 
			
		||||
plugins {
 | 
			
		||||
    alias(libs.plugins.androidApplication) apply false
 | 
			
		||||
    alias(libs.plugins.jetbrainsKotlinAndroid) apply false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
buildscript {
 | 
			
		||||
    extra.apply {
 | 
			
		||||
        set("room_version", "2.6.0")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user