Add Navigation via Voyager
This commit is contained in:
parent
699a60dffe
commit
2368d7b829
5 changed files with 213 additions and 103 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
127
composeApp/src/commonMain/kotlin/DetailScreen.kt
Normal file
127
composeApp/src/commonMain/kotlin/DetailScreen.kt
Normal 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
composeApp/src/commonMain/kotlin/LoadingScreen.kt
Normal file
36
composeApp/src/commonMain/kotlin/LoadingScreen.kt
Normal 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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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" }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue