diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 796bb03..47d87af 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -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"]}")
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8df0ded..9a329ab 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,6 +4,7 @@
>
+
+ @Query("SELECT EXISTS(SELECT 1 FROM favourites WHERE id = :id LIMIT 1)")
+ suspend fun isFavourite(id: Int): Boolean
+}
diff --git a/app/src/main/java/com/ti/mobpo/data/FavouritesRepository.kt b/app/src/main/java/com/ti/mobpo/data/FavouritesRepository.kt
new file mode 100644
index 0000000..4083c1b
--- /dev/null
+++ b/app/src/main/java/com/ti/mobpo/data/FavouritesRepository.kt
@@ -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>
+
+ /**
+ * 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
+}
diff --git a/app/src/main/java/com/ti/mobpo/data/OfflineFavouritesRepository.kt b/app/src/main/java/com/ti/mobpo/data/OfflineFavouritesRepository.kt
new file mode 100644
index 0000000..6f4b000
--- /dev/null
+++ b/app/src/main/java/com/ti/mobpo/data/OfflineFavouritesRepository.kt
@@ -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> {
+ 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)
+ }
+}
diff --git a/app/src/main/java/com/ti/mobpo/data/PokemonDatabase.kt b/app/src/main/java/com/ti/mobpo/data/PokemonDatabase.kt
new file mode 100644
index 0000000..24b5509
--- /dev/null
+++ b/app/src/main/java/com/ti/mobpo/data/PokemonDatabase.kt
@@ -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 }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/ti/mobpo/model/PokeModels.kt b/app/src/main/java/com/ti/mobpo/model/PokeModels.kt
index 81c8799..4312980 100644
--- a/app/src/main/java/com/ti/mobpo/model/PokeModels.kt
+++ b/app/src/main/java/com/ti/mobpo/model/PokeModels.kt
@@ -15,7 +15,8 @@ data class PokemonDetails(
val id: Int,
val name: String,
val types: List,
- @SerializedName("sprites") val sprites: Sprites
+ @SerializedName("sprites") val sprites: Sprites,
+ var isFavorite: Boolean = false
)
data class Type(
diff --git a/app/src/main/java/com/ti/mobpo/ui/Navigation.kt b/app/src/main/java/com/ti/mobpo/ui/Navigation.kt
index af60fa4..e09a3c7 100644
--- a/app/src/main/java/com/ti/mobpo/ui/Navigation.kt
+++ b/app/src/main/java/com/ti/mobpo/ui/Navigation.kt
@@ -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()
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/ti/mobpo/ui/Screen.kt b/app/src/main/java/com/ti/mobpo/ui/Screen.kt
index a564e79..d6e109b 100644
--- a/app/src/main/java/com/ti/mobpo/ui/Screen.kt
+++ b/app/src/main/java/com/ti/mobpo/ui/Screen.kt
@@ -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")
}
\ No newline at end of file
diff --git a/app/src/main/java/com/ti/mobpo/ui/PokeSearchUiState.kt b/app/src/main/java/com/ti/mobpo/ui/pokesearch/PokeSearchUiState.kt
similarity index 63%
rename from app/src/main/java/com/ti/mobpo/ui/PokeSearchUiState.kt
rename to app/src/main/java/com/ti/mobpo/ui/pokesearch/PokeSearchUiState.kt
index 2bd3d0f..5012bf5 100644
--- a/app/src/main/java/com/ti/mobpo/ui/PokeSearchUiState.kt
+++ b/app/src/main/java/com/ti/mobpo/ui/pokesearch/PokeSearchUiState.kt
@@ -1,3 +1,3 @@
-package com.ti.mobpo.ui
+package com.ti.mobpo.ui.pokesearch
data class PokeSearchUiState(val searchQuery: String = "")
diff --git a/app/src/main/java/com/ti/mobpo/ui/PokeSearchViewModel.kt b/app/src/main/java/com/ti/mobpo/ui/pokesearch/PokeSearchViewModel.kt
similarity index 53%
rename from app/src/main/java/com/ti/mobpo/ui/PokeSearchViewModel.kt
rename to app/src/main/java/com/ti/mobpo/ui/pokesearch/PokeSearchViewModel.kt
index a36b747..5bdc5a6 100644
--- a/app/src/main/java/com/ti/mobpo/ui/PokeSearchViewModel.kt
+++ b/app/src/main/java/com/ti/mobpo/ui/pokesearch/PokeSearchViewModel.kt
@@ -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?>(null)
@@ -26,7 +28,8 @@ class PokeSearchViewModel : ViewModel() {
val detailsList = mutableListOf()
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))
+ }
+ }
+ }
+ }
+ }
}
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 105daa3..a2aa2a7 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,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"
+ )
+ }
+ }
}
}
}
diff --git a/app/src/main/java/com/ti/mobpo/ui/screens/Profile.kt b/app/src/main/java/com/ti/mobpo/ui/screens/Profile.kt
deleted file mode 100644
index cf3f3b8..0000000
--- a/app/src/main/java/com/ti/mobpo/ui/screens/Profile.kt
+++ /dev/null
@@ -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")
-}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index a0985ef..a2062cf 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -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")
+ }
}
\ No newline at end of file