Snap for 14184262 from 54523667d4eb0c5c5bed5a50a6662c08f85be05d to androidx-compose-material3-adaptive-release
Change-Id: I5caeadf79a8c8dd190954a5dc55e278df643e9ae
diff --git a/app/build.gradle b/app/build.gradle
index eac44e2..6bb7c1d 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,10 +1,12 @@
+import androidx.build.KotlinTarget
+
plugins {
id("AndroidXPlugin")
id("AndroidXComposePlugin")
id("com.google.devtools.ksp")
id("com.android.library")
id("org.jetbrains.kotlin.android")
- id("androidx.room")
+ id("androidx.room") version "2.7.0"
alias(libs.plugins.kotlinSerialization)
}
@@ -14,25 +16,41 @@
compileSdk = 35
defaultConfig {
- // The schemas directory contains a schema file for each version of the Room database.
- // This is required to enable Room auto migrations.
+ // Propagate the proguard rules to be used by the including app module
+ consumerProguardFiles("proguard-rules.pro")
+ // We don't export schemas, but Room's Gradle plugin doesn't know that. We use the project
+ // directory as a placeholder.
room {
- schemaDirectory("$projectDir/schemas")
+ schemaDirectory("$projectDir/room_schemas")
}
}
-
- buildTypes {
- release {
- minifyEnabled false
- proguardFiles("proguard-rules.pro")
- }
- }
-
- buildFeatures {
- buildConfig = true
- }
}
+
+// Create a release build type and make sure it's the only one enabled.
+// This is needed because we benchmark the release build type only.
+android.buildTypes { release {} }
+androidComponents { beforeVariants(selector().all()) { enabled = buildType == 'release' } }
+
+/**
+ * Exclusion groups for JetBrains Compose. When a dependency depends on JetBrains Compose, prebuilts
+ * for all other platforms will be pulled in. We also want to ensure we're using *Jetpack* Compose
+ * and not mix JetBrains and Jetpack Compose.
+ *
+ * IMPORTANT: When adding a dependency here, please make sure that it is otherwise present on the
+ * classpath.
+ */
+def jetbrainsComposeExcludeGroups = [
+ 'org.jetbrains.androidx.lifecycle',
+ 'org.jetbrains.compose.animation',
+ 'org.jetbrains.compose.annotation-internal',
+ 'org.jetbrains.compose.collection-internal',
+ 'org.jetbrains.compose.foundation',
+ 'org.jetbrains.compose.runtime',
+ 'org.jetbrains.compose.ui',
+ 'org.jetbrains.skiko'
+]
+
dependencies {
// Compose
implementation(project(":compose:runtime:runtime"))
@@ -48,15 +66,26 @@
// AndroidX
implementation(libs.androidx.core)
- implementation(libs.testRunner)
- implementation("androidx.navigation:navigation-compose:2.8.2")
- implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4")
+ implementation("androidx.navigation:navigation-compose:2.7.7")
+ implementation("androidx.lifecycle:lifecycle-runtime:2.6.2")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
+ implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.2")
implementation("androidx.palette:palette:1.0.0")
// Image Loading
implementation("com.github.bumptech.glide:compose:1.0.0-beta01")
implementation("com.github.bumptech.glide:okhttp3-integration:4.16.0")
ksp("com.github.bumptech.glide:ksp:4.16.0")
+ implementation("io.coil-kt.coil3:coil-compose-android:3.1.0") {
+ jetbrainsComposeExcludeGroups.each { group ->
+ exclude group: group
+ }
+ }
+ implementation("io.coil-kt.coil3:coil-network-okhttp:3.1.0") {
+ jetbrainsComposeExcludeGroups.each { group ->
+ exclude group: group
+ }
+ }
// Kotlinx
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7")
@@ -66,13 +95,14 @@
implementation(libs.kotlinCoroutinesAndroid)
// Database
- implementation("androidx.room:room-runtime:2.6.1")
- implementation("androidx.room:room-ktx:2.6.1")
- ksp("androidx.room:room-compiler:2.6.1")
+ implementation("androidx.room:room-runtime:2.7.0")
+ implementation("androidx.room:room-ktx:2.7.0")
+ ksp("androidx.room:room-compiler:2.7.0")
// Network
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.retrofit2:converter-kotlinx-serialization:2.11.0")
+ api("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
implementation("com.squareup.okhttp3:mockwebserver:4.12.0")
implementation("com.squareup.okhttp3:okhttp-tls:4.12.0")
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index df35c17..0cf1f04 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -41,3 +41,21 @@
public static **[] values();
public static ** valueOf(java.lang.String);
}
+
+# Serializer for classes with named companion objects are retrieved using `getDeclaredClasses`.
+# If you have any, replace classes with those containing named companion objects.
+-keepattributes InnerClasses # Needed for `getDeclaredClasses`.
+
+-if @kotlinx.serialization.Serializable class com.skydoves.pokedex.compose.core.model.Pokemon
+{
+ static **$* *;
+}
+-keepnames class <1>$$serializer { # -keepnames suffices; class is kept when serializer() is kept.
+ static <1>$$serializer INSTANCE;
+}
+
+# Keep both serializer and serializable classes to save the attribute InnerClasses
+-keepclasseswithmembers, allowshrinking, allowobfuscation, allowaccessmodification class com.skydoves.pokedex.compose.core.model.Pokemon
+{
+ *;
+}
diff --git a/app/room_schemas/.placeholder b/app/room_schemas/.placeholder
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/room_schemas/.placeholder
diff --git a/app/schemas/com.skydoves.pokedex.compose.core.database.PokedexDatabase/2.json b/app/schemas/com.skydoves.pokedex.compose.core.database.PokedexDatabase/2.json
deleted file mode 100644
index b8235b6..0000000
--- a/app/schemas/com.skydoves.pokedex.compose.core.database.PokedexDatabase/2.json
+++ /dev/null
@@ -1,126 +0,0 @@
-{
- "formatVersion": 1,
- "database": {
- "version": 2,
- "identityHash": "3e4fc349c7e47ef58902f587531caab0",
- "entities": [
- {
- "tableName": "PokemonEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`page` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`name`))",
- "fields": [
- {
- "fieldPath": "page",
- "columnName": "page",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "name",
- "columnName": "name",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "url",
- "columnName": "url",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "autoGenerate": false,
- "columnNames": [
- "name"
- ]
- },
- "indices": [],
- "foreignKeys": []
- },
- {
- "tableName": "PokemonInfoEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `height` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `experience` INTEGER NOT NULL, `types` TEXT NOT NULL, `hp` INTEGER NOT NULL, `attack` INTEGER NOT NULL, `defense` INTEGER NOT NULL, `speed` INTEGER NOT NULL, `exp` INTEGER NOT NULL, PRIMARY KEY(`id`))",
- "fields": [
- {
- "fieldPath": "id",
- "columnName": "id",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "name",
- "columnName": "name",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "height",
- "columnName": "height",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "weight",
- "columnName": "weight",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "experience",
- "columnName": "experience",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "types",
- "columnName": "types",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "hp",
- "columnName": "hp",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "attack",
- "columnName": "attack",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "defense",
- "columnName": "defense",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "speed",
- "columnName": "speed",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "exp",
- "columnName": "exp",
- "affinity": "INTEGER",
- "notNull": true
- }
- ],
- "primaryKey": {
- "autoGenerate": false,
- "columnNames": [
- "id"
- ]
- },
- "indices": [],
- "foreignKeys": []
- }
- ],
- "views": [],
- "setupQueries": [
- "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
- "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3e4fc349c7e47ef58902f587531caab0')"
- ]
- }
-}
\ No newline at end of file
diff --git a/app/schemas/com.skydoves.pokedex.compose.core.database.PokedexDatabase/3.json b/app/schemas/com.skydoves.pokedex.compose.core.database.PokedexDatabase/3.json
deleted file mode 100644
index ca4d915..0000000
--- a/app/schemas/com.skydoves.pokedex.compose.core.database.PokedexDatabase/3.json
+++ /dev/null
@@ -1,108 +0,0 @@
-{
- "formatVersion": 1,
- "database": {
- "version": 3,
- "identityHash": "2ce2c0e046fdc408aab83eb7e475bf26",
- "entities": [
- {
- "tableName": "PokemonEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`page` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`name`))",
- "fields": [
- {
- "fieldPath": "page",
- "columnName": "page",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "name",
- "columnName": "name",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "url",
- "columnName": "url",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "autoGenerate": false,
- "columnNames": [
- "name"
- ]
- },
- "indices": [],
- "foreignKeys": []
- },
- {
- "tableName": "PokemonInfoEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `height` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `experience` INTEGER NOT NULL, `types` TEXT NOT NULL, `exp` INTEGER NOT NULL, `stats` TEXT NOT NULL, PRIMARY KEY(`id`))",
- "fields": [
- {
- "fieldPath": "id",
- "columnName": "id",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "name",
- "columnName": "name",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "height",
- "columnName": "height",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "weight",
- "columnName": "weight",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "experience",
- "columnName": "experience",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "types",
- "columnName": "types",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "exp",
- "columnName": "exp",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "stats",
- "columnName": "stats",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "autoGenerate": false,
- "columnNames": [
- "id"
- ]
- },
- "indices": [],
- "foreignKeys": []
- }
- ],
- "views": [],
- "setupQueries": [
- "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
- "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2ce2c0e046fdc408aab83eb7e475bf26')"
- ]
- }
-}
\ No newline at end of file
diff --git a/app/schemas/com.skydoves.pokedex.compose.core.database.PokedexDatabase/4.json b/app/schemas/com.skydoves.pokedex.compose.core.database.PokedexDatabase/4.json
deleted file mode 100644
index 7e92e13..0000000
--- a/app/schemas/com.skydoves.pokedex.compose.core.database.PokedexDatabase/4.json
+++ /dev/null
@@ -1,96 +0,0 @@
-{
- "formatVersion": 1,
- "database": {
- "version": 4,
- "identityHash": "24112c02c3e0f768491f43697e74e8ea",
- "entities": [
- {
- "tableName": "PokemonEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
- "fields": [
- {
- "fieldPath": "name",
- "columnName": "name",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "autoGenerate": false,
- "columnNames": [
- "name"
- ]
- },
- "indices": [],
- "foreignKeys": []
- },
- {
- "tableName": "PokemonInfoEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `height` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `experience` INTEGER NOT NULL, `types` TEXT NOT NULL, `exp` INTEGER NOT NULL, `stats` TEXT NOT NULL, PRIMARY KEY(`id`))",
- "fields": [
- {
- "fieldPath": "id",
- "columnName": "id",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "name",
- "columnName": "name",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "height",
- "columnName": "height",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "weight",
- "columnName": "weight",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "experience",
- "columnName": "experience",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "types",
- "columnName": "types",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "exp",
- "columnName": "exp",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "stats",
- "columnName": "stats",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "autoGenerate": false,
- "columnNames": [
- "id"
- ]
- },
- "indices": [],
- "foreignKeys": []
- }
- ],
- "views": [],
- "setupQueries": [
- "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
- "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '24112c02c3e0f768491f43697e74e8ea')"
- ]
- }
-}
\ No newline at end of file
diff --git a/app/schemas/com.skydoves.pokedex.compose.core.database.PokedexDatabase/5.json b/app/schemas/com.skydoves.pokedex.compose.core.database.PokedexDatabase/5.json
deleted file mode 100644
index f9517ce..0000000
--- a/app/schemas/com.skydoves.pokedex.compose.core.database.PokedexDatabase/5.json
+++ /dev/null
@@ -1,96 +0,0 @@
-{
- "formatVersion": 1,
- "database": {
- "version": 5,
- "identityHash": "24112c02c3e0f768491f43697e74e8ea",
- "entities": [
- {
- "tableName": "PokemonEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
- "fields": [
- {
- "fieldPath": "name",
- "columnName": "name",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "autoGenerate": false,
- "columnNames": [
- "name"
- ]
- },
- "indices": [],
- "foreignKeys": []
- },
- {
- "tableName": "PokemonInfoEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `height` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `experience` INTEGER NOT NULL, `types` TEXT NOT NULL, `exp` INTEGER NOT NULL, `stats` TEXT NOT NULL, PRIMARY KEY(`id`))",
- "fields": [
- {
- "fieldPath": "id",
- "columnName": "id",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "name",
- "columnName": "name",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "height",
- "columnName": "height",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "weight",
- "columnName": "weight",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "experience",
- "columnName": "experience",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "types",
- "columnName": "types",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "exp",
- "columnName": "exp",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "stats",
- "columnName": "stats",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "autoGenerate": false,
- "columnNames": [
- "id"
- ]
- },
- "indices": [],
- "foreignKeys": []
- }
- ],
- "views": [],
- "setupQueries": [
- "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
- "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '24112c02c3e0f768491f43697e74e8ea')"
- ]
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/PokedexFeatureFlags.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/PokedexFeatureFlags.kt
new file mode 100644
index 0000000..b8acea8
--- /dev/null
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/PokedexFeatureFlags.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.skydoves.pokedex.compose.core
+
+/** Contains feature flags for the Pokedex hero benchmark target */
+object PokedexFeatureFlags {
+ /**
+ * Whether to configure and use Coil for image loading of all images, or Glide if false. Please
+ * note that Glide will always be configured.
+ */
+ var UseCoil = true
+
+ /**
+ * Whether [androidx.compose.animation.SharedTransitionScope] should be used or replaced by
+ * simpler layouts instead. If false, shared element transitions will be off too.
+ */
+ var EnableSharedTransitionScope = true
+
+ /**
+ * Whether to enable shared element transitions between the activities.
+ * [EnableSharedTransitionScope] must be set to true, otherwise this flag will be false.
+ */
+ var EnableSharedElementTransitions = true
+ get() = EnableSharedTransitionScope && field
+}
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/data/repository/details/DetailsRepositoryImpl.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/data/repository/details/DetailsRepositoryImpl.kt
index 91991ca..2612f66 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/data/repository/details/DetailsRepositoryImpl.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/data/repository/details/DetailsRepositoryImpl.kt
@@ -18,8 +18,8 @@
import androidx.annotation.WorkerThread
import com.skydoves.pokedex.compose.core.database.PokemonInfoDao
-import com.skydoves.pokedex.compose.core.database.entitiy.mapper.asDomain
-import com.skydoves.pokedex.compose.core.database.entitiy.mapper.asEntity
+import com.skydoves.pokedex.compose.core.database.entitiy.mapper.asDatabaseEntity
+import com.skydoves.pokedex.compose.core.database.entitiy.mapper.asPresentationModel
import com.skydoves.pokedex.compose.core.network.Dispatcher
import com.skydoves.pokedex.compose.core.network.PokedexAppDispatchers
import com.skydoves.pokedex.compose.core.network.service.PokedexClient
@@ -38,7 +38,7 @@
override fun fetchPokemonInfo(
name: String,
onComplete: () -> Unit,
- onError: (String?) -> Unit
+ onError: (String?) -> Unit,
) =
flow {
val pokemonInfo = pokemonInfoDao.getPokemonInfo(name)
@@ -46,12 +46,12 @@
val response = pokedexClient.fetchPokemonInfo(name = name)
response
.onSuccess { data ->
- pokemonInfoDao.insertPokemonInfo(data.asEntity())
+ pokemonInfoDao.insertPokemonInfo(data.asDatabaseEntity())
emit(data)
}
.onFailure { throwable -> onError(throwable.message) }
} else {
- emit(pokemonInfo.asDomain())
+ emit(pokemonInfo.asPresentationModel())
}
}
.onCompletion { onComplete() }
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/data/repository/home/HomeRepositoryImpl.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/data/repository/home/HomeRepositoryImpl.kt
index b347870..05471a5 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/data/repository/home/HomeRepositoryImpl.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/data/repository/home/HomeRepositoryImpl.kt
@@ -34,7 +34,7 @@
private val pokedexClient: PokedexClient,
private val pokemonDao: PokemonDao,
@Dispatcher(PokedexAppDispatchers.IO) private val ioDispatcher: CoroutineDispatcher,
- private val apiUrl: HttpUrl
+ private val apiUrl: HttpUrl,
) : HomeRepository {
@WorkerThread
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/database/PokedexDatabase.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/database/PokedexDatabase.kt
index 3ba123c..2cb1821 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/database/PokedexDatabase.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/database/PokedexDatabase.kt
@@ -24,8 +24,8 @@
@Database(
entities = [PokemonEntity::class, PokemonInfoEntity::class],
- version = 5,
- exportSchema = true,
+ version = 1,
+ exportSchema = false, /* We don't require schema versioning in Hero Benchmarks */
)
@TypeConverters(value = [TypeResponseConverter::class, StatsResponseConverter::class])
abstract class PokedexDatabase : RoomDatabase() {
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/database/entitiy/mapper/PokemonEntityMapper.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/database/entitiy/mapper/PokemonEntityMapper.kt
index c61d20e..edd3012 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/database/entitiy/mapper/PokemonEntityMapper.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/database/entitiy/mapper/PokemonEntityMapper.kt
@@ -19,6 +19,7 @@
import com.skydoves.pokedex.compose.core.database.entitiy.PokemonEntity
import com.skydoves.pokedex.compose.core.model.Pokemon
import com.skydoves.pokedex.compose.core.model.PokemonNetworkModel
+import com.skydoves.pokedex.compose.core.network.di.ModuleLocator
import okhttp3.HttpUrl
fun List<PokemonNetworkModel>.asDatabaseEntity(): List<PokemonEntity> = map { pokemon ->
@@ -29,14 +30,17 @@
map { entity ->
Pokemon(
name = entity.name.replaceFirstChar { it.uppercase() },
- imageUrl =
- apiUrl
- .newBuilder()
- .addPathSegment("pokemon")
- .addPathSegment(entity.name)
- .addPathSegment("image")
- .build()
- .toString(),
- page = page
+ imageUrl = getPokemonImageUrlByName(name = entity.name, apiUrl = apiUrl).toString(),
+ page = page,
)
}
+
+fun getPokemonImageUrlByName(name: String, apiUrl: HttpUrl? = null): HttpUrl {
+ val baseApiUrl = apiUrl ?: ModuleLocator.networkModule.baseUrl
+ return baseApiUrl
+ .newBuilder()
+ .addPathSegment("pokemon")
+ .addPathSegment(name)
+ .addPathSegment("image")
+ .build()
+}
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/database/entitiy/mapper/PokemonInfoEntityMapper.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/database/entitiy/mapper/PokemonInfoEntityMapper.kt
index 7c105d2..e2bedd3 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/database/entitiy/mapper/PokemonInfoEntityMapper.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/database/entitiy/mapper/PokemonInfoEntityMapper.kt
@@ -19,39 +19,26 @@
import com.skydoves.pokedex.compose.core.database.entitiy.PokemonInfoEntity
import com.skydoves.pokedex.compose.core.model.PokemonInfo
-object PokemonInfoEntityMapper : EntityMapper<PokemonInfo, PokemonInfoEntity> {
+fun PokemonInfo.asDatabaseEntity(): PokemonInfoEntity =
+ PokemonInfoEntity(
+ id = id,
+ name = name,
+ height = height,
+ weight = weight,
+ experience = experience,
+ types = types,
+ exp = exp,
+ stats = stats,
+ )
- override fun asEntity(domain: PokemonInfo): PokemonInfoEntity {
- return PokemonInfoEntity(
- id = domain.id,
- name = domain.name,
- height = domain.height,
- weight = domain.weight,
- experience = domain.experience,
- types = domain.types,
- exp = domain.exp,
- stats = domain.stats,
- )
- }
-
- override fun asDomain(entity: PokemonInfoEntity): PokemonInfo {
- return PokemonInfo(
- id = entity.id,
- name = entity.name,
- height = entity.height,
- weight = entity.weight,
- experience = entity.experience,
- types = entity.types,
- exp = entity.exp,
- stats = entity.stats,
- )
- }
-}
-
-fun PokemonInfo.asEntity(): PokemonInfoEntity {
- return PokemonInfoEntityMapper.asEntity(this)
-}
-
-fun PokemonInfoEntity.asDomain(): PokemonInfo {
- return PokemonInfoEntityMapper.asDomain(this)
-}
+fun PokemonInfoEntity.asPresentationModel(): PokemonInfo =
+ PokemonInfo(
+ id = id,
+ name = name,
+ height = height,
+ weight = weight,
+ experience = experience,
+ types = types,
+ exp = exp,
+ stats = stats,
+ )
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/designsystem/component/PokedexAppBar.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/designsystem/component/PokedexAppBar.kt
index 524c450..ede81e3 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/designsystem/component/PokedexAppBar.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/designsystem/component/PokedexAppBar.kt
@@ -57,10 +57,7 @@
)
},
colors =
- TopAppBarDefaults.topAppBarColors()
- .copy(
- containerColor = PokedexTheme.colors.primary,
- ),
+ TopAppBarDefaults.topAppBarColors().copy(containerColor = PokedexTheme.colors.primary),
)
}
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/designsystem/component/PokedexProgressBar.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/designsystem/component/PokedexProgressBar.kt
index 716bf4b..5e44842 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/designsystem/component/PokedexProgressBar.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/designsystem/component/PokedexProgressBar.kt
@@ -26,6 +26,7 @@
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
@@ -43,12 +44,15 @@
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalInspectionMode
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.skydoves.pokedex.compose.core.designsystem.theme.PokedexTheme
+import com.skydoves.pokedex.compose.core.designsystem.utils.TraceAsync
import com.skydoves.pokedex.compose.core.designsystem.utils.pxToDp
+@Suppress("ConfigurationScreenWidthHeight")
@Composable
fun PokedexProgressBar(
modifier: Modifier = Modifier,
@@ -64,7 +68,7 @@
screenWidth
} else {
0f
- },
+ }
)
}
@@ -78,37 +82,35 @@
color = PokedexTheme.colors.absoluteWhite,
shape = RoundedCornerShape(64.dp),
)
- .clip(RoundedCornerShape(64.dp)),
+ .clip(RoundedCornerShape(64.dp))
) {
var textWidth by remember { mutableIntStateOf(0) }
val threshold = 16
val isInner by
- remember(
- progressWidth,
- textWidth,
- ) {
+ remember(progressWidth, textWidth) {
mutableStateOf(progressWidth > (textWidth + threshold * 2))
}
+ var animationRunning by remember { mutableStateOf(true) }
+ if (animationRunning) {
+ TraceAsync("PokedexProgressBar Animation (label = $label)")
+ }
+ Box(Modifier.size(1.dp).testTag("progress-animation-active-$animationRunning"))
val animation: Float by
animateFloatAsState(
targetValue = if (progressWidth == 0f) 0f else 1f,
// Configure the animation duration and easing.
animationSpec = tween(durationMillis = 950, easing = LinearOutSlowInEasing),
label = "",
+ finishedListener = { animationRunning = false },
)
Box(
modifier =
Modifier.align(Alignment.CenterStart)
- .width(
- progressWidth.toInt().pxToDp() * animation,
- )
+ .width(progressWidth.toInt().pxToDp() * animation)
.height(18.dp)
- .background(
- color = color,
- shape = RoundedCornerShape(64.dp),
- ),
+ .background(color = color, shape = RoundedCornerShape(64.dp))
) {
if (isInner) {
Text(
@@ -128,9 +130,7 @@
modifier =
Modifier.onSizeChanged { textWidth = it.width }
.align(Alignment.CenterStart)
- .padding(
- start = progressWidth.toInt().pxToDp() + threshold.pxToDp(),
- ),
+ .padding(start = progressWidth.toInt().pxToDp() + threshold.pxToDp()),
text = label,
fontSize = 12.sp,
color = PokedexTheme.colors.absoluteBlack,
@@ -146,7 +146,7 @@
PokedexTheme {
Box(
modifier =
- Modifier.fillMaxWidth().height(120.dp).background(PokedexTheme.colors.background),
+ Modifier.fillMaxWidth().height(120.dp).background(PokedexTheme.colors.background)
) {
PokedexProgressBar(
modifier = Modifier.align(Alignment.Center),
@@ -165,7 +165,7 @@
PokedexTheme {
Box(
modifier =
- Modifier.fillMaxWidth().height(120.dp).background(PokedexTheme.colors.background),
+ Modifier.fillMaxWidth().height(120.dp).background(PokedexTheme.colors.background)
) {
PokedexProgressBar(
modifier = Modifier.fillMaxWidth().align(Alignment.Center),
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/designsystem/component/PokedexSharedElement.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/designsystem/component/PokedexSharedElement.kt
index e160416..5578c91 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/designsystem/component/PokedexSharedElement.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/designsystem/component/PokedexSharedElement.kt
@@ -52,8 +52,8 @@
state: SharedTransitionScope.SharedContentState,
animatedVisibilityScope: AnimatedVisibilityScope,
boundsTransform: BoundsTransform = DefaultBoundsTransform,
- placeHolderSize: SharedTransitionScope.PlaceHolderSize =
- SharedTransitionScope.PlaceHolderSize.contentSize,
+ placeholderSize: SharedTransitionScope.PlaceholderSize =
+ SharedTransitionScope.PlaceholderSize.ContentSize,
renderInOverlayDuringTransition: Boolean = true,
zIndexInOverlay: Float = 0f,
clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip = ParentClip,
@@ -66,7 +66,7 @@
sharedContentState = state,
animatedVisibilityScope = animatedVisibilityScope,
boundsTransform = boundsTransform,
- placeHolderSize = placeHolderSize,
+ placeholderSize = placeholderSize,
renderInOverlayDuringTransition = renderInOverlayDuringTransition,
zIndexInOverlay = zIndexInOverlay,
clipInOverlayDuringTransition = clipInOverlayDuringTransition,
@@ -89,10 +89,7 @@
}
private val DefaultSpring =
- spring(
- stiffness = Spring.StiffnessMediumLow,
- visibilityThreshold = Rect.VisibilityThreshold,
- )
+ spring(stiffness = Spring.StiffnessMediumLow, visibilityThreshold = Rect.VisibilityThreshold)
@OptIn(ExperimentalSharedTransitionApi::class)
private val DefaultBoundsTransform = BoundsTransform { _, _ -> DefaultSpring }
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/designsystem/component/PokedexText.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/designsystem/component/PokedexText.kt
index b5cea7b..a35c45e 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/designsystem/component/PokedexText.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/designsystem/component/PokedexText.kt
@@ -37,8 +37,8 @@
@Composable
fun PokedexText(
text: String,
- previewText: String = text,
modifier: Modifier = Modifier,
+ previewText: String = text,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
fontStyle: FontStyle? = null,
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/designsystem/theme/PokedexTheme.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/designsystem/theme/PokedexTheme.kt
index 7d9a743..d3d50ec 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/designsystem/theme/PokedexTheme.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/designsystem/theme/PokedexTheme.kt
@@ -56,7 +56,7 @@
) {
Box(
modifier =
- Modifier.background(background.color).semantics { testTagsAsResourceId = true },
+ Modifier.background(background.color).semantics { testTagsAsResourceId = true }
) {
content()
}
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/designsystem/utils/TraceAsync.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/designsystem/utils/TraceAsync.kt
new file mode 100644
index 0000000..35c057f
--- /dev/null
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/designsystem/utils/TraceAsync.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.skydoves.pokedex.compose.core.designsystem.utils
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.tracing.Trace
+import kotlin.random.Random
+
+/**
+ * Trace the lifetime of this composable with an async trace block. Begins tracing when added to
+ * composition and ends tracing when removed. This is useful for tracking how long a composable is
+ * in a certain state, e.g. animations.
+ */
+@Composable
+fun TraceAsync(methodName: String) {
+ DisposableEffect(methodName) {
+ val tracingCookie = Random.nextInt()
+ Trace.beginAsyncSection(methodName, tracingCookie)
+ onDispose { Trace.endAsyncSection(methodName, tracingCookie) }
+ }
+}
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/di/RepositoryModule.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/di/RepositoryModule.kt
index b8790af..b0f7164 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/di/RepositoryModule.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/di/RepositoryModule.kt
@@ -31,7 +31,7 @@
private val pokemonDao: PokemonDao,
private val pokemonInfoDao: PokemonInfoDao,
private val ioDispatcher: CoroutineDispatcher,
- private val apiUrl: HttpUrl
+ private val apiUrl: HttpUrl,
) {
val detailsRepository: DetailsRepository by lazy {
DetailsRepositoryImpl(pokedexClient, pokemonInfoDao, ioDispatcher)
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/model/PokemonInfo.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/model/PokemonInfo.kt
index 8909227..5c2f9c9 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/model/PokemonInfo.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/model/PokemonInfo.kt
@@ -75,15 +75,9 @@
@SerialName(value = "stat") val stat: Stat,
)
- @Serializable
- data class Stat(
- @SerialName(value = "name") val name: String,
- )
+ @Serializable data class Stat(@SerialName(value = "name") val name: String)
- @Serializable
- data class Type(
- @SerialName(value = "name") val name: String,
- )
+ @Serializable data class Type(@SerialName(value = "name") val name: String)
companion object {
const val MAX_HP = 300
@@ -103,7 +97,7 @@
weight = random.nextInt(80, 300),
experience = random.nextInt(0, 100),
types = listOf(FakePokemonTypeResponse(random)),
- stats = listOf(fakePokemonStats(random))
+ stats = listOf(fakePokemonStats(random)),
)
}
@@ -122,7 +116,7 @@
return PokemonInfo.StatsResponse(
baseStat = random.nextInt(until = statMax),
effort = random.nextInt(),
- stat = stat
+ stat = stat,
)
}
@@ -133,7 +127,7 @@
"A big one",
"An adorable one",
"A tiny one",
- "A software-developing one"
+ "A software-developing one",
)
fun FakePokemonTypeResponse(random: Random = Random) =
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/model/PokemonNetworkModel.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/model/PokemonNetworkModel.kt
index 912f930..a568c58 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/model/PokemonNetworkModel.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/model/PokemonNetworkModel.kt
@@ -17,33 +17,328 @@
package com.skydoves.pokedex.compose.core.model
import androidx.compose.runtime.Immutable
+import androidx.compose.ui.util.trace
import kotlinx.serialization.Serializable
@Immutable @Serializable class PokemonNetworkModel(val name: String)
-fun fakePokemonNetworkModels() = FakeRandomizedNames.map { name -> PokemonNetworkModel(name) }
+fun fakePokemonNetworkModels(pokemonNames: List<String>) =
+ pokemonNames.map { name -> PokemonNetworkModel(name) }
-val FakePokemonNames =
- listOf(
- "Jason",
- "Jack",
- "Anna",
- "Bubir",
- "Xanto",
- "Vistesia",
- "Ulint-y",
- "Lapesareba",
- "Nemo",
- "Masurap"
- )
-val FakeRandomizedNames by lazy {
- FakePokemonNames.flatMap { name ->
- val nameChars = name.toCharArray()
- val shuffledChars = nameChars.apply { shuffle() }
+fun fakePokemonNames(limit: Int, offset: Int = 0): List<String> =
+ trace("fakePokemonNames(limit=$limit, offset=$offset)") {
+ return AllPokemonNames.subList(offset, minOf(offset + limit, AllPokemonNames.size))
+ }
+
+val AllPokemonNames =
+ trace("AllPokemonNames") {
listOf(
- shuffledChars.joinToString(""),
- shuffledChars.apply { reverse() }.joinToString(""),
- nameChars.apply { reverse() }.joinToString("")
+ "Ablazeon",
+ "Aerofer",
+ "Amphibyte",
+ "Anglark",
+ "Aquastrike",
+ "Arborix",
+ "Arctiflow",
+ "Armadile",
+ "Astrobat",
+ "Audioson",
+ "Backfire",
+ "Barricade",
+ "Basaltix",
+ "Batterfly",
+ "Beambyte",
+ "Berylix",
+ "Biobloom",
+ "Bizarroid",
+ "Blazefin",
+ "Boltclaw",
+ "Bonetail",
+ "Boulderisk",
+ "Bramblepuff",
+ "Bronzite",
+ "Bubblepod",
+ "Buggle",
+ "Bulbasa",
+ "Bumblefoot",
+ "Burrowix",
+ "Buzzardly",
+ "Cactuspear",
+ "Cadmiumite",
+ "Canopyawn",
+ "Capribble",
+ "Carapaceon",
+ "Caveworm",
+ "Celestrike",
+ "Cinderwing",
+ "Citrineon",
+ "Cliffhopper",
+ "Cloudillo",
+ "Coalabear",
+ "Cobaltix",
+ "Cometear",
+ "Coralite",
+ "Cosmite",
+ "Cragrawler",
+ "Creekle",
+ "Crestawl",
+ "Cryofrost",
+ "Cubblestone",
+ "Cyberspyke",
+ "Dampfly",
+ "Darkowl",
+ "Dawnwing",
+ "Deepshell",
+ "Deltawing",
+ "Desertail",
+ "Dewdrop",
+ "Diamondix",
+ "Digmound",
+ "Dirtdigger",
+ "Diskdrive",
+ "Dizzybat",
+ "Doombloom",
+ "Dracoat",
+ "Dragofin",
+ "Drakonix",
+ "Dreamist",
+ "Drillburrow",
+ "Dronix",
+ "Drowsipurr",
+ "Duneviper",
+ "Dustbunny",
+ "Dynamite",
+ "Ebonwing",
+ "Echoark",
+ "Eelixir",
+ "Emberfox",
+ "Emeraldite",
+ "Energlow",
+ "Equinox",
+ "Eruptile",
+ "Everbloom",
+ "Fableaf",
+ "Falconryx",
+ "Fangsnap",
+ "Featherfin",
+ "Ferroclaw",
+ "Fiercehorn",
+ "Firefang",
+ "Fissurion",
+ "Flashfire",
+ "Flatsnout",
+ "Flitterby",
+ "Flowertail",
+ "Flutterfin",
+ "Fogwhisper",
+ "Forestomp",
+ "Fossilite",
+ "Fractalix",
+ "Frostbite",
+ "Fungoyle",
+ "Galaxite",
+ "Galeonix",
+ "Gargoyle",
+ "Gascloud",
+ "Geminite",
+ "Glacieron",
+ "Glimmerbug",
+ "Gloomfang",
+ "Glowfin",
+ "Graniteel",
+ "Grassnake",
+ "Gravityx",
+ "Grubsqueak",
+ "Gunkpile",
+ "Gustwing",
+ "Hailstone",
+ "Hammerhead",
+ "Harmonic",
+ "Hazehorn",
+ "Heatwave",
+ "Heavyhorn",
+ "Helixite",
+ "Herbivore",
+ "Hexagon",
+ "Hillsnake",
+ "Hollowawk",
+ "Honeycomb",
+ "Hornetail",
+ "Hoverbug",
+ "Hummingwing",
+ "Hydrocoil",
+ "Icefang",
+ "Icicleon",
+ "Igniteon",
+ "Illumite",
+ "Ironhide",
+ "Jadeon",
+ "Jasperite",
+ "Jetstream",
+ "Jungleop",
+ "Juniperyn",
+ "Kelpfin",
+ "Kindlefly",
+ "Kingfisher",
+ "Knightowl",
+ "Knucklehead",
+ "Labyrinth",
+ "Lagooner",
+ "Lavashell",
+ "Leafwing",
+ "Leapingfrog",
+ "Lightbeam",
+ "Lightningbug",
+ "Limestone",
+ "Liquidite",
+ "Lunamoth",
+ "Magmite",
+ "Malachite",
+ "Mantisect",
+ "Marbleon",
+ "Marshwiggle",
+ "Maskito",
+ "Meadowfin",
+ "Megaton",
+ "Melodyte",
+ "Meteoric",
+ "Midnightowl",
+ "Mistralyn",
+ "Moltenix",
+ "Moonbeam",
+ "Mossback",
+ "Mudskipper",
+ "Mysticlaw",
+ "Nectarin",
+ "Netherfang",
+ "Nightshade",
+ "Nimbusowl",
+ "Nocturne",
+ "Novaflare",
+ "Nuggeteer",
+ "Obsidian",
+ "Oceanaut",
+ "Opalite",
+ "Orbitron",
+ "Overgrowth",
+ "Oxideon",
+ "Ozonefly",
+ "Palestone",
+ "Panthera",
+ "Parallax",
+ "Patchleaf",
+ "Pebblepuff",
+ "Pendulum",
+ "Peridot",
+ "Phantomist",
+ "Phasewalk",
+ "Pinecone",
+ "Pinwheel",
+ "Pixelite",
+ "Plainsrunner",
+ "Plasmafin",
+ "Plumbob",
+ "Poisonivy",
+ "Polaris",
+ "Pollenpuff",
+ "Pondskater",
+ "Prickleback",
+ "Prismite",
+ "Pumiceon",
+ "Pyrefly",
+ "Quakehorn",
+ "Quartzite",
+ "Quicksilver",
+ "Radiant",
+ "Raindrop",
+ "Raptoros",
+ "Razorfin",
+ "Reefwalker",
+ "Ripplefin",
+ "Riverunner",
+ "Rockhopper",
+ "Rubblebug",
+ "Rustmite",
+ "Saberfang",
+ "Saphireon",
+ "Scarabite",
+ "Scorchpaw",
+ "Seabreeze",
+ "Seaslug",
+ "Shadowclaw",
+ "Sharpfin",
+ "Shellshock",
+ "Shimmeron",
+ "Shockwave",
+ "Silicaon",
+ "Silverwing",
+ "Skitterbug",
+ "Skywhale",
+ "Slagpile",
+ "Sleetfoot",
+ "Smoketail",
+ "Snaggletooth",
+ "Snakeweed",
+ "Snowdrift",
+ "Solaris",
+ "Sonicboom",
+ "Sparkfly",
+ "Spectrite",
+ "Spikeball",
+ "Springtail",
+ "Stagbeetle",
+ "Starblaze",
+ "Stonefish",
+ "Stormcloud",
+ "Streamer",
+ "Strikewing",
+ "Sunbeam",
+ "Sunstone",
+ "Swampfin",
+ "Swiftail",
+ "Sycamore",
+ "Tanglefoot",
+ "Tarnish",
+ "Terraform",
+ "Thornback",
+ "Thunderbug",
+ "Tidalwave",
+ "Timberwolf",
+ "Tinytail",
+ "Topazite",
+ "Torrential",
+ "Toxiclaw",
+ "Tranquil",
+ "Tremorix",
+ "Tribyte",
+ "Tricorne",
+ "Twilight",
+ "Twisteron",
+ "Undergrowth",
+ "Undertow",
+ "Unicorn",
+ "Valiant",
+ "Vaporize",
+ "Venomite",
+ "Veridian",
+ "Vibraharp",
+ "Volcanic",
+ "Voltwing",
+ "Vortexon",
+ "Wallowby",
+ "Warpwing",
+ "Waterbug",
+ "Wavecrest",
+ "Waxwing",
+ "Wildfire",
+ "Windigo",
+ "Wispfire",
+ "Woodsprite",
+ "Wormhole",
+ "Wyvernix",
+ "Xenonix",
+ "Zephyron",
+ "Ziggurat",
+ "Zincite",
)
}
-}
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/navigation/LocalComposeNavigator.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/navigation/LocalComposeNavigator.kt
index f77ab07..fad499e 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/navigation/LocalComposeNavigator.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/navigation/LocalComposeNavigator.kt
@@ -25,7 +25,7 @@
compositionLocalOf {
error(
"No AppComposeNavigator provided! " +
- "Make sure to wrap all usages of Pokedex components in PokedexTheme.",
+ "Make sure to wrap all usages of Pokedex components in PokedexTheme."
)
}
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/navigation/NavigationAnimation.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/navigation/NavigationAnimation.kt
index 4644c89..10063de 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/navigation/NavigationAnimation.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/navigation/NavigationAnimation.kt
@@ -17,6 +17,11 @@
package com.skydoves.pokedex.compose.core.navigation
import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
import androidx.compose.ui.geometry.Rect
-val boundsTransform = { _: Rect, _: Rect -> tween<Rect>(550) }
+val navigationEnterTransition = fadeIn(tween(PokedexTransitionDurationMs))
+val navigationExitTransition = fadeOut(tween(PokedexTransitionDurationMs))
+val boundsTransform = { _: Rect, _: Rect -> tween<Rect>(PokedexTransitionDurationMs) }
+const val PokedexTransitionDurationMs = 700
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/navigation/NavigationCommand.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/navigation/NavigationCommand.kt
index fb3fc23..e6b7ed4 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/navigation/NavigationCommand.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/navigation/NavigationCommand.kt
@@ -23,15 +23,15 @@
}
sealed interface ComposeNavigationCommand : NavigationCommand {
- data class NavigateToRoute<T : Any>(val route: T, val options: NavOptions? = null) :
+ data class NavigateToRoute<T : PokedexScreen>(val route: T, val options: NavOptions? = null) :
ComposeNavigationCommand
- data class NavigateUpWithResult<R, T : Any>(
+ data class NavigateUpWithResult<R, T : PokedexScreen>(
val key: String,
val result: R,
val route: T? = null,
) : ComposeNavigationCommand
- data class PopUpToRoute<T : Any>(val route: T, val inclusive: Boolean) :
+ data class PopUpToRoute<T : PokedexScreen>(val route: T, val inclusive: Boolean) :
ComposeNavigationCommand
}
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/navigation/Navigator.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/navigation/Navigator.kt
index 1d54821..e94eb4e 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/navigation/Navigator.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/navigation/Navigator.kt
@@ -37,7 +37,7 @@
}
}
-abstract class AppComposeNavigator<T : Any> : Navigator() {
+abstract class AppComposeNavigator<T : PokedexScreen> : Navigator() {
abstract fun navigate(route: T, optionsBuilder: (NavOptionsBuilder.() -> Unit)? = null)
abstract fun <R> navigateBackWithResult(key: String, result: R, route: T?)
@@ -56,14 +56,11 @@
private fun NavController.handleComposeNavigationCommand(navigationCommand: NavigationCommand) {
when (navigationCommand) {
is ComposeNavigationCommand.NavigateToRoute<*> -> {
- navigate(navigationCommand.route, navigationCommand.options)
+ navigate(navigationCommand.route.asRoute(), navigationCommand.options)
}
NavigationCommand.NavigateUp -> navigateUp()
is ComposeNavigationCommand.PopUpToRoute<*> ->
- popBackStack(
- navigationCommand.route,
- navigationCommand.inclusive,
- )
+ popBackStack(navigationCommand.route.asRoute(), navigationCommand.inclusive)
is ComposeNavigationCommand.NavigateUpWithResult<*, *> -> {
navUpWithResult(navigationCommand)
}
@@ -71,17 +68,13 @@
}
private fun NavController.navUpWithResult(
- navigationCommand: ComposeNavigationCommand.NavigateUpWithResult<*, *>,
+ navigationCommand: ComposeNavigationCommand.NavigateUpWithResult<*, *>
) {
val backStackEntry =
- navigationCommand.route?.let { getBackStackEntry(it) } ?: previousBackStackEntry
- backStackEntry
- ?.savedStateHandle
- ?.set(
- navigationCommand.key,
- navigationCommand.result,
- )
+ navigationCommand.route?.let { getBackStackEntry(it.asRoute()) }
+ ?: previousBackStackEntry
+ backStackEntry?.savedStateHandle?.set(navigationCommand.key, navigationCommand.result)
- navigationCommand.route?.let { popBackStack(it, false) } ?: navigateUp()
+ navigationCommand.route?.let { popBackStack(it.asRoute(), false) } ?: navigateUp()
}
}
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/navigation/PokedexComposeNavigator.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/navigation/PokedexComposeNavigator.kt
index 2bd72c8..03dc29c 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/navigation/PokedexComposeNavigator.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/navigation/PokedexComposeNavigator.kt
@@ -28,10 +28,7 @@
override fun navigateAndClearBackStack(route: PokedexScreen) {
navigationCommands.tryEmit(
- ComposeNavigationCommand.NavigateToRoute(
- route,
- navOptions { popUpTo(0) },
- ),
+ ComposeNavigationCommand.NavigateToRoute(route, navOptions { popUpTo(0) })
)
}
@@ -41,11 +38,7 @@
override fun <R> navigateBackWithResult(key: String, result: R, route: PokedexScreen?) {
navigationCommands.tryEmit(
- ComposeNavigationCommand.NavigateUpWithResult(
- key = key,
- result = result,
- route = route,
- ),
+ ComposeNavigationCommand.NavigateUpWithResult(key = key, result = result, route = route)
)
}
}
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/navigation/PokedexScreen.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/navigation/PokedexScreen.kt
index e99c3a3..eb59644 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/navigation/PokedexScreen.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/navigation/PokedexScreen.kt
@@ -37,11 +37,24 @@
import kotlinx.serialization.Serializable
sealed interface PokedexScreen {
- @Serializable object Home : PokedexScreen
+ fun asRoute(): String
+
+ @Serializable
+ object Home : PokedexScreen {
+ const val NAVIGATION_ROUTE = "home"
+
+ override fun asRoute() = NAVIGATION_ROUTE
+ }
@Serializable
data class Details(val pokemon: Pokemon) : PokedexScreen {
+ override fun asRoute() = createRoute(pokemon.name)
+
companion object {
+ const val NAVIGATION_ROUTE = "pokemon/{name}/"
+
+ fun createRoute(name: String) = "pokemon/$name/"
+
val typeMap = mapOf(typeOf<Pokemon>() to PokemonType)
}
}
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/network/PokedexAppDispatchers.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/network/PokedexAppDispatchers.kt
index 52021b9..e272254 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/network/PokedexAppDispatchers.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/network/PokedexAppDispatchers.kt
@@ -22,5 +22,5 @@
annotation class Dispatcher(@Suppress("Unused") val pokedexAppDispatchers: PokedexAppDispatchers)
enum class PokedexAppDispatchers {
- IO,
+ IO
}
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/network/di/ModuleLocator.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/network/di/ModuleLocator.kt
index bdd7e89..3aab54e 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/network/di/ModuleLocator.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/network/di/ModuleLocator.kt
@@ -37,13 +37,13 @@
val networkModule by lazy {
NetworkModule(
json = serializationModule.json,
- networkCoroutineContext = dispatchersModule.io
+ networkCoroutineContext = dispatchersModule.io,
)
}
val databaseModule by lazy {
DatabaseModule(
context = requireNotNull(context) { "Please attach the context using attach" },
- json = serializationModule.json
+ json = serializationModule.json,
)
}
val repositoryModule by lazy {
@@ -52,7 +52,7 @@
databaseModule.pokemonDao,
databaseModule.pokemonInfoDao,
dispatchersModule.io,
- networkModule.baseUrl
+ networkModule.baseUrl,
)
}
}
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/network/di/NetworkModule.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/network/di/NetworkModule.kt
index 0172c24..51250b6 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/network/di/NetworkModule.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/network/di/NetworkModule.kt
@@ -82,16 +82,12 @@
fun okHttpClientFactory(): OkHttpClient {
return OkHttpClient.Builder()
- .apply {
- if (true) {
- this.addNetworkInterceptor(
- HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }
- )
- }
- }
+ .addNetworkInterceptor(
+ HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }
+ )
.sslSocketFactory(
sslSocketFactory = localhostCertificates.sslSocketFactory(),
- trustManager = localhostCertificates.trustManager
+ trustManager = localhostCertificates.trustManager,
)
.build()
}
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/network/model/PokemonResponse.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/network/model/PokemonResponse.kt
index d87b7b7..e7f4798 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/network/model/PokemonResponse.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/network/model/PokemonResponse.kt
@@ -16,6 +16,7 @@
package com.skydoves.pokedex.compose.core.network.model
+import com.skydoves.pokedex.compose.core.model.AllPokemonNames
import com.skydoves.pokedex.compose.core.model.PokemonNetworkModel
import com.skydoves.pokedex.compose.core.model.fakePokemonNetworkModels
import kotlinx.serialization.SerialName
@@ -35,5 +36,6 @@
* @param pokemons The pokemons to be contained in the response, a list of generated items with fake
* data by default.
*/
-fun fakePokemonResponse(pokemons: List<PokemonNetworkModel> = fakePokemonNetworkModels()) =
- PokemonResponse(count = pokemons.size, previous = null, next = null, results = pokemons)
+fun fakePokemonResponse(
+ pokemons: List<PokemonNetworkModel> = fakePokemonNetworkModels(AllPokemonNames)
+) = PokemonResponse(count = pokemons.size, previous = null, next = null, results = pokemons)
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/network/service/PokedexClient.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/network/service/PokedexClient.kt
index 51c24a3..dbac51e 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/network/service/PokedexClient.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/network/service/PokedexClient.kt
@@ -23,10 +23,7 @@
suspend fun fetchPokemonList(page: Int): Result<PokemonResponse> =
kotlin.runCatching {
- pokedexService.fetchPokemonList(
- limit = PAGING_SIZE,
- offset = page * PAGING_SIZE,
- )
+ pokedexService.fetchPokemonList(limit = PAGING_SIZE, offset = page * PAGING_SIZE)
}
suspend fun fetchPokemonInfo(name: String): Result<PokemonInfo> =
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/network/service/PokedexMockWebServer.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/network/service/PokedexMockWebServer.kt
index c3f8c1d..8ed2629 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/network/service/PokedexMockWebServer.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/network/service/PokedexMockWebServer.kt
@@ -18,8 +18,10 @@
import android.graphics.Bitmap
import androidx.compose.integration.hero.common.implementation.GradientBitmap
-import com.skydoves.pokedex.compose.core.model.FakeRandomizedNames
+import com.skydoves.pokedex.compose.core.model.AllPokemonNames
import com.skydoves.pokedex.compose.core.model.fakePokemonInfo
+import com.skydoves.pokedex.compose.core.model.fakePokemonNames
+import com.skydoves.pokedex.compose.core.model.fakePokemonNetworkModels
import com.skydoves.pokedex.compose.core.network.model.fakePokemonResponse
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@@ -48,7 +50,7 @@
val response =
try {
when {
- pokemonEndpointRegex.matches(requestPath) -> pokemonHandler()
+ pokemonEndpointRegex.matches(requestPath) -> pokemonHandler(request)
pokemonInfoEndpointRegex.matches(requestPath) -> pokemonInfoHandler(request)
pokemonImageEndpointRegex.matches(requestPath) -> pokemonImageHandler(request)
else -> MockResponse().setResponseCode(404)
@@ -62,10 +64,19 @@
return response
}
- private fun pokemonHandler(): MockResponse {
- return MockResponse()
- .setResponseCode(200)
- .setBody(json.encodeToString(fakePokemonResponse()))
+ private fun pokemonHandler(request: RecordedRequest): MockResponse {
+ val requestUrl = request.requestUrl
+ if (requestUrl == null) return MockResponse().setResponseCode(404)
+ val maxPokemon = requestUrl.queryParameter("limit")?.toInt() ?: 20
+ val fetchingOffset = requestUrl.queryParameter("offset")?.toInt() ?: 0
+ val response =
+ fakePokemonResponse(
+ pokemons =
+ fakePokemonNetworkModels(
+ pokemonNames = fakePokemonNames(limit = maxPokemon, offset = fetchingOffset)
+ )
+ )
+ return MockResponse().setResponseCode(200).setBody(json.encodeToString(response))
}
private fun pokemonInfoHandler(request: RecordedRequest): MockResponse {
@@ -74,13 +85,15 @@
val pokemonName = requestUrl.pathSegments.last()
val fakePokemonInfo =
json.encodeToString(
- fakePokemonInfo(id = FakeRandomizedNames.indexOf(pokemonName), name = pokemonName)
+ fakePokemonInfo(id = AllPokemonNames.indexOf(pokemonName), name = pokemonName)
)
return MockResponse().setResponseCode(200).setBody(fakePokemonInfo)
}
private fun pokemonImageHandler(request: RecordedRequest): MockResponse {
- val pathSegments = request.requestUrl!!.pathSegments
+ val requestUrl = request.requestUrl
+ if (requestUrl == null) return MockResponse().setResponseCode(404)
+ val pathSegments = requestUrl.pathSegments
val pokemonName = pathSegments[pathSegments.size - 2]
val image = GradientBitmap(width = 500, height = 500, seed = pokemonName.hashCode())
val buffer = Buffer()
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/preview/PokedexPreviewTheme.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/preview/PokedexPreviewTheme.kt
index 01c84aa..2a00e29 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/preview/PokedexPreviewTheme.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/preview/PokedexPreviewTheme.kt
@@ -29,11 +29,9 @@
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun PokedexPreviewTheme(
- content: @Composable SharedTransitionScope.(AnimatedVisibilityScope) -> Unit,
+ content: @Composable SharedTransitionScope.(AnimatedVisibilityScope) -> Unit
) {
- CompositionLocalProvider(
- LocalComposeNavigator provides PokedexComposeNavigator(),
- ) {
+ CompositionLocalProvider(LocalComposeNavigator provides PokedexComposeNavigator()) {
PokedexTheme {
SharedTransitionScope { modifier ->
AnimatedVisibility(modifier = modifier, visible = true, label = "") {
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/preview/PreviewUtils.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/preview/PreviewUtils.kt
index 058a1e4..8da64cc 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/preview/PreviewUtils.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/preview/PreviewUtils.kt
@@ -23,18 +23,14 @@
@Suppress("Unused")
fun mockPokemon() =
- Pokemon(
- page = 0,
- name = "bulbasaur",
- imageUrl = "https://pokeapi.co/api/v2/pokemon/1/",
- )
+ Pokemon(page = 0, name = "bulbasaur", imageUrl = "https://pokeapi.co/api/v2/pokemon/1/")
fun mockPokemonList() =
List(10) {
Pokemon(
page = 0,
name = "bulbasaur$it",
- imageUrl = "https://pokeapi.co/api/v2/pokemon/1/"
+ imageUrl = "https://pokeapi.co/api/v2/pokemon/1/",
)
}
@@ -55,22 +51,22 @@
PokemonInfo.StatsResponse(
baseStat = 20,
effort = 0,
- stat = PokemonInfo.Stat("hp")
+ stat = PokemonInfo.Stat("hp"),
),
PokemonInfo.StatsResponse(
baseStat = 40,
effort = 0,
- stat = PokemonInfo.Stat("attack")
+ stat = PokemonInfo.Stat("attack"),
),
PokemonInfo.StatsResponse(
baseStat = 60,
effort = 0,
- stat = PokemonInfo.Stat("defense")
+ stat = PokemonInfo.Stat("defense"),
),
PokemonInfo.StatsResponse(
baseStat = 80,
effort = 0,
- stat = PokemonInfo.Stat("attack")
+ stat = PokemonInfo.Stat("attack"),
),
),
)
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/viewmodel/PokedexViewModelFactory.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/viewmodel/PokedexViewModelFactory.kt
index 9765c77..8573547 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/viewmodel/PokedexViewModelFactory.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/viewmodel/PokedexViewModelFactory.kt
@@ -16,22 +16,14 @@
package com.skydoves.pokedex.compose.core.viewmodel
-import androidx.compose.runtime.compositionLocalWithComputedDefaultOf
-import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.skydoves.pokedex.compose.core.di.RepositoryModule
-import com.skydoves.pokedex.compose.core.network.di.ModuleLocator
import com.skydoves.pokedex.compose.feature.details.DetailsViewModel
import com.skydoves.pokedex.compose.feature.home.HomeViewModel
-val LocalPokedexViewModelFactory = compositionLocalWithComputedDefaultOf {
- ModuleLocator.attach(context = { LocalContext.currentValue })
- PokedexViewModelFactory(ModuleLocator.repositoryModule)
-}
-
-fun PokedexViewModelFactory(repositoryModule: RepositoryModule) = viewModelFactory {
+fun pokedexViewModelFactory(repositoryModule: RepositoryModule) = viewModelFactory {
initializer { DetailsViewModel(repositoryModule.detailsRepository, createSavedStateHandle()) }
initializer { HomeViewModel(repositoryModule.homeRepository) }
}
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/viewmodel/ViewModelStateFlow.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/viewmodel/ViewModelStateFlow.kt
index 0ba4a27..ff7c0a0 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/viewmodel/ViewModelStateFlow.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/viewmodel/ViewModelStateFlow.kt
@@ -17,6 +17,7 @@
package com.skydoves.pokedex.compose.core.viewmodel
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.ExperimentalForInheritanceCoroutinesApi
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -30,12 +31,12 @@
* once Kotlin 2.0 stable version is released and the new Compose compiler is compatible with Kotlin
* 2.0.
*/
+// TODO: Remove this class b/400932000
+@OptIn(ExperimentalForInheritanceCoroutinesApi::class)
class ViewModelStateFlow<T>(private val key: ViewModelKey, value: T) : MutableStateFlow<T> {
private val mutableStateFlow: MutableStateFlow<Map<ViewModelKey, T>> =
- MutableStateFlow(
- mapOf(key to value),
- )
+ MutableStateFlow(mapOf(key to value))
override val subscriptionCount: StateFlow<Int>
get() = mutableStateFlow.subscriptionCount
@@ -45,7 +46,7 @@
if (key != this.key) {
throw IllegalArgumentException(
"Used different key to emit new value: $value!" +
- "Don't manipulate key value or try to emit out of ViewModels",
+ "Don't manipulate key value or try to emit out of ViewModels"
)
}
@@ -60,7 +61,7 @@
if (key != this.key) {
throw IllegalArgumentException(
"Used different key to emit new value: $value!" +
- "Don't manipulate key value or try to emit out of ViewModels",
+ "Don't manipulate key value or try to emit out of ViewModels"
)
}
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/feature/details/DetailsBackground.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/feature/details/DetailsBackground.kt
index e5142eb..b7758ff 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/feature/details/DetailsBackground.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/feature/details/DetailsBackground.kt
@@ -36,11 +36,7 @@
val domainColor = Color(domain)
if (light != null) {
val lightColor = Color(light)
- val gradient =
- arrayOf(
- 0.0f to domainColor,
- 1f to lightColor,
- )
+ val gradient = arrayOf(0.0f to domainColor, 1f to lightColor)
Brush.verticalGradient(colorStops = gradient)
} else {
Brush.linearGradient(colors = listOf(domainColor, domainColor))
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/feature/details/DetailsViewModel.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/feature/details/DetailsViewModel.kt
index 2b0fd6b..1f985e4 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/feature/details/DetailsViewModel.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/feature/details/DetailsViewModel.kt
@@ -20,7 +20,6 @@
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.skydoves.pokedex.compose.core.data.repository.details.DetailsRepository
-import com.skydoves.pokedex.compose.core.model.Pokemon
import com.skydoves.pokedex.compose.core.model.PokemonInfo
import com.skydoves.pokedex.compose.core.viewmodel.BaseViewModel
import com.skydoves.pokedex.compose.core.viewmodel.ViewModelStateFlow
@@ -31,22 +30,20 @@
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.stateIn
-class DetailsViewModel(
- detailsRepository: DetailsRepository,
- savedStateHandle: SavedStateHandle,
-) : BaseViewModel() {
+class DetailsViewModel(detailsRepository: DetailsRepository, savedStateHandle: SavedStateHandle) :
+ BaseViewModel() {
internal val uiState: ViewModelStateFlow<DetailsUiState> =
viewModelStateFlow(DetailsUiState.Loading)
- val pokemon = savedStateHandle.getStateFlow<Pokemon?>("pokemon", null)
+ val pokemonName = savedStateHandle.getStateFlow<String?>("name", null)
@OptIn(ExperimentalCoroutinesApi::class)
val pokemonInfo: StateFlow<PokemonInfo?> =
- pokemon
+ pokemonName
.filterNotNull()
.flatMapLatest { pokemon ->
detailsRepository.fetchPokemonInfo(
- name = pokemon.name.replaceFirstChar { it.lowercase() },
+ name = pokemon.replaceFirstChar { it.lowercase() },
onComplete = { uiState.tryEmit(key, DetailsUiState.Idle) },
onError = { uiState.tryEmit(key, DetailsUiState.Error(it)) },
)
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/feature/details/PokedexDetails.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/feature/details/PokedexDetails.kt
index ef9182b..999d5de 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/feature/details/PokedexDetails.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/feature/details/PokedexDetails.kt
@@ -22,6 +22,7 @@
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
+import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -47,10 +48,13 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@@ -60,45 +64,53 @@
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.palette.graphics.Palette
+import coil3.compose.AsyncImage
+import coil3.request.ImageRequest
+import coil3.request.crossfade
import com.bumptech.glide.integration.compose.CrossFade
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
import com.bumptech.glide.integration.compose.GlideImage
import com.bumptech.glide.integration.compose.placeholder
import com.skydoves.pokedex.compose.R
+import com.skydoves.pokedex.compose.core.PokedexFeatureFlags
import com.skydoves.pokedex.compose.core.data.repository.details.FakeDetailsRepository
+import com.skydoves.pokedex.compose.core.database.entitiy.mapper.getPokemonImageUrlByName
import com.skydoves.pokedex.compose.core.designsystem.component.PokedexCircularProgress
import com.skydoves.pokedex.compose.core.designsystem.component.PokedexText
import com.skydoves.pokedex.compose.core.designsystem.component.pokedexSharedElement
import com.skydoves.pokedex.compose.core.designsystem.theme.PokedexTheme
import com.skydoves.pokedex.compose.core.designsystem.utils.getPokemonTypeColor
-import com.skydoves.pokedex.compose.core.model.Pokemon
import com.skydoves.pokedex.compose.core.model.PokemonInfo
import com.skydoves.pokedex.compose.core.navigation.boundsTransform
import com.skydoves.pokedex.compose.core.navigation.currentComposeNavigator
import com.skydoves.pokedex.compose.core.preview.PokedexPreviewTheme
import com.skydoves.pokedex.compose.core.preview.PreviewUtils
-import com.skydoves.pokedex.compose.core.viewmodel.LocalPokedexViewModelFactory
@Composable
fun PokedexDetails(
- sharedTransitionScope: SharedTransitionScope,
+ sharedTransitionScope: SharedTransitionScope?,
animatedVisibilityScope: AnimatedVisibilityScope,
- detailsViewModel: DetailsViewModel = viewModel(factory = LocalPokedexViewModelFactory.current),
+ detailsViewModel: DetailsViewModel,
) {
val uiState by detailsViewModel.uiState.collectAsStateWithLifecycle()
- val pokemon by detailsViewModel.pokemon.collectAsStateWithLifecycle()
+ val pokemonName by detailsViewModel.pokemonName.collectAsStateWithLifecycle()
val pokemonInfo by detailsViewModel.pokemonInfo.collectAsStateWithLifecycle()
Column(
modifier =
- Modifier.fillMaxSize().verticalScroll(rememberScrollState()).testTag("PokedexDetails"),
+ Modifier.fillMaxSize().verticalScroll(rememberScrollState()).testTag("PokedexDetails")
) {
DetailsHeader(
sharedTransitionScope = sharedTransitionScope,
animatedVisibilityScope = animatedVisibilityScope,
- pokemon = pokemon,
+ pokemonName = pokemonName,
pokemonInfo = pokemonInfo,
)
+ if (sharedTransitionScope != null) {
+ val statusText =
+ "pokedex-details-transition-active-${sharedTransitionScope.isTransitionActive}"
+ Text(statusText, Modifier.semantics { testTag = statusText })
+ }
if (uiState == DetailsUiState.Idle && pokemonInfo != null) {
DetailsInfo(pokemonInfo = pokemonInfo!!)
@@ -110,23 +122,17 @@
}
}
-@OptIn(ExperimentalGlideComposeApi::class)
@Composable
private fun DetailsHeader(
- sharedTransitionScope: SharedTransitionScope,
+ sharedTransitionScope: SharedTransitionScope?,
animatedVisibilityScope: AnimatedVisibilityScope,
- pokemon: Pokemon?,
+ pokemonName: String?,
pokemonInfo: PokemonInfo?,
) {
val composeNavigator = currentComposeNavigator
val palette by remember { mutableStateOf<Palette?>(null) }
val shape =
- RoundedCornerShape(
- topStart = 0.dp,
- topEnd = 0.dp,
- bottomStart = 64.dp,
- bottomEnd = 64.dp,
- )
+ RoundedCornerShape(topStart = 0.dp, topEnd = 0.dp, bottomStart = 64.dp, bottomEnd = 64.dp)
val backgroundBrush by palette.paletteBackgroundBrush()
@@ -135,14 +141,17 @@
Modifier.fillMaxWidth()
.height(290.dp)
.shadow(elevation = 9.dp, shape = shape)
- .background(brush = backgroundBrush, shape = shape),
+ .background(brush = backgroundBrush, shape = shape)
) {
Row(
modifier = Modifier.padding(12.dp).statusBarsPadding(),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
- modifier = Modifier.padding(end = 6.dp).clickable { composeNavigator.navigateUp() },
+ modifier =
+ Modifier.testTag("pokedexDetailsBack").padding(end = 6.dp).clickable {
+ composeNavigator.navigateUp()
+ },
painter = painterResource(id = R.drawable.ic_arrow),
tint = PokedexTheme.colors.absoluteWhite,
contentDescription = null,
@@ -150,7 +159,7 @@
Text(
modifier = Modifier.padding(horizontal = 10.dp),
- text = pokemon?.name.orEmpty(),
+ text = pokemonName.orEmpty(),
color = PokedexTheme.colors.absoluteWhite,
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
@@ -166,26 +175,29 @@
fontSize = 18.sp,
)
- GlideImage(
+ PokemonHeaderImage(
+ pokemonName,
modifier =
Modifier.align(Alignment.BottomCenter)
.padding(bottom = 20.dp)
.size(190.dp)
- .pokedexSharedElement(
- sharedTransitionScope = sharedTransitionScope,
- isLocalInspectionMode = LocalInspectionMode.current,
- state =
- sharedTransitionScope.rememberSharedContentState(
- key = "image-${pokemon?.name}"
- ),
- animatedVisibilityScope = animatedVisibilityScope,
- boundsTransform = boundsTransform,
+ .then(
+ if (
+ sharedTransitionScope != null &&
+ PokedexFeatureFlags.EnableSharedElementTransitions
+ ) {
+ Modifier.pokedexSharedElement(
+ sharedTransitionScope = sharedTransitionScope,
+ isLocalInspectionMode = LocalInspectionMode.current,
+ state =
+ sharedTransitionScope.rememberSharedContentState(
+ key = "image-$pokemonName"
+ ),
+ animatedVisibilityScope = animatedVisibilityScope,
+ boundsTransform = boundsTransform,
+ )
+ } else Modifier
),
- model = pokemon?.imageUrl,
- contentScale = ContentScale.Inside,
- transition = CrossFade,
- contentDescription = pokemon?.name,
- loading = placeholder(painterResource(id = R.drawable.pokemon_preview)),
)
}
@@ -193,17 +205,24 @@
modifier =
Modifier.padding(top = 24.dp)
.fillMaxWidth()
- .pokedexSharedElement(
- sharedTransitionScope = sharedTransitionScope,
- isLocalInspectionMode = LocalInspectionMode.current,
- state =
- sharedTransitionScope.rememberSharedContentState(
- key = "name-${pokemon?.name}"
- ),
- animatedVisibilityScope = animatedVisibilityScope,
- boundsTransform = boundsTransform,
+ .then(
+ if (
+ sharedTransitionScope != null &&
+ PokedexFeatureFlags.EnableSharedElementTransitions
+ ) {
+ Modifier.pokedexSharedElement(
+ sharedTransitionScope = sharedTransitionScope,
+ isLocalInspectionMode = LocalInspectionMode.current,
+ state =
+ sharedTransitionScope.rememberSharedContentState(
+ key = "name-$pokemonName"
+ ),
+ animatedVisibilityScope = animatedVisibilityScope,
+ boundsTransform = boundsTransform,
+ )
+ } else Modifier
),
- text = pokemon?.name.orEmpty(),
+ text = pokemonName.orEmpty(),
previewText = "skydoves",
color = PokedexTheme.colors.black,
fontWeight = FontWeight.Bold,
@@ -212,6 +231,37 @@
)
}
+@OptIn(ExperimentalGlideComposeApi::class)
+@Composable
+private fun PokemonHeaderImage(pokemonName: String?, modifier: Modifier) {
+ val pokemonImageUrl =
+ if (pokemonName != null) {
+ getPokemonImageUrlByName(pokemonName).toString()
+ } else null
+ if (PokedexFeatureFlags.UseCoil) {
+ AsyncImage(
+ modifier = modifier,
+ model =
+ ImageRequest.Builder(LocalContext.current)
+ .data(pokemonImageUrl)
+ .crossfade(PokemonHeaderImageCrossfadeDurationMillis)
+ .build(),
+ contentDescription = pokemonName,
+ contentScale = ContentScale.Inside,
+ placeholder = painterResource(id = R.drawable.pokemon_preview),
+ )
+ } else {
+ GlideImage(
+ modifier = modifier,
+ model = pokemonImageUrl,
+ contentScale = ContentScale.Inside,
+ transition = CrossFade(tween(PokemonHeaderImageCrossfadeDurationMillis)),
+ contentDescription = pokemonName,
+ loading = placeholder(painterResource(id = R.drawable.pokemon_preview)),
+ )
+ }
+}
+
@Composable
private fun DetailsInfo(pokemonInfo: PokemonInfo) {
Row(
@@ -253,9 +303,7 @@
}
@Composable
-private fun DetailsStatus(
- pokemonInfo: PokemonInfo,
-) {
+private fun DetailsStatus(pokemonInfo: PokemonInfo) {
Text(
modifier = Modifier.fillMaxWidth().padding(top = 22.dp, bottom = 16.dp),
text = stringResource(id = R.string.base_stats),
@@ -305,9 +353,7 @@
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PokedexDetailsStatusPreview() {
- PokedexPreviewTheme {
- DetailsStatus(
- pokemonInfo = PreviewUtils.mockPokemonInfo(),
- )
- }
+ PokedexPreviewTheme { DetailsStatus(pokemonInfo = PreviewUtils.mockPokemonInfo()) }
}
+
+private const val PokemonHeaderImageCrossfadeDurationMillis = 250
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/feature/details/PokemonInfoItem.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/feature/details/PokemonInfoItem.kt
index c2fd3b9..bff4445 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/feature/details/PokemonInfoItem.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/feature/details/PokemonInfoItem.kt
@@ -28,10 +28,7 @@
import com.skydoves.pokedex.compose.core.designsystem.theme.PokedexTheme
@Composable
-internal fun PokemonInfoItem(
- title: String?,
- content: String?,
-) {
+internal fun PokemonInfoItem(title: String?, content: String?) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
PokedexText(
modifier = Modifier.padding(10.dp),
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/feature/details/PokemonStatusItem.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/feature/details/PokemonStatusItem.kt
index 8b6f05e..653179b 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/feature/details/PokemonStatusItem.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/feature/details/PokemonStatusItem.kt
@@ -31,14 +31,8 @@
import com.skydoves.pokedex.compose.core.designsystem.theme.PokedexTheme
@Composable
-internal fun PokemonStatusItem(
- modifier: Modifier = Modifier,
- pokedexStatus: PokedexStatus,
-) {
- Row(
- modifier = modifier,
- horizontalArrangement = Arrangement.SpaceEvenly,
- ) {
+internal fun PokemonStatusItem(modifier: Modifier = Modifier, pokedexStatus: PokedexStatus) {
+ Row(modifier = modifier, horizontalArrangement = Arrangement.SpaceEvenly) {
Text(
modifier = Modifier.padding(start = 32.dp).widthIn(min = 20.dp),
text = pokedexStatus.type,
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/feature/home/PokedexHome.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/feature/home/PokedexHome.kt
index adf454e..5cc9c28 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/feature/home/PokedexHome.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/feature/home/PokedexHome.kt
@@ -23,6 +23,7 @@
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
+import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -37,7 +38,6 @@
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
-import androidx.compose.material3.CardColors
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -49,9 +49,12 @@
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.platform.LocalInspectionMode
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@@ -60,11 +63,15 @@
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.palette.graphics.Palette
+import coil3.compose.AsyncImage
+import coil3.request.ImageRequest
+import coil3.request.crossfade
import com.bumptech.glide.integration.compose.CrossFade
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
import com.bumptech.glide.integration.compose.GlideImage
import com.bumptech.glide.integration.compose.placeholder
import com.skydoves.pokedex.compose.R
+import com.skydoves.pokedex.compose.core.PokedexFeatureFlags
import com.skydoves.pokedex.compose.core.data.repository.home.FakeHomeRepository
import com.skydoves.pokedex.compose.core.designsystem.component.PokedexAppBar
import com.skydoves.pokedex.compose.core.designsystem.component.PokedexCircularProgress
@@ -76,15 +83,14 @@
import com.skydoves.pokedex.compose.core.navigation.currentComposeNavigator
import com.skydoves.pokedex.compose.core.preview.PokedexPreviewTheme
import com.skydoves.pokedex.compose.core.preview.PreviewUtils
-import com.skydoves.pokedex.compose.core.viewmodel.LocalPokedexViewModelFactory
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@Composable
fun PokedexHome(
- sharedTransitionScope: SharedTransitionScope,
+ sharedTransitionScope: SharedTransitionScope?,
animatedVisibilityScope: AnimatedVisibilityScope,
- homeViewModel: HomeViewModel = viewModel(factory = LocalPokedexViewModelFactory.current),
+ homeViewModel: HomeViewModel,
) {
val uiState by homeViewModel.uiState.collectAsStateWithLifecycle()
val pokemonList by homeViewModel.pokemonList.collectAsStateWithLifecycle()
@@ -104,19 +110,24 @@
@Composable
private fun HomeContent(
- sharedTransitionScope: SharedTransitionScope,
+ sharedTransitionScope: SharedTransitionScope?,
animatedVisibilityScope: AnimatedVisibilityScope,
uiState: HomeUiState,
pokemonList: ImmutableList<Pokemon>,
fetchNextPokemonList: () -> Unit,
) {
+ if (sharedTransitionScope != null) {
+ val statusText =
+ "pokedex-home-transition-active-${sharedTransitionScope.isTransitionActive}"
+ Text(statusText, Modifier.semantics { testTag = statusText })
+ }
Box(modifier = Modifier.fillMaxSize()) {
val gridState = rememberLazyGridState()
- LaunchedEffect(gridState) {
+ LaunchedEffect(gridState, pokemonList) {
val paginationThreshold = pokemonList.size - PaginationBufferSize
snapshotFlow { gridState.firstVisibleItemIndex >= paginationThreshold }
- .collect {
- if (uiState != HomeUiState.Loading) {
+ .collect { shouldFetchNewItems ->
+ if (shouldFetchNewItems) {
fetchNextPokemonList()
}
}
@@ -145,7 +156,7 @@
@OptIn(ExperimentalGlideComposeApi::class)
@Composable
private fun PokemonCard(
- sharedTransitionScope: SharedTransitionScope,
+ sharedTransitionScope: SharedTransitionScope?,
animatedVisibilityScope: AnimatedVisibilityScope,
pokemon: Pokemon,
) {
@@ -155,12 +166,12 @@
Card(
modifier =
- Modifier.padding(6.dp).fillMaxWidth().testTag("Pokemon").clickable {
+ Modifier.padding(6.dp).fillMaxWidth().testTag("${pokemon.name}_card").clickable {
composeNavigator.navigate(PokedexScreen.Details(pokemon = pokemon))
},
shape = RoundedCornerShape(14.dp),
colors =
- CardColors(
+ CardDefaults.cardColors(
containerColor = backgroundColor,
contentColor = backgroundColor,
disabledContainerColor = backgroundColor,
@@ -168,41 +179,51 @@
),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
) {
- GlideImage(
+ PokemonCardImage(
modifier =
Modifier.align(Alignment.CenterHorizontally)
.padding(top = 20.dp)
.size(120.dp)
- .pokedexSharedElement(
- sharedTransitionScope = sharedTransitionScope,
- isLocalInspectionMode = LocalInspectionMode.current,
- state =
- sharedTransitionScope.rememberSharedContentState(
- key = "image-${pokemon.name}"
- ),
- animatedVisibilityScope = animatedVisibilityScope,
- boundsTransform = boundsTransform,
+ .then(
+ if (
+ sharedTransitionScope != null &&
+ PokedexFeatureFlags.EnableSharedElementTransitions
+ ) {
+ Modifier.pokedexSharedElement(
+ sharedTransitionScope = sharedTransitionScope,
+ isLocalInspectionMode = LocalInspectionMode.current,
+ state =
+ sharedTransitionScope.rememberSharedContentState(
+ key = "image-${pokemon.name}"
+ ),
+ animatedVisibilityScope = animatedVisibilityScope,
+ boundsTransform = boundsTransform,
+ )
+ } else Modifier
),
- contentDescription = pokemon.name,
- model = pokemon.imageUrl,
- contentScale = ContentScale.Inside,
- transition = CrossFade,
- loading = placeholder(painterResource(id = R.drawable.pokemon_preview)),
+ pokemon = pokemon,
)
Text(
modifier =
Modifier.align(Alignment.CenterHorizontally)
.fillMaxWidth()
- .pokedexSharedElement(
- sharedTransitionScope = sharedTransitionScope,
- isLocalInspectionMode = LocalInspectionMode.current,
- state =
- sharedTransitionScope.rememberSharedContentState(
- key = "name-${pokemon.name}"
- ),
- animatedVisibilityScope = animatedVisibilityScope,
- boundsTransform = boundsTransform,
+ .then(
+ if (
+ sharedTransitionScope != null &&
+ PokedexFeatureFlags.EnableSharedElementTransitions
+ ) {
+ Modifier.pokedexSharedElement(
+ sharedTransitionScope = sharedTransitionScope,
+ isLocalInspectionMode = LocalInspectionMode.current,
+ state =
+ sharedTransitionScope.rememberSharedContentState(
+ key = "name-${pokemon.name}"
+ ),
+ animatedVisibilityScope = animatedVisibilityScope,
+ boundsTransform = boundsTransform,
+ )
+ } else Modifier
)
.padding(12.dp),
text = pokemon.name,
@@ -214,6 +235,33 @@
}
}
+@Composable
+@OptIn(ExperimentalGlideComposeApi::class)
+private fun PokemonCardImage(pokemon: Pokemon, modifier: Modifier = Modifier) {
+ if (PokedexFeatureFlags.UseCoil) {
+ AsyncImage(
+ modifier = modifier,
+ contentDescription = pokemon.name,
+ model =
+ ImageRequest.Builder(LocalContext.current)
+ .data(pokemon.imageUrl)
+ .crossfade(PokemonCardImageCrossfadeDurationMillis)
+ .build(),
+ contentScale = ContentScale.Inside,
+ placeholder = painterResource(id = R.drawable.pokemon_preview),
+ )
+ } else {
+ GlideImage(
+ modifier = modifier,
+ contentDescription = pokemon.name,
+ model = pokemon.imageUrl,
+ contentScale = ContentScale.Inside,
+ transition = CrossFade(tween(PokemonCardImageCrossfadeDurationMillis)),
+ loading = placeholder(painterResource(id = R.drawable.pokemon_preview)),
+ )
+ }
+}
+
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
@@ -225,7 +273,7 @@
animatedVisibilityScope = this,
sharedTransitionScope = this@SharedTransitionScope,
homeViewModel =
- viewModel { HomeViewModel(homeRepository = FakeHomeRepository()) }
+ viewModel { HomeViewModel(homeRepository = FakeHomeRepository()) },
)
}
}
@@ -248,4 +296,5 @@
}
}
-private const val PaginationBufferSize = 8
+private const val PaginationBufferSize = 48
+private const val PokemonCardImageCrossfadeDurationMillis = 250
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/navigation/PokedexNavHost.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/navigation/PokedexNavHost.kt
index ea11813..813dbdd 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/navigation/PokedexNavHost.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/navigation/PokedexNavHost.kt
@@ -18,20 +18,52 @@
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionLayout
+import androidx.compose.animation.SharedTransitionScope
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
+import com.skydoves.pokedex.compose.core.PokedexFeatureFlags
import com.skydoves.pokedex.compose.core.navigation.PokedexScreen
+import com.skydoves.pokedex.compose.core.navigation.navigationEnterTransition
+import com.skydoves.pokedex.compose.core.navigation.navigationExitTransition
+import com.skydoves.pokedex.compose.core.network.di.ModuleLocator
+import com.skydoves.pokedex.compose.core.viewmodel.pokedexViewModelFactory
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
-fun PokedexNavHost(navHostController: NavHostController) {
- SharedTransitionLayout {
- NavHost(
- navController = navHostController,
- startDestination = PokedexScreen.Home,
- ) {
- pokedexNavigation(this@SharedTransitionLayout)
+fun PokedexNavHost(navHostController: NavHostController, startDestination: PokedexScreen) {
+ if (PokedexFeatureFlags.EnableSharedTransitionScope) {
+ SharedTransitionLayout {
+ PokedexNavigation(
+ navHostController,
+ sharedTransitionScope = this@SharedTransitionLayout,
+ startDestination = startDestination,
+ )
}
+ } else {
+ PokedexNavigation(
+ navHostController,
+ sharedTransitionScope = null,
+ startDestination = startDestination,
+ )
+ }
+}
+
+@OptIn(ExperimentalSharedTransitionApi::class)
+@Composable
+private fun PokedexNavigation(
+ navHostController: NavHostController,
+ sharedTransitionScope: SharedTransitionScope?,
+ startDestination: PokedexScreen,
+) {
+ val viewModelFactory = remember { pokedexViewModelFactory(ModuleLocator.repositoryModule) }
+ NavHost(
+ navController = navHostController,
+ startDestination = startDestination.asRoute(),
+ enterTransition = { navigationEnterTransition },
+ exitTransition = { navigationExitTransition },
+ ) {
+ pokedexNavigation(sharedTransitionScope = sharedTransitionScope, viewModelFactory)
}
}
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/navigation/PokedexNavigation.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/navigation/PokedexNavigation.kt
index e31a779..e546460 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/navigation/PokedexNavigation.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/navigation/PokedexNavigation.kt
@@ -16,26 +16,55 @@
package com.skydoves.pokedex.compose.navigation
+import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTag
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
+import com.skydoves.pokedex.compose.core.designsystem.utils.TraceAsync
import com.skydoves.pokedex.compose.core.navigation.PokedexScreen
import com.skydoves.pokedex.compose.feature.details.PokedexDetails
import com.skydoves.pokedex.compose.feature.home.PokedexHome
@OptIn(ExperimentalSharedTransitionApi::class)
-fun NavGraphBuilder.pokedexNavigation(sharedTransitionScope: SharedTransitionScope) {
- composable<PokedexScreen.Home> {
- PokedexHome(sharedTransitionScope = sharedTransitionScope, animatedVisibilityScope = this)
- }
-
- composable<PokedexScreen.Details>(
- typeMap = PokedexScreen.Details.typeMap,
- ) {
- PokedexDetails(
+fun NavGraphBuilder.pokedexNavigation(
+ sharedTransitionScope: SharedTransitionScope?,
+ viewModelFactory: ViewModelProvider.Factory,
+) {
+ composable(PokedexScreen.Home.NAVIGATION_ROUTE) {
+ TrackTransitionStatus("home")
+ if (this.transition.isRunning) {
+ TraceAsync("Pokedex Home Navigation Transition")
+ }
+ PokedexHome(
sharedTransitionScope = sharedTransitionScope,
- animatedVisibilityScope = this
+ animatedVisibilityScope = this@composable,
+ homeViewModel = viewModel(factory = viewModelFactory),
)
}
+
+ composable(PokedexScreen.Details.NAVIGATION_ROUTE) { backStackEntry ->
+ TrackTransitionStatus("details")
+ if (this.transition.isRunning) {
+ TraceAsync("Pokedex Details Navigation Transition")
+ }
+ PokedexDetails(
+ sharedTransitionScope = sharedTransitionScope,
+ animatedVisibilityScope = this@composable,
+ detailsViewModel = viewModel(factory = viewModelFactory),
+ )
+ }
+}
+
+@Composable
+private fun AnimatedContentScope.TrackTransitionStatus(tag: String) {
+ val status = "pokedex-$tag-transition-active-${[email protected]}"
+ Text(text = status, Modifier.semantics { testTag = status })
}
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/ui/PokedexGlideAppModule.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/ui/PokedexComposeGlideAppModule.kt
similarity index 90%
rename from app/src/main/kotlin/com/skydoves/pokedex/compose/ui/PokedexGlideAppModule.kt
rename to app/src/main/kotlin/com/skydoves/pokedex/compose/ui/PokedexComposeGlideAppModule.kt
index 12a9330..5c4b35d 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/ui/PokedexGlideAppModule.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/ui/PokedexComposeGlideAppModule.kt
@@ -22,19 +22,19 @@
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
import com.bumptech.glide.load.model.GlideUrl
-import com.bumptech.glide.module.AppGlideModule
+import com.bumptech.glide.module.LibraryGlideModule
import com.skydoves.pokedex.compose.core.network.di.ModuleLocator
import java.io.InputStream
@GlideModule
-class PokedexGlideAppModule : AppGlideModule() {
+class PokedexComposeGlideAppModule : LibraryGlideModule() {
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
val okHttpClient = ModuleLocator.networkModule.okHttpClientFactory()
val loaderFactory = OkHttpUrlLoader.Factory(okHttpClient)
registry.replace<GlideUrl, InputStream>(
/* modelClass = */ GlideUrl::class.java,
/* dataClass = */ InputStream::class.java,
- /* factory = */ loaderFactory
+ /* factory = */ loaderFactory,
)
}
}
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/ui/PokedexMain.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/ui/PokedexMain.kt
index 4613103..7b19923 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/ui/PokedexMain.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/ui/PokedexMain.kt
@@ -26,6 +26,10 @@
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.compose.rememberNavController
+import coil3.ImageLoader
+import coil3.compose.setSingletonImageLoaderFactory
+import coil3.network.okhttp.OkHttpNetworkFetcherFactory
+import com.skydoves.pokedex.compose.core.PokedexFeatureFlags
import com.skydoves.pokedex.compose.core.designsystem.theme.PokedexTheme
import com.skydoves.pokedex.compose.core.navigation.AppComposeNavigator
import com.skydoves.pokedex.compose.core.navigation.LocalComposeNavigator
@@ -36,21 +40,43 @@
@Composable
fun PokedexMain(
- composeNavigator: AppComposeNavigator<PokedexScreen> = remember { PokedexComposeNavigator() }
+ composeNavigator: AppComposeNavigator<PokedexScreen> = remember { PokedexComposeNavigator() },
+ startDestination: PokedexScreen,
) {
PokedexTheme {
CompositionLocalProvider(LocalComposeNavigator provides composeNavigator) {
val context = LocalContext.current
DisposableEffect(context) {
(context as? ComponentActivity)?.enableEdgeToEdge()
- Trace.beginSection("ModuleLocator.attach")
- ModuleLocator.attach(context = { context })
- Trace.endSection()
onDispose { ModuleLocator.detach() }
}
+ Trace.beginSection("ModuleLocator.attach")
+ ModuleLocator.attach(context = { context })
+ Trace.endSection()
+ if (PokedexFeatureFlags.UseCoil) {
+ ConfigureCoil()
+ }
val navHostController = rememberNavController()
LaunchedEffect(Unit) { composeNavigator.handleNavigationCommands(navHostController) }
- PokedexNavHost(navHostController = navHostController)
+ PokedexNavHost(
+ navHostController = navHostController,
+ startDestination = startDestination,
+ )
}
}
}
+
+@Composable
+private fun ConfigureCoil() {
+ setSingletonImageLoaderFactory { context ->
+ ImageLoader.Builder(context)
+ .components {
+ add(
+ OkHttpNetworkFetcherFactory(
+ callFactory = ModuleLocator.networkModule.okHttpClient.newBuilder().build()
+ )
+ )
+ }
+ .build()
+ }
+}