12. Migrate business logic (Room and VM)

Daniyar Nurgaliyev
5 min readJun 23, 2024

--

Overview

This document outlines the steps to migrate the business logic related to Room database and ViewModel from an Android-specific module to a shared module that can be utilized across both Android and iOS platforms. This migration aims to centralize the business logic, facilitating code reuse and consistency across platforms.

Plan for Migrating Business Logic (Room and ViewModel)

AndroidApp Module

Remove Room Database and MainViewModel:

Delete Room Database Files:

  • Delete files related to the Room database from the data folder.
  • This step ensures that the database logic is no longer tied to the Android-specific implementation.

Move MainViewModel:

  • Transfer the MainViewModel to the shared module.
  • This allows the ViewModel to be used across both Android and iOS platforms, promoting code reuse.

Rename Module:

  • Rename the appModule to reflect its purpose of storing ViewModel dependency injection only.

iOSApp Module

Initialize Koin DI:

  • Set up Koin Dependency Injection in the iOS app.
  • This setup is necessary to manage dependencies in the iOS context, aligning with the shared module’s DI configuration.

Shared Module

commonMain:

  • Create a data directory to store Room logic moved from the androidApp module.
  • Adjust the moved MainViewModel to fit the shared module context.
  • Initialize Koin Dependency Injection.

androidMain and iosMain:

  • Create and initialize the database.
  • For iosMain, additionally create a separate Koin DI file to handle platform-specific requirements.

Step-by-Step Execution

Step 1: Room Database Files Migration

User Entity:

By defining this entity in the shared module, we ensure that both Android and iOS platforms have a consistent data model.

…/shared/src/commonMain/kotlin/com/example/shared/data/User.kt

@Entity(tableName = "user_table")
data class User(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val name: String
)

UserDao Interface:

The DAO provides methods to perform database operations such as insert, update, delete, and query. Placing it in the shared module allows for shared database operations across platforms.

…/shared/src/commonMain/kotlin/com/example/shared/data/UserDao.kt

@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(user: User)

@Query("SELECT * FROM user_table ORDER BY name ASC")
fun getUsers(): Flow<List<User>>

@Update
suspend fun update(user: User)

@Delete
suspend fun delete(user: User)

@Query("DELETE FROM user_table")
suspend fun deleteAll()
}

UserRepository:

in case of UserRepository, we pass to constructor the database file, because we are injecting this file from different platform sources

…/shared/src/commonMain/kotlin/com/example/shared/data/UserRepository.kt

class UserRepository(private val database: UserDatabase) {


private val userDao: UserDao by lazy {
database.userDao()
}

val allUsers: Flow<List<User>> = userDao.getUsers()

suspend fun insert(user: User) {
userDao.insert(user)
}

suspend fun update(user: User) {
userDao.update(user)
}

suspend fun delete(user: User) {
userDao.delete(user)
}

suspend fun deleteAll() {
userDao.deleteAll()
}
}

in case or creation of database we have to do it in every platform using specific driver and in shared module we can keep logic of creating DAO and the builder.

…/shared/src/commonMain/kotlin/com/example/shared/data/SharedUserDatabase.kt

@Database(entities = [User::class], version = 1, exportSchema = false)
abstract class UserDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}

fun getRoomDatabase(
builder: RoomDatabase.Builder<UserDatabase>
): UserDatabase {
return builder
// .addMigrations(MIGRATIONS)
// .fallbackToDestructiveMigrationOnDowngrade()
.setDriver(BundledSQLiteDriver())
.setQueryCoroutineContext(Dispatchers.IO)
.build()
}

Android Database Builder

This function provides the database builder specific to Android.

…/shared/src/androidMain/kotlin/com/example/shared/data/UserDatabase.kt

fun getDatabaseBuilder(ctx: Context): RoomDatabase.Builder<UserDatabase> {
val appContext = ctx.applicationContext
val dbFile = appContext.getDatabasePath("my_room.db")
return Room.databaseBuilder<UserDatabase>(
context = appContext,
name = dbFile.absolutePath
)
}

fun getDatabase(ctx: Context): UserDatabase {
return getRoomDatabase(getDatabaseBuilder(ctx))
}

It sets up the database path and context for Android, ensuring the database is correctly initialized in the Android environment.

iOS Database Builder

This function provides the database builder specific to iOS.

…/shared/src/iosMain/kotlin/com/example/shared/data/UserDatabase.kt

fun getDatabaseBuilder(): RoomDatabase.Builder<UserDatabase> {
val dbFilePath = NSHomeDirectory() + "/my_room.db"
return Room.databaseBuilder<UserDatabase>(
name = dbFilePath,
factory = { UserDatabase::class.instantiateImpl() }
)
}

fun getDatabase(): UserDatabase {
return getRoomDatabase(getDatabaseBuilder())
}

It sets up the database path for iOS, ensuring the database is correctly initialized in the iOS environment.

2. Viewmodel file migration

Viewmodel should stay the same except need to change the package

/shared/src/commonMain/kotlin/com/example/shared/MainViewModel.kt

class MainViewModel(private val repository: UserRepository) : ViewModel() {
val allUsers: Flow<List<User>> = repository.allUsers

fun insert(user: User) = viewModelScope.launch {
repository.insert(user)
}

fun update(user: User) = viewModelScope.launch {
repository.update(user)
}

fun delete(user: User) = viewModelScope.launch {
repository.delete(user)
}

fun deleteAll() = viewModelScope.launch {
repository.deleteAll()
}
}

By placing the ViewModel in the shared module, we can utilize it across both platforms. It interacts with the UserRepository to perform data operations and expose data to the UI.

Step 3: Koin Injection for ViewModel and Room Files

1.shared Module:

We would create injection for UserRepository and MainViewModel.

Also, create an expect function. This is cross-platform specific, meaning if we declare the function as expect, then we expect it to be implemented in other platform-specific folders within the shared module.

…/shared/src/commonMain/kotlin/com/example/shared/di/SharedDi.kt

expect fun platformModule(): Module

val sharedKoinModules = module {
single<UserRepository> { UserRepository(get()) }
single<MainViewModel> { MainViewModel(get()) }
}

2. androidMain Module:

Here we implement it with the keyword actual.

…/shared/src/androidMain/kotlin/com/example/shared/di/DI.kt

actual fun platformModule() = module {
single<UserDatabase> { getDatabase(get()) }
}

3. iosMain Module:

Here as well with the keyword actual.

…/shared/src/iosMain/kotlin/com/example/shared/di/DI.kt

actual fun platformModule() = module {
single<UserDatabase> { getDatabase() }
}

Additionally, in iOS, we need to separately implement DI.

…/shared/src/iosMain/kotlin/com/example/shared/KoinInitializer.kt

fun initKoin() {
startKoin {
modules(platformModule() + sharedKoinModules)
}

class MainInjector: KoinComponent {
}
}

Step 4: Adjust androidApp module

Adjust DI Module:

Rename AppModule.kt to ViewModelsModule.kt:

This renaming reflects the purpose of the module, which is now focused on ViewModel dependency injection.

…/androidApp/src/main/java/com/example/migrationtocmp/di/ViewModelsModule.kt

val viewModelModule = module {
viewModel { MainViewModel(get()) }
}

In MainActivity, ensure all imports are correctly adjusted to reflect the changes.

In MyApplication, add the required DI modules:

Modify the onCreate method to include the newly structured DI modules.

…/androidApp/src/main/java/com/example/migrationtocmp/MyApplication.kt

class MyApplication : Application() {
override fun onCreate() {
...
startKoin {
...
modules(platformModule() + sharedKoinModules + viewModelModule)
}
}
}

Conclusion

This migration centralizes business logic, facilitating code reuse and consistency across Android and iOS platforms. By moving the Room database and ViewModel to a shared module and setting up platform-specific DI, we achieve a streamlined and maintainable codebase that enhances development efficiency and ensures a unified approach across both platforms.

Unlisted

--

--