Snap for 13363539 from 74b961ed0bc81a43bd67d7b6dae669958cab9b03 to androidx-xr-arcore-release

Change-Id: I5f686692b0e932725f5e8537ecbf21ebade854c4
diff --git a/app/build.gradle b/app/build.gradle
index 5f49e03..f17205a 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,3 +1,5 @@
+import androidx.build.KotlinTarget
+
 plugins {
     id("AndroidXPlugin")
     id("AndroidXComposePlugin")
@@ -14,17 +16,12 @@
     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")
-        }
-    }
-
-    buildTypes {
-        release {
-            minifyEnabled false
-            proguardFiles("proguard-rules.pro")
+            schemaDirectory("$projectDir")
         }
     }
 
@@ -33,6 +30,25 @@
     }
 }
 
+/**
+ * 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"))
@@ -43,17 +59,30 @@
     implementation(project(":compose:material3:material3"))
     implementation(project(":compose:foundation:foundation"))
     implementation(project(":compose:foundation:foundation-layout"))
+    implementation(project(":compose:integration-tests:hero:hero-common:hero-common-implementation"))
     implementation("androidx.activity:activity-compose:1.9.2")
 
     // AndroidX
     implementation(libs.androidx.core)
     implementation(libs.testRunner)
-    implementation("androidx.navigation:navigation-compose:2.8.2")
+    implementation("androidx.navigation:navigation-compose:2.8.5")
     implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4")
     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")
@@ -63,9 +92,10 @@
     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")
+    // TODO: Update to Room 2.7.0 stable once released (b/407725691)
+    implementation("androidx.room:room-runtime:2.7.0-rc03")
+    implementation("androidx.room:room-ktx:2.7.0-rc03")
+    ksp("androidx.room:room-compiler:2.7.0-rc03")
 
     // Network
     implementation("com.squareup.retrofit2:retrofit:2.11.0")
@@ -74,3 +104,8 @@
     implementation("com.squareup.okhttp3:mockwebserver:4.12.0")
     implementation("com.squareup.okhttp3:okhttp-tls:4.12.0")
 }
+
+androidx {
+    //TODO(b/402389694): Target Kotlin 2.
+    kotlinTarget = KotlinTarget.KOTLIN_1_9
+}
\ No newline at end of file
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/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 d24909b..0000000
--- a/app/schemas/com.skydoves.pokedex.compose.core.database.PokedexDatabase/4.json
+++ /dev/null
@@ -1,108 +0,0 @@
-{
-  "formatVersion": 1,
-  "database": {
-    "version": 4,
-    "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/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..a05a3c7
--- /dev/null
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/PokedexFeatureFlags.kt
@@ -0,0 +1,26 @@
+/*
+ * 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
+}
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 16a4355..b347870 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
@@ -18,8 +18,8 @@
 
 import androidx.annotation.WorkerThread
 import com.skydoves.pokedex.compose.core.database.PokemonDao
-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
@@ -28,11 +28,13 @@
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.onCompletion
 import kotlinx.coroutines.flow.onStart
+import okhttp3.HttpUrl
 
 class HomeRepositoryImpl(
     private val pokedexClient: PokedexClient,
     private val pokemonDao: PokemonDao,
     @Dispatcher(PokedexAppDispatchers.IO) private val ioDispatcher: CoroutineDispatcher,
+    private val apiUrl: HttpUrl
 ) : HomeRepository {
 
     @WorkerThread
@@ -43,20 +45,18 @@
         onError: (String?) -> Unit,
     ) =
         flow {
-                var pokemons = pokemonDao.getPokemonList(page).asDomain()
-                if (pokemons.isEmpty()) {
-                    val response = pokedexClient.fetchPokemonList(page = page)
-                    response
-                        .onSuccess { data ->
-                            pokemons = data.results
-                            pokemons.forEach { pokemon -> pokemon.page = page }
-                            pokemonDao.insertPokemonList(pokemons.asEntity())
-                            emit(pokemonDao.getAllPokemonList(page).asDomain())
-                        }
-                        .onFailure { throwable -> onError(throwable.message) }
-                } else {
-                    emit(pokemonDao.getAllPokemonList(page).asDomain())
-                }
+                // Start out by fetching cached data
+                emit(pokemonDao.getPokemonList().asPresentationModel(apiUrl, page))
+                // Afterwards, we'll make a request to the API to still get new data
+                val networkPokemonResponse = pokedexClient.fetchPokemonList(page = page)
+                networkPokemonResponse
+                    .onSuccess { data ->
+                        val networkFetchedPokemons = data.results
+                        pokemonDao.insertPokemonList(networkFetchedPokemons.asDatabaseEntity())
+                        // We re-query the database to account for concurrent modifications
+                        emit(pokemonDao.getAllPokemonList().asPresentationModel(apiUrl, page))
+                    }
+                    .onFailure { throwable -> onError(throwable.message) }
             }
             .onStart { onStart() }
             .onCompletion { onComplete() }
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 5f168d8..bef6a0b 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 = 4,
-    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/PokemonDao.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/database/PokemonDao.kt
index d93be5a..b0d5fdd 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/database/PokemonDao.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/database/PokemonDao.kt
@@ -28,9 +28,9 @@
     @Insert(onConflict = OnConflictStrategy.REPLACE)
     suspend fun insertPokemonList(pokemonList: List<PokemonEntity>)
 
-    @Query("SELECT * FROM PokemonEntity WHERE page = :page_")
-    suspend fun getPokemonList(page_: Int): List<PokemonEntity>
+    @Query("SELECT * FROM PokemonEntity") suspend fun getPokemonList(): List<PokemonEntity>
 
-    @Query("SELECT * FROM PokemonEntity WHERE page <= :page_")
-    suspend fun getAllPokemonList(page_: Int): List<PokemonEntity>
+    @Query("SELECT * FROM PokemonEntity") suspend fun getAllPokemonList(): List<PokemonEntity>
+
+    @Query("DELETE FROM PokemonEntity") suspend fun deleteAll()
 }
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/database/di/DatabaseModule.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/database/di/DatabaseModule.kt
index b525c9a..d99e519 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/database/di/DatabaseModule.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/database/di/DatabaseModule.kt
@@ -25,7 +25,7 @@
 import com.skydoves.pokedex.compose.core.database.TypeResponseConverter
 import kotlinx.serialization.json.Json
 
-class DatabaseModule(private val context: Context, private val json: Json) {
+class DatabaseModule(private val context: () -> Context, private val json: Json) {
     val typeResponseConverter: TypeResponseConverter by lazy { TypeResponseConverter(json) }
 
     val statsResponseConverter: StatsResponseConverter by lazy { StatsResponseConverter(json) }
@@ -34,7 +34,7 @@
         // fallbackToDestructiveMigration requires a parameter in Room 2.7 that's not available in
         //  2.6. Forward-compatibility checks like androidx_max_dep_versions will fail without this.
         @Suppress("DEPRECATION")
-        Room.databaseBuilder(context, PokedexDatabase::class.java, "Pokedex.db")
+        Room.databaseBuilder(context(), PokedexDatabase::class.java, "Pokedex.db")
             .fallbackToDestructiveMigration()
             .addTypeConverter(typeResponseConverter)
             .addTypeConverter(statsResponseConverter)
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/database/entitiy/PokemonEntity.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/database/entitiy/PokemonEntity.kt
index 7d2fa9a..6dbb314 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/database/entitiy/PokemonEntity.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/database/entitiy/PokemonEntity.kt
@@ -19,9 +19,4 @@
 import androidx.room.Entity
 import androidx.room.PrimaryKey
 
-@Entity
-data class PokemonEntity(
-    var page: Int = 0,
-    @PrimaryKey val name: String,
-    val url: String,
-)
+@Entity data class PokemonEntity(@PrimaryKey val name: String)
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 4edbb1b..c61d20e 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
@@ -18,34 +18,25 @@
 
 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 okhttp3.HttpUrl
 
-object PokemonEntityMapper : EntityMapper<List<Pokemon>, List<PokemonEntity>> {
+fun List<PokemonNetworkModel>.asDatabaseEntity(): List<PokemonEntity> = map { pokemon ->
+    PokemonEntity(name = pokemon.name)
+}
 
-    override fun asEntity(domain: List<Pokemon>): List<PokemonEntity> {
-        return domain.map { pokemon ->
-            PokemonEntity(
-                page = pokemon.page,
-                name = pokemon.name,
-                url = pokemon.url,
-            )
-        }
+fun List<PokemonEntity>.asPresentationModel(apiUrl: HttpUrl, page: Int = 0): List<Pokemon> =
+    map { entity ->
+        Pokemon(
+            name = entity.name.replaceFirstChar { it.uppercase() },
+            imageUrl =
+                apiUrl
+                    .newBuilder()
+                    .addPathSegment("pokemon")
+                    .addPathSegment(entity.name)
+                    .addPathSegment("image")
+                    .build()
+                    .toString(),
+            page = page
+        )
     }
-
-    override fun asDomain(entity: List<PokemonEntity>): List<Pokemon> {
-        return entity.map { pokemonEntity ->
-            Pokemon(
-                page = pokemonEntity.page,
-                nameField = pokemonEntity.name,
-                url = pokemonEntity.url,
-            )
-        }
-    }
-}
-
-fun List<Pokemon>.asEntity(): List<PokemonEntity> {
-    return PokemonEntityMapper.asEntity(this)
-}
-
-fun List<PokemonEntity>?.asDomain(): List<Pokemon> {
-    return PokemonEntityMapper.asDomain(this.orEmpty())
-}
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 1f7153a..b8790af 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
@@ -24,18 +24,20 @@
 import com.skydoves.pokedex.compose.core.database.PokemonInfoDao
 import com.skydoves.pokedex.compose.core.network.service.PokedexClient
 import kotlinx.coroutines.CoroutineDispatcher
+import okhttp3.HttpUrl
 
 class RepositoryModule(
     private val pokedexClient: PokedexClient,
     private val pokemonDao: PokemonDao,
     private val pokemonInfoDao: PokemonInfoDao,
-    private val ioDispatcher: CoroutineDispatcher
+    private val ioDispatcher: CoroutineDispatcher,
+    private val apiUrl: HttpUrl
 ) {
     val detailsRepository: DetailsRepository by lazy {
         DetailsRepositoryImpl(pokedexClient, pokemonInfoDao, ioDispatcher)
     }
 
     val homeRepository: HomeRepository by lazy {
-        HomeRepositoryImpl(pokedexClient, pokemonDao, ioDispatcher)
+        HomeRepositoryImpl(pokedexClient, pokemonDao, ioDispatcher, apiUrl)
     }
 }
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/model/Pokemon.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/model/Pokemon.kt
index b9ac4d2..941674c 100644
--- a/app/src/main/kotlin/com/skydoves/pokedex/compose/core/model/Pokemon.kt
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/model/Pokemon.kt
@@ -20,22 +20,12 @@
 import android.os.Parcel
 import android.os.Parcelable
 import androidx.compose.runtime.Immutable
-import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
-import okhttp3.HttpUrl
 
 @SuppressLint("BanParcelableUsage") // TODO(b/374318532): Migrate to VersionedParcelable
 @Immutable
 @Serializable
-data class Pokemon(
-    var page: Int = 0,
-    @SerialName(value = "name") val nameField: String,
-    @SerialName(value = "url") val url: String,
-    val imageUrl: String = imageUrlFromPokemonInfoUrl(url)
-) : Parcelable {
-
-    val name: String
-        get() = nameField.replaceFirstChar { it.uppercase() }
+data class Pokemon(var page: Int = 0, val name: String, val imageUrl: String) : Parcelable {
 
     constructor(
         parcel: Parcel
@@ -43,8 +33,8 @@
 
     override fun writeToParcel(parcel: Parcel, flags: Int) {
         parcel.writeInt(page)
-        parcel.writeString(nameField)
-        parcel.writeString(url)
+        parcel.writeString(name)
+        parcel.writeString(imageUrl)
     }
 
     override fun describeContents() = 0
@@ -55,52 +45,3 @@
         override fun newArray(size: Int): Array<Pokemon?> = arrayOfNulls(size)
     }
 }
-
-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() }
-        listOf(
-            shuffledChars.joinToString(""),
-            shuffledChars.apply { reverse() }.joinToString(""),
-            nameChars.apply { reverse() }.joinToString("")
-        )
-    }
-}
-
-/**
- * Create a list of [Pokemon] with fake names and corresponding URLs.
- *
- * @param apiBaseUrl The base URL of the API to fetch data from
- */
-fun fakePokemons(apiBaseUrl: HttpUrl): List<Pokemon> =
-    FakeRandomizedNames.mapIndexed { index, name ->
-        Pokemon(nameField = name, url = "${apiBaseUrl}pokemon/${index}/")
-    }
-
-/**
- * Derive the URL for a Pokemon's image from the Pokemon's URL
- *
- * For example, `https://localhost:3000/api/v2/pokemon/0` would become:
- * `https://localhost:3000/api/v2/pokemon/0/image`
- *
- * @param pokemonInfoUrl The URL of the pokemon details
- * @return A string with the URL for the image
- */
-private fun imageUrlFromPokemonInfoUrl(pokemonInfoUrl: String): String {
-    val separator = if (pokemonInfoUrl.endsWith("/")) "" else "/"
-    return "${pokemonInfoUrl}${separator}image"
-}
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 d2b4622..8909227 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
@@ -109,12 +109,22 @@
 
 var FakePokemonStats = listOf("hp", "attack", "speed", "defense")
 
-fun fakePokemonStats(random: Random = Random) =
-    PokemonInfo.StatsResponse(
-        baseStat = random.nextInt(),
+fun fakePokemonStats(random: Random = Random): PokemonInfo.StatsResponse {
+    val stat = PokemonInfo.Stat(FakePokemonStats.random())
+    val statMax =
+        when (stat.name) {
+            "hp" -> PokemonInfo.MAX_HP
+            "attack" -> PokemonInfo.MAX_ATTACK
+            "speed" -> PokemonInfo.MAX_SPEED
+            "defense" -> PokemonInfo.MAX_DEFENSE
+            else -> 100
+        }
+    return PokemonInfo.StatsResponse(
+        baseStat = random.nextInt(until = statMax),
         effort = random.nextInt(),
-        stat = PokemonInfo.Stat(FakePokemonStats.random())
+        stat = stat
     )
+}
 
 var FakePokemonTypes =
     listOf(
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
new file mode 100644
index 0000000..912f930
--- /dev/null
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/model/PokemonNetworkModel.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.model
+
+import androidx.compose.runtime.Immutable
+import kotlinx.serialization.Serializable
+
+@Immutable @Serializable class PokemonNetworkModel(val name: String)
+
+fun fakePokemonNetworkModels() = FakeRandomizedNames.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() }
+        listOf(
+            shuffledChars.joinToString(""),
+            shuffledChars.apply { reverse() }.joinToString(""),
+            nameChars.apply { reverse() }.joinToString("")
+        )
+    }
+}
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
new file mode 100644
index 0000000..bdd7e89
--- /dev/null
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/core/network/di/ModuleLocator.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.network.di
+
+import android.content.Context
+import com.skydoves.pokedex.compose.core.database.di.DatabaseModule
+import com.skydoves.pokedex.compose.core.di.RepositoryModule
+
+object ModuleLocator {
+
+    private var context: (() -> Context)? = null
+
+    fun attach(context: () -> Context) {
+        this.context = context
+    }
+
+    fun detach() {
+        context = null
+    }
+
+    val serializationModule by lazy { SerializationModule() }
+    val dispatchersModule by lazy { DispatchersModule() }
+    val networkModule by lazy {
+        NetworkModule(
+            json = serializationModule.json,
+            networkCoroutineContext = dispatchersModule.io
+        )
+    }
+    val databaseModule by lazy {
+        DatabaseModule(
+            context = requireNotNull(context) { "Please attach the context using attach" },
+            json = serializationModule.json
+        )
+    }
+    val repositoryModule by lazy {
+        RepositoryModule(
+            networkModule.pokedexClient,
+            databaseModule.pokemonDao,
+            databaseModule.pokemonInfoDao,
+            dispatchersModule.io,
+            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 83fa810..0172c24 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
@@ -32,7 +32,6 @@
 
 package com.skydoves.pokedex.compose.core.network.di
 
-import com.skydoves.pokedex.compose.BuildConfig
 import com.skydoves.pokedex.compose.core.network.service.PokedexClient
 import com.skydoves.pokedex.compose.core.network.service.PokedexService
 import com.skydoves.pokedex.compose.core.network.service.pokedexMockWebServer
@@ -81,10 +80,10 @@
         runBlocking(networkCoroutineContext) { mockServer.url("/api/v2/") }
     }
 
-    val okHttpClient: OkHttpClient by lazy {
-        OkHttpClient.Builder()
+    fun okHttpClientFactory(): OkHttpClient {
+        return OkHttpClient.Builder()
             .apply {
-                if (BuildConfig.DEBUG) {
+                if (true) {
                     this.addNetworkInterceptor(
                         HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }
                     )
@@ -97,6 +96,8 @@
             .build()
     }
 
+    val okHttpClient: OkHttpClient by lazy { okHttpClientFactory() }
+
     val retrofit: Retrofit by lazy {
         Retrofit.Builder()
             .client(okHttpClient)
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 2a34bc9..d87b7b7 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,26 +16,24 @@
 
 package com.skydoves.pokedex.compose.core.network.model
 
-import com.skydoves.pokedex.compose.core.model.Pokemon
-import com.skydoves.pokedex.compose.core.model.fakePokemons
+import com.skydoves.pokedex.compose.core.model.PokemonNetworkModel
+import com.skydoves.pokedex.compose.core.model.fakePokemonNetworkModels
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
-import okhttp3.HttpUrl
 
 @Serializable
 data class PokemonResponse(
     @SerialName(value = "count") val count: Int,
     @SerialName(value = "next") val next: String?,
     @SerialName(value = "previous") val previous: String?,
-    @SerialName(value = "results") val results: List<Pokemon>,
+    @SerialName(value = "results") val results: List<PokemonNetworkModel>,
 )
 
 /**
- * Create a [PokemonResponse] with a list of [pokemons], [fakePokemons] by default.
+ * Create a [PokemonResponse] with a list of [pokemons].
  *
- * @param pokemonInfoEndpointUrl The URL of the pokemon info endpoint to derive the image URL from
+ * @param pokemons The pokemons to be contained in the response, a list of generated items with fake
+ *   data by default.
  */
-fun fakePokemonResponse(
-    pokemonInfoEndpointUrl: HttpUrl,
-    pokemons: List<Pokemon> = fakePokemons(pokemonInfoEndpointUrl)
-) = PokemonResponse(count = pokemons.size, previous = null, next = null, results = pokemons)
+fun fakePokemonResponse(pokemons: List<PokemonNetworkModel> = fakePokemonNetworkModels()) =
+    PokemonResponse(count = pokemons.size, previous = null, next = null, results = pokemons)
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 5f515de..c3f8c1d 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
@@ -16,6 +16,8 @@
 
 package com.skydoves.pokedex.compose.core.network.service
 
+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.fakePokemonInfo
 import com.skydoves.pokedex.compose.core.network.model.fakePokemonResponse
@@ -25,49 +27,64 @@
 import okhttp3.mockwebserver.MockResponse
 import okhttp3.mockwebserver.MockWebServer
 import okhttp3.mockwebserver.RecordedRequest
-import org.intellij.lang.annotations.Language
+import okio.Buffer
 
 /**
  * A [okhttp3.mockwebserver.MockWebServer] with a [Dispatcher] that sends responses with fake data
  * for our API.
  */
 fun pokedexMockWebServer(json: Json) =
-    MockWebServer().apply {
-        val pokemonEndpointRegex = Regex(PokemonEndpointPattern)
-        val pokemonInfoEndpointRegex = Regex(PokemonInfoEndpointPattern)
-        dispatcher =
-            object : Dispatcher() {
-                override fun dispatch(request: RecordedRequest): MockResponse {
-                    val requestPath = request.path
-                    if (requestPath == null) return MockResponse().setResponseCode(404)
-                    return when {
-                        pokemonEndpointRegex.matches(requestPath) -> {
-                            val mockWebServerUrl = this@apply.url("/api/v2/")
-                            val responseData = fakePokemonResponse(mockWebServerUrl)
-                            MockResponse()
-                                .setResponseCode(200)
-                                .setBody(json.encodeToString(responseData))
-                        }
-                        pokemonInfoEndpointRegex.matches(requestPath) -> {
-                            val requestUrl = request.requestUrl
-                            if (requestUrl == null) return MockResponse().setResponseCode(404)
-                            val pokemonName = requestUrl.pathSegments.last()
-                            val fakePokemonInfo =
-                                json.encodeToString(
-                                    fakePokemonInfo(
-                                        id = FakeRandomizedNames.indexOf(pokemonName),
-                                        name = pokemonName
-                                    )
-                                )
-                            return MockResponse().setResponseCode(200).setBody(fakePokemonInfo)
-                        }
-                        else -> MockResponse().setResponseCode(404)
-                    }
+    MockWebServer().apply { dispatcher = PokedexMockDispatcher(json) }
+
+/** This [Dispatcher] provides fake responses for our API. */
+private class PokedexMockDispatcher(private val json: Json) : Dispatcher() {
+    private val pokemonEndpointRegex = Regex("/api/v2/pokemon(\\?(?<query>(.*)))")
+    private val pokemonInfoEndpointRegex = Regex("/api/v2/pokemon/(?<name>\\w*)(/?)")
+    private val pokemonImageEndpointRegex = Regex("/api/v2/pokemon/(?<name>.*)/image(/?)")
+
+    override fun dispatch(request: RecordedRequest): MockResponse {
+        val requestPath = request.path
+        if (requestPath == null) return MockResponse().setResponseCode(404)
+        val response =
+            try {
+                when {
+                    pokemonEndpointRegex.matches(requestPath) -> pokemonHandler()
+                    pokemonInfoEndpointRegex.matches(requestPath) -> pokemonInfoHandler(request)
+                    pokemonImageEndpointRegex.matches(requestPath) -> pokemonImageHandler(request)
+                    else -> MockResponse().setResponseCode(404)
                 }
+            } catch (exception: Exception) {
+                exception.printStackTrace()
+                MockResponse()
+                    .setResponseCode(500)
+                    .setBody(exception.message ?: "Unknown Error Occurred")
             }
+        return response
     }
 
-@Language("RegExp") private const val PokemonEndpointPattern = "/api/v2/pokemon(\\?(?<query>(.*)))"
+    private fun pokemonHandler(): MockResponse {
+        return MockResponse()
+            .setResponseCode(200)
+            .setBody(json.encodeToString(fakePokemonResponse()))
+    }
 
-@Language("RegExp")
-private const val PokemonInfoEndpointPattern = "/api/v2/pokemon/(?<name>\\w*)(/?)"
+    private fun pokemonInfoHandler(request: RecordedRequest): MockResponse {
+        val requestUrl = request.requestUrl
+        if (requestUrl == null) return MockResponse().setResponseCode(404)
+        val pokemonName = requestUrl.pathSegments.last()
+        val fakePokemonInfo =
+            json.encodeToString(
+                fakePokemonInfo(id = FakeRandomizedNames.indexOf(pokemonName), name = pokemonName)
+            )
+        return MockResponse().setResponseCode(200).setBody(fakePokemonInfo)
+    }
+
+    private fun pokemonImageHandler(request: RecordedRequest): MockResponse {
+        val pathSegments = request.requestUrl!!.pathSegments
+        val pokemonName = pathSegments[pathSegments.size - 2]
+        val image = GradientBitmap(width = 500, height = 500, seed = pokemonName.hashCode())
+        val buffer = Buffer()
+        image.compress(Bitmap.CompressFormat.PNG, 100, buffer.outputStream())
+        return MockResponse().setResponseCode(200).setBody(buffer)
+    }
+}
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 f622d05..058a1e4 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
@@ -25,11 +25,18 @@
     fun mockPokemon() =
         Pokemon(
             page = 0,
-            nameField = "bulbasaur",
-            url = "https://pokeapi.co/api/v2/pokemon/1/",
+            name = "bulbasaur",
+            imageUrl = "https://pokeapi.co/api/v2/pokemon/1/",
         )
 
-    fun mockPokemonList() = List(10) { Pokemon(page = 0, nameField = "bulbasaur$it", url = "") }
+    fun mockPokemonList() =
+        List(10) {
+            Pokemon(
+                page = 0,
+                name = "bulbasaur$it",
+                imageUrl = "https://pokeapi.co/api/v2/pokemon/1/"
+            )
+        }
 
     fun mockPokemonInfo() =
         PokemonInfo(
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 0848a0b..9765c77 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
@@ -21,32 +21,14 @@
 import androidx.lifecycle.createSavedStateHandle
 import androidx.lifecycle.viewmodel.initializer
 import androidx.lifecycle.viewmodel.viewModelFactory
-import com.skydoves.pokedex.compose.core.database.di.DatabaseModule
 import com.skydoves.pokedex.compose.core.di.RepositoryModule
-import com.skydoves.pokedex.compose.core.network.di.DispatchersModule
-import com.skydoves.pokedex.compose.core.network.di.NetworkModule
-import com.skydoves.pokedex.compose.core.network.di.SerializationModule
+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 {
-    val serializationModule = SerializationModule()
-    val dispatchersModule = DispatchersModule()
-    val networkModule =
-        NetworkModule(
-            json = serializationModule.json,
-            networkCoroutineContext = dispatchersModule.io
-        )
-    val databaseModule =
-        DatabaseModule(context = LocalContext.currentValue, json = serializationModule.json)
-    val repositoryModule =
-        RepositoryModule(
-            networkModule.pokedexClient,
-            databaseModule.pokemonDao,
-            databaseModule.pokemonInfoDao,
-            dispatchersModule.io
-        )
-    PokedexViewModelFactory(repositoryModule)
+    ModuleLocator.attach(context = { LocalContext.currentValue })
+    PokedexViewModelFactory(ModuleLocator.repositoryModule)
 }
 
 fun PokedexViewModelFactory(repositoryModule: RepositoryModule) = viewModelFactory {
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..de57265 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,6 +31,7 @@
  * once Kotlin 2.0 stable version is released and the new Compose compiler is compatible with Kotlin
  * 2.0.
  */
+@OptIn(ExperimentalForInheritanceCoroutinesApi::class) // TODO: Remove this class b/400932000
 class ViewModelStateFlow<T>(private val key: ViewModelKey, value: T) : MutableStateFlow<T> {
 
     private val mutableStateFlow: MutableStateFlow<Map<ViewModelKey, T>> =
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 f12fbe0..2b0fd6b 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
@@ -46,7 +46,7 @@
             .filterNotNull()
             .flatMapLatest { pokemon ->
                 detailsRepository.fetchPokemonInfo(
-                    name = pokemon.nameField.replaceFirstChar { it.lowercase() },
+                    name = pokemon.name.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..784070e 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,6 +48,7 @@
 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
@@ -60,11 +62,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.details.FakeDetailsRepository
 import com.skydoves.pokedex.compose.core.designsystem.component.PokedexCircularProgress
 import com.skydoves.pokedex.compose.core.designsystem.component.PokedexText
@@ -110,7 +116,6 @@
     }
 }
 
-@OptIn(ExperimentalGlideComposeApi::class)
 @Composable
 private fun DetailsHeader(
     sharedTransitionScope: SharedTransitionScope,
@@ -166,7 +171,8 @@
             fontSize = 18.sp,
         )
 
-        GlideImage(
+        PokemonHeaderImage(
+            pokemon,
             modifier =
                 Modifier.align(Alignment.BottomCenter)
                     .padding(bottom = 20.dp)
@@ -180,12 +186,7 @@
                             ),
                         animatedVisibilityScope = animatedVisibilityScope,
                         boundsTransform = boundsTransform,
-                    ),
-            model = pokemon?.imageUrl,
-            contentScale = ContentScale.Inside,
-            transition = CrossFade,
-            contentDescription = pokemon?.name,
-            loading = placeholder(painterResource(id = R.drawable.pokemon_preview)),
+                    )
         )
     }
 
@@ -212,6 +213,33 @@
     )
 }
 
+@OptIn(ExperimentalGlideComposeApi::class)
+@Composable
+private fun PokemonHeaderImage(pokemon: Pokemon?, modifier: Modifier) {
+    if (PokedexFeatureFlags.UseCoil) {
+        AsyncImage(
+            modifier = modifier,
+            model =
+                ImageRequest.Builder(LocalContext.current)
+                    .data(pokemon?.imageUrl)
+                    .crossfade(PokemonHeaderImageCrossfadeDurationMillis)
+                    .build(),
+            contentDescription = pokemon?.name,
+            contentScale = ContentScale.Inside,
+            placeholder = painterResource(id = R.drawable.pokemon_preview),
+        )
+    } else {
+        GlideImage(
+            modifier = modifier,
+            model = pokemon?.imageUrl,
+            contentScale = ContentScale.Inside,
+            transition = CrossFade(tween(PokemonHeaderImageCrossfadeDurationMillis)),
+            contentDescription = pokemon?.name,
+            loading = placeholder(painterResource(id = R.drawable.pokemon_preview)),
+        )
+    }
+}
+
 @Composable
 private fun DetailsInfo(pokemonInfo: PokemonInfo) {
     Row(
@@ -311,3 +339,5 @@
         )
     }
 }
+
+private const val PokemonHeaderImageCrossfadeDurationMillis = 250
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 6a1c102..4cb1a37 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
@@ -33,19 +34,23 @@
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.lazy.grid.GridCells
 import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
-import androidx.compose.foundation.lazy.grid.itemsIndexed
+import androidx.compose.foundation.lazy.grid.items
+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
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshotFlow
 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
@@ -57,11 +62,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
@@ -108,18 +117,23 @@
     fetchNextPokemonList: () -> Unit,
 ) {
     Box(modifier = Modifier.fillMaxSize()) {
-        val threadHold = 8
+        val gridState = rememberLazyGridState()
+        LaunchedEffect(gridState) {
+            val paginationThreshold = pokemonList.size - PaginationBufferSize
+            snapshotFlow { gridState.firstVisibleItemIndex >= paginationThreshold }
+                .collect {
+                    if (uiState != HomeUiState.Loading) {
+                        fetchNextPokemonList()
+                    }
+                }
+        }
         LazyVerticalGrid(
+            state = gridState,
             modifier = Modifier.testTag("PokedexList"),
             columns = GridCells.Fixed(2),
             contentPadding = PaddingValues(6.dp),
         ) {
-            itemsIndexed(items = pokemonList, key = { _, pokemon -> pokemon.name }) { index, pokemon
-                ->
-                if ((index + threadHold) >= pokemonList.size && uiState != HomeUiState.Loading) {
-                    fetchNextPokemonList()
-                }
-
+            items(items = pokemonList, key = { pokemon -> pokemon.name }) { pokemon ->
                 PokemonCard(
                     animatedVisibilityScope = animatedVisibilityScope,
                     sharedTransitionScope = sharedTransitionScope,
@@ -160,7 +174,7 @@
             ),
         elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
     ) {
-        GlideImage(
+        PokemonCardImage(
             modifier =
                 Modifier.align(Alignment.CenterHorizontally)
                     .padding(top = 20.dp)
@@ -175,16 +189,7 @@
                         animatedVisibilityScope = animatedVisibilityScope,
                         boundsTransform = boundsTransform,
                     ),
-            contentDescription = pokemon.name,
-            model = pokemon.imageUrl,
-            contentScale = ContentScale.Inside,
-            transition = CrossFade,
-            loading =
-                placeholder(
-                    painterResource(
-                        id = R.drawable.pokemon_preview,
-                    )
-                ),
+            pokemon = pokemon
         )
 
         Text(
@@ -211,6 +216,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
@@ -244,3 +276,6 @@
         )
     }
 }
+
+private const val PaginationBufferSize = 8
+private const val PokemonCardImageCrossfadeDurationMillis = 250
diff --git a/app/src/main/kotlin/com/skydoves/pokedex/compose/ui/PokedexGlideAppModule.kt b/app/src/main/kotlin/com/skydoves/pokedex/compose/ui/PokedexGlideAppModule.kt
new file mode 100644
index 0000000..12a9330
--- /dev/null
+++ b/app/src/main/kotlin/com/skydoves/pokedex/compose/ui/PokedexGlideAppModule.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.ui
+
+import android.content.Context
+import com.bumptech.glide.Glide
+import com.bumptech.glide.Registry
+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.skydoves.pokedex.compose.core.network.di.ModuleLocator
+import java.io.InputStream
+
+@GlideModule
+class PokedexGlideAppModule : AppGlideModule() {
+    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
+        )
+    }
+}
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 9cfb72c..bc9c595 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
@@ -16,6 +16,7 @@
 
 package com.skydoves.pokedex.compose.ui
 
+import android.os.Trace
 import androidx.activity.ComponentActivity
 import androidx.activity.enableEdgeToEdge
 import androidx.compose.runtime.Composable
@@ -25,11 +26,16 @@
 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
 import com.skydoves.pokedex.compose.core.navigation.PokedexComposeNavigator
 import com.skydoves.pokedex.compose.core.navigation.PokedexScreen
+import com.skydoves.pokedex.compose.core.network.di.ModuleLocator
 import com.skydoves.pokedex.compose.navigation.PokedexNavHost
 
 @Composable
@@ -41,7 +47,13 @@
             val context = LocalContext.current
             DisposableEffect(context) {
                 (context as? ComponentActivity)?.enableEdgeToEdge()
-                onDispose {}
+                Trace.beginSection("ModuleLocator.attach")
+                ModuleLocator.attach(context = { context })
+                Trace.endSection()
+                onDispose { ModuleLocator.detach() }
+            }
+            if (PokedexFeatureFlags.UseCoil) {
+                ConfigureCoil()
             }
             val navHostController = rememberNavController()
             LaunchedEffect(Unit) { composeNavigator.handleNavigationCommands(navHostController) }
@@ -49,3 +61,18 @@
         }
     }
 }
+
+@Composable
+private fun ConfigureCoil() {
+    setSingletonImageLoaderFactory { context ->
+        ImageLoader.Builder(context)
+            .components {
+                add(
+                    OkHttpNetworkFetcherFactory(
+                        callFactory = ModuleLocator.networkModule.okHttpClient.newBuilder().build()
+                    )
+                )
+            }
+            .build()
+    }
+}