12. Migrate business logic (Room and VM)
--
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 theandroidApp
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.