Add Navigation via Voyager

This commit is contained in:
FirephoenixX02 2024-04-12 10:41:11 +02:00
parent 699a60dffe
commit 2368d7b829
5 changed files with 213 additions and 103 deletions

View file

@ -35,6 +35,9 @@ kotlin {
implementation(libs.kamel.image) implementation(libs.kamel.image)
implementation(libs.ktor.client.okhttp) implementation(libs.ktor.client.okhttp)
implementation(libs.json) implementation(libs.json)
implementation(libs.voyager.navigator)
implementation(libs.voyager.tab.navigator)
implementation(libs.voyager.transitions)
} }
desktopMain.dependencies { desktopMain.dependencies {
implementation(compose.desktop.currentOs) implementation(compose.desktop.currentOs)

View file

@ -1,51 +1,48 @@
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.* import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier import androidx.compose.runtime.remember
import androidx.compose.ui.geometry.CornerRadius import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Offset import cafe.adriel.voyager.navigator.Navigator
import androidx.compose.ui.geometry.Size import cafe.adriel.voyager.transitions.FadeTransition
import androidx.compose.ui.graphics.Color import cafe.adriel.voyager.transitions.SlideTransition
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import io.kamel.image.KamelImage
import io.kamel.image.asyncPainterResource
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.ui.tooling.preview.Preview import org.jetbrains.compose.ui.tooling.preview.Preview
import org.json.JSONObject import org.json.JSONObject
import org.jetbrains.compose.resources.ExperimentalResourceApi
import pokedex.composeapp.generated.resources.Res import pokedex.composeapp.generated.resources.Res
import pokedex.composeapp.generated.resources.*; import pokedex.composeapp.generated.resources.bug
import pokedex.composeapp.generated.resources.dark
import pokedex.composeapp.generated.resources.dragon
import pokedex.composeapp.generated.resources.electric
import pokedex.composeapp.generated.resources.fairy
import pokedex.composeapp.generated.resources.fighting
import pokedex.composeapp.generated.resources.fire
import pokedex.composeapp.generated.resources.flying
import pokedex.composeapp.generated.resources.ghost
import pokedex.composeapp.generated.resources.grass
import pokedex.composeapp.generated.resources.ground
import pokedex.composeapp.generated.resources.ice
import pokedex.composeapp.generated.resources.normal
import pokedex.composeapp.generated.resources.poison
import pokedex.composeapp.generated.resources.psychic
import pokedex.composeapp.generated.resources.rock
import pokedex.composeapp.generated.resources.steel
import pokedex.composeapp.generated.resources.water
import java.net.URL import java.net.URL
import java.util.Locale import java.util.Locale
var pokemap = ArrayList<Pokemon>()
val apiString = "https://pokeapi.co/api/v2/pokemon/"
@OptIn(ExperimentalResourceApi::class) @OptIn(ExperimentalResourceApi::class)
@Composable @Composable
@Preview @Preview
fun App() { fun App() {
var pokemap = ArrayList<Pokemon>()
val apiString = "https://pokeapi.co/api/v2/pokemon/"
var dataLoaded by remember { mutableStateOf(false) } var dataLoaded by remember { mutableStateOf(false) }
val orange = Color(0xFFffa500)
val pokemonTypeDrawableMap = hashMapOf( val pokemonTypeDrawableMap = hashMapOf(
"normal" to Res.drawable.normal, "normal" to Res.drawable.normal,
@ -70,6 +67,10 @@ fun App() {
MaterialTheme { MaterialTheme {
if (!dataLoaded) { if (!dataLoaded) {
Navigator(screen = LoadingScreen()) {
navigator -> FadeTransition(navigator)
}
// Load data asynchronously // Load data asynchronously
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
pokemap = withContext(Dispatchers.IO) { pokemap = withContext(Dispatchers.IO) {
@ -83,88 +84,23 @@ fun App() {
dataLoaded = true dataLoaded = true
} }
//Show Loading Circle
Box(modifier = Modifier.background(color = orange).fillMaxSize()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator(
modifier = Modifier.size(128.dp),
color = Color.White
)
Text(
"Fetching data from API...",
color = Color.White,
fontWeight = FontWeight.Bold,
modifier = Modifier.height(200.dp)
)
}
}
} else { } else {
println("Pokemon should show up any second!") println("Pokemon should show up any second!")
//Display Pokemon Grid //Display Pokemon Grid
Box(modifier = Modifier.background(color = orange).fillMaxSize()) { Navigator(screen = HomeScreen(getPokeMap(), pokemonTypeDrawableMap)) {
LazyVerticalGrid( navigator -> SlideTransition(navigator)
columns = GridCells.Adaptive(minSize = 256.dp),
) {
items(pokemap.size) { index ->
Box(modifier = Modifier.size(256.dp).padding(5.dp)) {
Canvas(modifier = Modifier.matchParentSize()) {
drawRoundRect(
color = Color.White,
topLeft = Offset(0f, 0f),
size = Size(size.width, size.height),
cornerRadius = CornerRadius(20f, 20f),
)
}
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
KamelImage(
resource = asyncPainterResource(pokemap[index].imageUrl),
modifier = Modifier.size(128.dp),
contentDescription = "",
alignment = Alignment.Center
)
}
Column(
modifier = Modifier.fillMaxSize().padding(vertical = 8.dp),
verticalArrangement = Arrangement.Bottom,
horizontalAlignment = Alignment.CenterHorizontally
) {
val type: String = pokemap[index].type
Box(modifier = Modifier.size(32.dp)) {
Image(
org.jetbrains.compose.resources.painterResource(
pokemonTypeDrawableMap[type]!!
), contentDescription = null
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = pokemap[index].name,
textAlign = TextAlign.Center,
color = Color.Black,
fontWeight = FontWeight.Bold
)
}
}
}
}
} }
} }
} }
} }
// Function to load Pokemon data asynchronously // Function to load Pokemon data asynchronously, leave suspend even if IDE complains
suspend fun loadPokemonData(apiString: String, startId: Int, endId: Int): List<Pokemon> { suspend fun loadPokemonData(apiString: String, startId: Int, endId: Int): List<Pokemon> {
val pokemap = ArrayList<Pokemon>() val pokemap = ArrayList<Pokemon>()
for (i in startId..endId) { for (i in startId..endId) {
val json = JSONObject(URL(apiString + i).readText()); val json = JSONObject(URL(apiString + i).readText());
val sprites = json.optJSONObject("sprites") val sprites = json.optJSONObject("sprites")
val types = json.getJSONArray("types") val type: String = json.getJSONArray("types").optJSONObject(0).optJSONObject("type").optString("name")
val firstType = types.optJSONObject(0)
val type: String = firstType.optJSONObject("type").optString("name")
val name: String = json.optString("name") val name: String = json.optString("name")
.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } .replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
@ -176,3 +112,7 @@ suspend fun loadPokemonData(apiString: String, startId: Int, endId: Int): List<P
} }
return pokemap return pokemap
} }
fun getPokeMap(): ArrayList<Pokemon> {
return pokemap
}

View file

@ -0,0 +1,127 @@
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PaintingStyle.Companion.Stroke
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.DrawStyle
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import io.kamel.image.KamelImage
import io.kamel.image.asyncPainterResource
import org.json.JSONObject
import java.net.URL
import java.util.Locale
data class DetailScreen(
val pokemon: Pokemon
) : Screen {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val orange = Color(0xFFffa500)
val apiString = "https://pokeapi.co/api/v2/pokemon/"
val json = JSONObject(URL(apiString + pokemon.name.lowercase()).readText());
val type: String =
json.getJSONArray("types").optJSONObject(0).optJSONObject("type").optString("name")
val name: String = json.optString("name")
.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
val xp: String = json.optString("base_experience")
val height: String = json.optString("height")
val weight: String = json.optString("weight")
Box(
modifier = Modifier.background(color = orange).fillMaxSize(),
contentAlignment = Alignment.Center
) {
//Back Button
Button(onClick = { navigator.push(navigator.lastItem) }) {
Text("Back")
}
//Details
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
KamelImage(
resource = asyncPainterResource(pokemon.imageUrl),
modifier = Modifier.size(256.dp),
contentDescription = "",
alignment = Alignment.Center
)
}
Box(
modifier = Modifier.size(256.dp).padding(5.dp), contentAlignment = Alignment.Center
) {
Canvas(modifier = Modifier.matchParentSize()) {
drawRoundRect(
color = Color.White,
topLeft = Offset(0f, 0f),
size = Size(size.width, size.height),
cornerRadius = CornerRadius(20f, 20f),
)
drawRoundRect(
color = Color.Black,
topLeft = Offset(0f, 0f),
size = Size(size.width, size.height),
cornerRadius = CornerRadius(20f, 20f),
style = Stroke(1.dp.toPx())
)
}
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(16.dp)
) {
Text(
"Name: $name",
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold
)
Text(
"Base XP: $xp",
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold
)
Text(
"Height: $height",
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold
)
Text(
"Weight: ${weight}kg",
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold
)
}
}
}
}
}

View file

@ -0,0 +1,36 @@
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.screen.Screen
class LoadingScreen : Screen {
@Composable
override fun Content() {
val orange = Color(0xFFffa500)
Box(modifier = Modifier.background(color = orange).fillMaxSize()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator(
modifier = Modifier.size(128.dp),
color = Color.White
)
Text(
"Fetching data from API...",
color = Color.White,
fontWeight = FontWeight.Bold,
modifier = Modifier.height(200.dp)
)
}
}
}
}

View file

@ -22,6 +22,7 @@ kotlin = "1.9.22"
lifecycleProcess = "2.7.0" lifecycleProcess = "2.7.0"
media3Effect = "1.3.1" media3Effect = "1.3.1"
ktor = "2.3.10" ktor = "2.3.10"
voyagerNavigator = "1.0.0"
[libraries] [libraries]
androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycleProcess" } androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycleProcess" }
@ -42,6 +43,9 @@ androidx-activity-compose = { module = "androidx.activity:activity-compose", ver
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" }
androidx-media3-effect = { group = "androidx.media3", name = "media3-effect", version.ref = "media3Effect" } androidx-media3-effect = { group = "androidx.media3", name = "media3-effect", version.ref = "media3Effect" }
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyagerNavigator" }
voyager-tab-navigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyagerNavigator" }
voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyagerNavigator" }
[plugins] [plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" } androidApplication = { id = "com.android.application", version.ref = "agp" }