Techbase Academy
Android · Lesson 70

Migrate Room Database to Kotlin Multiplatform

12 phasesA Shared Room Database for FeeTrackerAndroid Studio
Phase 1 of 12
Lesson Introduction

The database is often the biggest piece of Android code that blocks sharing. In lesson 34 you used Room, which looked Android-only: @Entity, @Dao and @Database backed by the phone's SQLite. Room now supports Kotlin Multiplatform, so the same entities, DAOs and queries can live in commonMain and run on Android and iOS (and desktop). Moving the database is the last big step in sharing the data layer.

This lesson covers Migrate Room Database to Kotlin Multiplatform and the Kotlin Multiplatform quiz topics from the Kotlin Multiplatform programme (https://developer.android.com/courses/pathways/kotlin-multiplatform), following the Android Developers guide Set up Room for Kotlin Multiplatform (https://developer.android.com/kotlin/multiplatform/room) and the Room setup page (https://developer.android.com/training/data-storage/room).

By the end you will be able to:

  • Explain what makes Room portable: SQLiteDriver, KSP per target and a database constructor.
  • Move entities, DAOs and the database class to commonMain.
  • Configure KSP for every target and the schema directory.
  • Write a platform-specific database builder for Android and iOS.
  • Use the bundled SQLite driver and know why it matters.
  • Test the database from common code.
  • Review the whole programme with a quiz-style checklist.

Big idea: Room KMP changes how the database is created, not how you use it. Entities, DAOs, Flow and suspend functions stay the same.

Honesty note: this course teaches Room 3 (androidx.room3, lesson 34) with Room 2.x equivalents. Room's multiplatform setup was introduced in Room 2.7 and is carried into Room 3, but names such as the Gradle plugin, the constructor annotation, and the driver artifacts are updated from release to release, and I could not check every one against the live documentation while writing. Use the official page as the source of truth for exact imports and artifact coordinates, and use this lesson for the structure and reasoning.

✏️ Your Task
Practise what you just learned about Lesson Introduction. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 2 of 12
Prerequisite Concepts
  • Room entities, DAOs, Flow, migrations (lesson 34).
  • Repositories and offline-first design (lessons 62 and 64).
  • KMP structure, source sets, expect/actual (lessons 66 to 68).
  • KSP (lessons 34 and 65).
  • Interfaces for platform services (lesson 68).
✏️ Your Task
Practise what you just learned about Prerequisite Concepts. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 3 of 12
Conceptual Understanding: What Room Needs to Be Portable

Room on Android used the framework's SQLite through Android classes (SupportSQLiteDatabase). That does not exist on iOS. Three changes make Room portable:

Piece What it does
SQLiteDriver (from androidx.sqlite) A small, multiplatform interface for opening a database and running statements. Room talks to the driver instead of Android classes
Bundled SQLite driver A driver that ships its own copy of SQLite inside your app, so the same SQLite version runs on every platform
KSP per target Room's compiler generates code for each target, so the processor is added to each

Room 3, which you learned in lesson 34, is already built on the driver idea (you saw setDriver(AndroidSQLiteDriver())). Moving to KMP replaces the Android-only driver with a bundled one in shared code, and makes the database builder platform-specific.

The flow

text
commonMain
  @Entity, @Dao, @Database  ── generated per target by KSP ──► Android impl, iOS impl
  expect object AppDatabaseConstructor : RoomDatabaseConstructor<AppDatabase>

androidMain   builder:  Room.databaseBuilder(context, name)  + driver
iosMain       builder:  Room.databaseBuilder(path)           + driver
commonMain    getRoomDatabase(builder): AppDatabase          (applies driver, dispatcher, build)
✏️ Your Task
Practise what you just learned about Conceptual Understanding: What Room Needs to Be Portable. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 4 of 12
Step-by-Step Build 1: Dependencies and Plugins

Version catalog

libs.versions.toml
[versions]
room = "<latest stable Room 3>"
sqlite = "<latest stable androidx.sqlite>"
ksp = "<matches your Kotlin version>"
[libraries]
androidx-room-runtime = { group = "androidx.room3", name = "room3-runtime", version.ref = "room" }
androidx-room-compiler = { group = "androidx.room3", name = "room3-compiler", version.ref = "room" }
androidx-sqlite-bundled = { group = "androidx.sqlite", name = "sqlite-bundled", version.ref = "sqlite" }
[plugins]
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
androidx-room = { id = "androidx.room3", version.ref = "room" }

(The Room Gradle plugin's id has changed between Room 2.x (androidx.room) and Room 3. Check the setup page. The Room Gradle plugin is the supported way to configure the schema directory for KMP projects.)

The shared module's build file

kotlin
// shared/build.gradle.kts
plugins {
 alias(libs.plugins.kotlin.multiplatform)
 alias(libs.plugins.android.kotlin.multiplatform.library)
 alias(libs.plugins.ksp)
 alias(libs.plugins.androidx.room)
}
kotlin {
 android { namespace = "com.example.fees.shared"; compileSdk = 36; minSdk = 24 }
 iosArm64()
 iosSimulatorArm64()
 sourceSets {
 commonMain.dependencies {
 implementation(libs.androidx.room.runtime)
 implementation(libs.androidx.sqlite.bundled)
 }
 }
}
room {
 schemaDirectory("$projectDir/schemas") // committed to Git; used for migrations and tests
}
dependencies {
 // KSP must run for EVERY target. One line per target:
 add("kspAndroid", libs.androidx.room.compiler)
 add("kspIosArm64", libs.androidx.room.compiler)
 add("kspIosSimulatorArm64", libs.androidx.room.compiler)
}

The line to remember is the per-target KSP: with a single-platform project you wrote ksp(...); in KMP, Room's compiler must generate code for each target, so you add the processor to kspAndroid, kspIosArm64 and kspIosSimulatorArm64. Forgetting one gives the classic error "Room database implementation not found" when you run that platform. (If you also declare a JVM desktop target, add kspJvm.) With the dedicated Android library plugin, the configuration name for the Android target is kspAndroid; with the legacy androidTarget(), check the name KSP generates for your setup.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 1: Dependencies and Plugins. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 5 of 12
Step-by-Step Build 2: Move Entities, DAOs and the Database

Move these files to shared/src/commonMain/kotlin/.... They are almost unchanged.

Entity and DAO

kotlin
// commonMain
import androidx.room3.Dao
import androidx.room3.Entity
import androidx.room3.PrimaryKey
import androidx.room3.Query
import androidx.room3.Upsert
import kotlinx.coroutines.flow.Flow
@Entity(tableName = "payments")
data class PaymentEntity(
 @PrimaryKey val id: Int,
 val studentName: String,
 val amountKobo: Long,
 val paidAtEpochMillis: Long
)
@Dao
interface PaymentDao {
 @Query("SELECT * FROM payments ORDER BY paidAtEpochMillis DESC")
 fun observeAll(): Flow<List<PaymentEntity>>
 @Upsert
 suspend fun upsertAll(payments: List<PaymentEntity>)
 @Query("DELETE FROM payments")
 suspend fun clear()
}

Notes: nothing here refers to Android. If your entity used java.time.Instant, java.util.Date, or android.* types, change them to primitives (as here, epoch milliseconds) or write type converters that use multiplatform types (kotlinx-datetime). Type converters are written in common code and registered with the same annotation as in lesson 34 (@ColumnTypeConverter in Room 3, @TypeConverter in Room 2.x).

The database class and its constructor

On Android you could call the generated AppDatabase_Impl by reflection. On iOS there is no reflection, so Room KMP needs an explicit database constructor that you declare as an expect object. KSP generates the actual for each platform.

kotlin
// commonMain
import androidx.room3.ConstructedBy
import androidx.room3.Database
import androidx.room3.RoomDatabase
import androidx.room3.RoomDatabaseConstructor
@Database(entities = [PaymentEntity::class], version = 1, exportSchema = true)
@ConstructedBy(AppDatabaseConstructor::class)
abstract class AppDatabase : RoomDatabase() {
 abstract fun paymentDao(): PaymentDao
}
// The compiler generates the 'actual' implementations
@Suppress("KotlinNoActualForExpect")
expect object AppDatabaseConstructor : RoomDatabaseConstructor<AppDatabase> {
 override fun initialize(): AppDatabase
}

@Suppress("KotlinNoActualForExpect") hides the IDE warning that no actual exists in your source: KSP generates it at build time.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 2: Move Entities, DAOs and the Database. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 6 of 12
Step-by-Step Build 3: Platform-Specific Builders

Only one thing really differs by platform: where the database file lives. Each platform supplies a builder that knows the path, and shared code finishes the job.

Common: finish building

kotlin
// commonMain
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import kotlinx.coroutines.Dispatchers
fun getRoomDatabase(builder: RoomDatabase.Builder<AppDatabase>): AppDatabase =
 builder
 .setDriver(BundledSQLiteDriver())
 .setQueryCoroutineContext(Dispatchers.Default) // IO dispatcher is not in common code on all targets
 .build()

Two details deserve attention:

  • BundledSQLiteDriver includes SQLite inside your app. The same SQLite version and features (such as full-text search and JSON functions) then behave identically on Android and iOS, instead of depending on whatever the phone ships. The cost is a larger app (a few megabytes).
  • Dispatchers: Dispatchers.IO is available on the JVM and, in recent coroutines versions, on native too, but in older setups it was not in common code. Choose Dispatchers.Default (or an injected dispatcher) unless your coroutines version documents IO as available in common code.

Android builder

kotlin
// androidMain
import android.content.Context
import androidx.room3.Room
import androidx.room3.RoomDatabase
fun getDatabaseBuilder(context: Context): RoomDatabase.Builder<AppDatabase> {
 val appContext = context.applicationContext
 val dbFile = appContext.getDatabasePath("fees.db")
 return Room.databaseBuilder<AppDatabase>(
 context = appContext,
 name = dbFile.absolutePath
 )
}

iOS builder

kotlin
// iosMain
import androidx.room3.Room
import androidx.room3.RoomDatabase
import kotlinx.cinterop.ExperimentalForeignApi
import platform.Foundation.NSDocumentDirectory
import platform.Foundation.NSFileManager
import platform.Foundation.NSUserDomainMask
fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
 val dbFilePath = documentDirectory() + "/fees.db"
 return Room.databaseBuilder<AppDatabase>(name = dbFilePath)
}
@OptIn(ExperimentalForeignApi::class)
private fun documentDirectory(): String {
 val documentDirectory = NSFileManager.defaultManager.URLForDirectory(
 directory = NSDocumentDirectory,
 inDomain = NSUserDomainMask,
 appropriateForURL = null,
 create = false,
 error = null
 )
 return requireNotNull(documentDirectory?.path)
}

The two functions have the same name and a different signature (Android needs a Context), so they are not an expect/actual pair; each platform app calls its own. That is fine: the app wiring, not shared code, creates the database and passes the finished AppDatabase into shared classes.

Wiring it in

kotlin
// Android app
val database = getRoomDatabase(getDatabaseBuilder(applicationContext))
val repository = DefaultPaymentsRepository(database.paymentDao(), ...)
swift
// iOS app (Swift), via a small Kotlin helper in iosMain that wraps the builder
let database = DatabaseFactory().create() // calls getRoomDatabase(getDatabaseBuilder())

With Hilt in the Android app (lesson 65), the @Provides function from lesson 68 simply returns getRoomDatabase(getDatabaseBuilder(context)).

✏️ Your Task
Practise what you just learned about Step-by-Step Build 3: Platform-Specific Builders. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 7 of 12
Step-by-Step Build 4: Migrating an Existing Database

If the app is already in the Play Store with users, their data lives in a Room 2.x (or Room 3) database file on Android. Moving to KMP must not lose it.

Concern What to do
Schema Keep the same tables, columns and version. Do not change the schema during the move
Database file name and location Use the same path on Android (getDatabasePath("fees.db") matches the old name)
Driver change Moving to the bundled driver changes the SQLite engine, not the file format; SQLite files are portable across versions
Existing migrations Keep your Migration objects and register them with addMigrations(...) in the common builder (migrations written with the driver-based SQLiteConnection API work on all platforms; older ones using SupportSQLiteDatabase must be rewritten)
Auto migrations @Database(autoMigrations = ...) continues to work with exported schemas
Testing Open a saved copy of an old database on a device and verify the data

A migration written for KMP uses the driver connection API:

kotlin
// commonMain
import androidx.room3.migration.Migration
import androidx.sqlite.SQLiteConnection
import androidx.sqlite.execSQL
val MIGRATION_1_2 = object : Migration(1, 2) {
 override fun migrate(connection: SQLiteConnection) {
 connection.execSQL("ALTER TABLE payments ADD COLUMN receiptNumber TEXT")
 }
}
fun getRoomDatabase(builder: RoomDatabase.Builder<AppDatabase>): AppDatabase =
 builder
 .setDriver(BundledSQLiteDriver())
 .setQueryCoroutineContext(Dispatchers.Default)
 .addMigrations(MIGRATION_1_2)
 .build()

(Migration.migrate takes a SQLiteConnection in the driver-based API; the older SupportSQLiteDatabase signature is Android-only. Check the migration guide for your Room version.)

Roll out carefully: release the KMP-based Android app to a small percentage of users first (staged rollout, lesson 49) and watch for database crashes before going wider.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 4: Migrating an Existing Database. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 8 of 12
Step-by-Step Build 5: Testing the Database from Common Code

Room KMP lets you test in commonTest with an in-memory database, which runs on every target:

kotlin
// shared/src/androidHostTest or a platform source set that has a driver and a builder
// For a common test you need an in-memory builder from each platform; the pattern:
// commonTest
expect fun createInMemoryDatabase(): AppDatabase
kotlin
// androidHostTest (needs AndroidX test core to supply a Context) or androidDeviceTest
actual fun createInMemoryDatabase(): AppDatabase =
 Room.inMemoryDatabaseBuilder<AppDatabase>(ApplicationProvider.getApplicationContext())
 .setDriver(BundledSQLiteDriver())
 .setQueryCoroutineContext(Dispatchers.Unconfined)
 .build()
kotlin
// commonTest
class PaymentDaoTest {
 private lateinit var db: AppDatabase
 @BeforeTest fun setUp() { db = createInMemoryDatabase() }
 @AfterTest fun tearDown() { db.close() }
 @Test
 fun upsert_then_observe_returns_rows() = runTest {
 db.paymentDao().upsertAll(listOf(PaymentEntity(1, "Ada", 150_000_00, 0)))
 val rows = db.paymentDao().observeAll().first()
 assertEquals("Ada", rows.single().studentName)
 }
}

(The exact in-memory builder name and parameters differ by Room version and platform. Check the testing section of the setup page. The key idea is that your DAO tests are shared, and only the way to create the database differs.)

Other persistence in KMP

  • DataStore (preferences or typed) also supports multiplatform, using a file path per platform, in the same style as Room's builder.
  • SQLDelight is a popular alternative to Room that generates Kotlin from SQL and has long supported KMP. Choose it if you prefer writing SQL files, or if you started before Room supported KMP.
  • Whichever you use, the repository from lesson 62 stays the same, because it depends on interfaces.
✏️ Your Task
Practise what you just learned about Step-by-Step Build 5: Testing the Database from Common Code. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 9 of 12
Step-by-Step Build 6: Programme Review (The KMP Quiz Topics)

The programme ends with a quiz on the Kotlin Multiplatform basics. Use this checklist, and answer each in your own words before looking at the notes:

  1. What is KMP, and what does it share? Logic across targets; UI optional.
  2. How is common code compiled for iOS? Kotlin/Native to a framework.
  3. What are source sets, and which one can see which? Platform sets see common; never the reverse.
  4. expect/actual versus interfaces? Stateless facts versus services.
  5. Which test library works in common code? kotlin.test.
  6. How does Android Studio create a shared module? New Module wizard, "Kotlin Multiplatform Shared Module".
  7. What task does Xcode call for direct integration? embedAndSignAppleFrameworkForXcode.
  8. What does klibs.io do? Searches multiplatform libraries by supported targets.
  9. What makes Room portable? The SQLiteDriver API, bundled SQLite, a database constructor, KSP per target.
  10. Why a bundled driver? Identical SQLite behaviour across platforms.

If you can answer all ten, you have the whole pathway.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 6: Programme Review (The KMP Quiz Topics). Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 10 of 12
Guided Practice Exercises
🎯 Your Challenge

Exercise 1: Debug the crash

The app builds, runs on Android, and crashes on iOS with a message that the database implementation cannot be found. List the two most likely missing pieces.

(1) The Room compiler was not added for the iOS targets (kspIosArm64 and kspIosSimulatorArm64), so no implementation was generated. (2) The @ConstructedBy annotation or the expect object ... : RoomDatabaseConstructor is missing, so Room cannot instantiate the database without reflection.

🎯 Your Challenge

Exercise 2: Common or platform?

For each, say commonMain or a platform source set: (a) @Dao interface; (b) Room.databaseBuilder(context, path); (c) getRoomDatabase(builder); (d) the iOS document directory lookup.

(a) commonMain. (b) androidMain (it needs a Context). (c) commonMain, because it only configures the builder. (d) iosMain, because it uses Apple's Foundation APIs.

🎯 Your Challenge

Exercise 3: Protect user data

You are shipping the KMP version of an app whose users already have data. Give four precautions.

(1) Keep the same schema and version, with no schema changes in the same release. (2) Keep the same database file name and location on Android. (3) Keep and test all existing migrations, rewriting any that use the old Android-only API. (4) Test with a saved copy of a real old database, then use a staged rollout and watch crash and database error rates. Back up or export data if the app has a way to do so.

✏️ Your Task
Practise what you just learned about Guided Practice Exercises. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 11 of 12
Common Mistakes (and How to Fix Them)

Mistake #1: Adding KSP for only one target. Fix: add the Room compiler for every target.

Mistake #2: No @ConstructedBy constructor. Fix: declare the expect object and the annotation.

Mistake #3: Leaving java.* types in entities. Fix: use primitives or multiplatform types.

Mistake #4: Forgetting the schema directory. Fix: configure it with the Room plugin and commit the files.

Mistake #5: Changing the schema while moving to KMP. Fix: move first, change later.

Mistake #6: Using the Android framework driver in common code. Fix: use the bundled driver.

Mistake #7: Creating several database instances. Fix: build one and share it.

Mistake #8: Old migrations using Android-only types. Fix: rewrite them with the driver connection API.

Mistake #9: Testing only on Android. Fix: share DAO tests and run them on each target.

Mistake #10: Copying setup code from an old blog. Fix: check the current official page.

✏️ Your Task
Practise what you just learned about Common Mistakes (and How to Fix Them). Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 12 of 12
Knowledge Check

Question 1. What is the role of BundledSQLiteDriver?

It ships SQLite inside the app so the same SQLite behaviour runs on every platform, and it works in common code.

Question 2. Why do we add the Room compiler to several KSP configurations?

Code must be generated for each target, so each needs the processor.

Question 3. What does @ConstructedBy solve?

It tells Room how to instantiate the generated database on platforms without reflection, via a generated actual of the RoomDatabaseConstructor.

Question 4. Which part of the setup is platform-specific?

The database builder, because the file location is different on each platform.

Question 5. What should stay the same during the migration?

The schema, version, file name and location on Android, and the user's data.

Go deeper: official resources

✏️ Your Task
Practise what you just learned about Knowledge Check. Open Android Studio, type the examples by hand, change a value, and observe what happens.
🏗️ Build It — Mini Project
A Shared Room Database for FeeTracker

Project goal: Move the FeeTracker database into the shared module and prove it works on Android, with iOS configured.

Requirements:

  1. Add the Room runtime, bundled driver, KSP and the Room Gradle plugin to :shared, with the compiler on every target's KSP configuration.
  2. Move PaymentEntity, PaymentDao and AppDatabase into commonMain, with the @ConstructedBy constructor.
  3. Write getRoomDatabase(builder) in common code using BundledSQLiteDriver.
  4. Write the Android builder in androidMain and the iOS builder in iosMain.
  5. Provide the database to the Android app through Hilt and replace the old Android-only database class.
  6. Keep the schema, version and file name identical, and add one migration (version 1 to 2, a nullable receiptNumber column) written with the driver connection API.
  7. Write DAO tests that run against an in-memory database, and a migration test that opens an exported version-1 schema.
  8. Write a short rollout plan for existing users.
  9. Answer the ten quiz topics from Build 6 in your own words.

Stretch goal: replace the Android-only DataStore in your app with multiplatform DataStore and share your settings repository.

The rollout plan (example):

text
Before release
 - Schema, version and file name unchanged (fees.db, version 1) -> then migration to 2 in a later release
 - Verified: open a copy of a production-style database from the previous app version; all rows present
 - All unit tests and DAO tests green on Android; iOS tests green in macOS CI

Release
 - Staged rollout: 5% -> 20% -> 50% -> 100% over a week
 - Watch Play Console / Crashlytics for SQLiteException and migration errors

Rollback
 - Halt the rollout; previous version remains installable (database format unchanged)

The Hilt provider:

kotlin
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
 @Provides @Singleton
 fun provideDatabase(@ApplicationContext context: Context): AppDatabase =
 getRoomDatabase(getDatabaseBuilder(context))
 @Provides
 fun providePaymentDao(db: AppDatabase): PaymentDao = db.paymentDao()
}

An in-memory DAO test and a quiz answer example:

kotlin
@Test
fun clear_removes_all_rows() = runTest {
 val dao = db.paymentDao()
 dao.upsertAll(listOf(PaymentEntity(1, "Ada", 100, 0), PaymentEntity(2, "Tunde", 200, 0)))
 dao.clear()
 assertTrue(dao.observeAll().first().isEmpty())
}
text
Q9 (What makes Room portable?)
Room talks to a small SQLiteDriver interface instead of Android classes, so it can run on any platform
that has a driver. The bundled driver ships SQLite with the app for identical behaviour. KSP generates
the Room code for each target, and an expect object with @ConstructedBy lets Room create the database
on platforms without reflection.
1

Open your finished project in Android Studio. Check that the .gitignore file the wizard created is in the project root. It keeps local.properties, build folders and keystores out of Git. Never commit passwords or signing keys.

2

Choose Git > Create Git Repository… (or VCS > Enable Version Control Integration in older versions), select the project folder, then open Git > Commit, tick every file, and write the message “Add Android lesson 70 project”.

3

On github.com click New repository, name it android-lesson-70-migrate-room-database-to-kotlin-multiplatf, set it to Public, leave Add a README unticked, and click Create repository.

4

Back in Android Studio choose Git > Manage Remotes…, add the repository URL as origin, then choose Git > Push… and push your main branch. Prefer the terminal? Run git remote add origin <your-url> then git push -u origin main.

5

Add a README.md with a screenshot of your app running and a short description. Employers and clients read READMEs first. Your project is now part of your public portfolio. 🎉

Lesson 70 complete! 🎉

You covered:

  • Lesson Introduction
  • Prerequisite Concepts
  • Conceptual Understanding: What Room Needs to Be Portable
  • Step-by-Step Build 1: Dependencies and Plugins
  • Step-by-Step Build 2: Move Entities, DAOs and the Database
  • Step-by-Step Build 3: Platform-Specific Builders
  • Step-by-Step Build 4: Migrating an Existing Database
  • Step-by-Step Build 5: Testing the Database from Common Code
  • Step-by-Step Build 6: Programme Review (The KMP Quiz Topics)
  • Guided Practice Exercises
  • Common Mistakes (and How to Fix Them)
  • Knowledge Check
All 72 lessons in Google Android Studio ·Techbase Academy ·© 2025–2026 Techbase Consultant Services, Ibadan
Esc
↑ ↓ to moveEnter to openEsc to close